Merge branch 'next' into next-respect

This commit is contained in:
Wyatt Johnson
2018-09-18 15:56:38 -06:00
committed by GitHub
208 changed files with 5834 additions and 1187 deletions
+5 -15
View File
@@ -2,11 +2,7 @@ import React from "react";
import { StatelessComponent } from "react";
import ReactDOM from "react-dom";
import {
createContext,
TalkContext,
TalkContextProvider,
} from "talk-framework/lib/bootstrap";
import { createManaged } from "talk-framework/lib/bootstrap";
import AppContainer from "./containers/AppContainer";
import resizePopup from "./dom/resizePopup";
@@ -32,23 +28,17 @@ function pollPopupHeight(interval: number = 100) {
}, interval);
}
// This is called when the context is first initialized.
async function init({ relayEnvironment }: TalkContext) {
await initLocalState(relayEnvironment);
}
async function main() {
// Bootstrap our context.
const context = await createContext({
init,
const ManagedTalkContextProvider = await createManaged({
initLocalState,
localesData,
userLocales: navigator.languages,
});
const Index: StatelessComponent = () => (
<TalkContextProvider value={context}>
<ManagedTalkContextProvider>
<AppContainer />
</TalkContextProvider>
</ManagedTalkContextProvider>
);
ReactDOM.render(<Index />, document.getElementById("app"));
@@ -216,7 +216,7 @@ exports[`navigates to sign up form 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -239,7 +239,7 @@ exports[`navigates to sign up form 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -34,11 +34,11 @@ exports[`accepts correct password 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -180,11 +180,11 @@ exports[`accepts valid email 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -290,11 +290,11 @@ exports[`checks for invalid email 1`] = `
value="invalid-email"
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -321,11 +321,11 @@ exports[`checks for invalid email 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -546,11 +546,11 @@ exports[`shows error when submitting empty form 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -577,11 +577,11 @@ exports[`shows error when submitting empty form 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -34,11 +34,11 @@ exports[`accepts correct password 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -56,7 +56,7 @@ exports[`accepts correct password 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -70,11 +70,11 @@ exports[`accepts correct password 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -92,7 +92,7 @@ exports[`accepts correct password 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -124,11 +124,11 @@ exports[`accepts correct password 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -215,11 +215,11 @@ exports[`accepts correct password confirmation 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -237,7 +237,7 @@ exports[`accepts correct password confirmation 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -251,11 +251,11 @@ exports[`accepts correct password confirmation 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -273,7 +273,7 @@ exports[`accepts correct password confirmation 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -287,11 +287,11 @@ exports[`accepts correct password confirmation 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -318,11 +318,11 @@ exports[`accepts correct password confirmation 1`] = `
value="testtest"
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -418,7 +418,7 @@ exports[`accepts valid email 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -432,11 +432,11 @@ exports[`accepts valid email 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -454,7 +454,7 @@ exports[`accepts valid email 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -468,11 +468,11 @@ exports[`accepts valid email 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -499,11 +499,11 @@ exports[`accepts valid email 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -590,11 +590,11 @@ exports[`accepts valid username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -612,7 +612,7 @@ exports[`accepts valid username 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -635,7 +635,7 @@ exports[`accepts valid username 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -649,11 +649,11 @@ exports[`accepts valid username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -680,11 +680,11 @@ exports[`accepts valid username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -771,11 +771,11 @@ exports[`checks for invalid characters in username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -793,7 +793,7 @@ exports[`checks for invalid characters in username 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -807,11 +807,11 @@ exports[`checks for invalid characters in username 1`] = `
value="$%$§$%$§%"
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -829,7 +829,7 @@ exports[`checks for invalid characters in username 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -843,11 +843,11 @@ exports[`checks for invalid characters in username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -874,11 +874,11 @@ exports[`checks for invalid characters in username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -965,11 +965,11 @@ exports[`checks for invalid email 1`] = `
value="invalid-email"
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -987,7 +987,7 @@ exports[`checks for invalid email 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -1001,11 +1001,11 @@ exports[`checks for invalid email 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1023,7 +1023,7 @@ exports[`checks for invalid email 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -1037,11 +1037,11 @@ exports[`checks for invalid email 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1068,11 +1068,11 @@ exports[`checks for invalid email 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1159,11 +1159,11 @@ exports[`checks for too long username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1181,7 +1181,7 @@ exports[`checks for too long username 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -1195,11 +1195,11 @@ exports[`checks for too long username 1`] = `
value="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1217,7 +1217,7 @@ exports[`checks for too long username 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -1231,11 +1231,11 @@ exports[`checks for too long username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1262,11 +1262,11 @@ exports[`checks for too long username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1353,11 +1353,11 @@ exports[`checks for too short password 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1375,7 +1375,7 @@ exports[`checks for too short password 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -1389,11 +1389,11 @@ exports[`checks for too short password 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1411,7 +1411,7 @@ exports[`checks for too short password 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -1425,11 +1425,11 @@ exports[`checks for too short password 1`] = `
value="pass"
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1456,11 +1456,11 @@ exports[`checks for too short password 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1547,11 +1547,11 @@ exports[`checks for too short username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1569,7 +1569,7 @@ exports[`checks for too short username 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -1583,11 +1583,11 @@ exports[`checks for too short username 1`] = `
value="u"
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1605,7 +1605,7 @@ exports[`checks for too short username 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -1619,11 +1619,11 @@ exports[`checks for too short username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1650,11 +1650,11 @@ exports[`checks for too short username 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1741,11 +1741,11 @@ exports[`checks for wrong password confirmation 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1763,7 +1763,7 @@ exports[`checks for wrong password confirmation 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -1777,11 +1777,11 @@ exports[`checks for wrong password confirmation 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1799,7 +1799,7 @@ exports[`checks for wrong password confirmation 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -1813,11 +1813,11 @@ exports[`checks for wrong password confirmation 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1844,11 +1844,11 @@ exports[`checks for wrong password confirmation 1`] = `
value="not-matching"
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -1944,7 +1944,7 @@ exports[`renders sign up form 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -1967,7 +1967,7 @@ exports[`renders sign up form 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -2077,11 +2077,11 @@ exports[`shows error when submitting empty form 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -2099,7 +2099,7 @@ exports[`shows error when submitting empty form 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -2113,11 +2113,11 @@ exports[`shows error when submitting empty form 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -2135,7 +2135,7 @@ exports[`shows error when submitting empty form 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -2149,11 +2149,11 @@ exports[`shows error when submitting empty form 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -2180,11 +2180,11 @@ exports[`shows error when submitting empty form 1`] = `
value=""
/>
<div
className="ValidationMessage-root ValidationMessage-colorError ValidationMessage-fullWidth"
className="Message-root Message-colorError Message-fullWidth"
>
<span
aria-hidden="true"
className="Icon-root ValidationMessage-icon Icon-sm"
className="Icon-root MessageIcon-root Icon-sm"
>
warning
</span>
@@ -2280,7 +2280,7 @@ exports[`shows server error 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -2303,7 +2303,7 @@ exports[`shows server error 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -2427,7 +2427,7 @@ exports[`shows server error 2`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -2450,7 +2450,7 @@ exports[`shows server error 2`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -2569,7 +2569,7 @@ exports[`submits form successfully 1`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -2592,7 +2592,7 @@ exports[`submits form successfully 1`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
@@ -2711,7 +2711,7 @@ exports[`submits form successfully 2`] = `
Username
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
A unique identifier displayed on your comments. You may use “_” and “.”
</p>
@@ -2734,7 +2734,7 @@ exports[`submits form successfully 2`] = `
Password
</label>
<p
className="Typography-root Typography-inputDescription Typography-colorTextSecondary"
className="Typography-root Typography-detail Typography-colorTextSecondary"
>
Must be at least 8 characters
</p>
+4 -1
View File
@@ -1,4 +1,6 @@
import { EventEmitter2 } from "eventemitter2";
import { IResolvers } from "graphql-tools";
import { noop } from "lodash";
import React from "react";
import TestRenderer from "react-test-renderer";
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
@@ -29,7 +31,6 @@ export default function create(params: CreateParams) {
logNetwork: params.logNetwork,
resolvers: params.resolvers,
initLocalState: (localRecord, source, env) => {
localRecord.setValue(0, "authRevision");
if (params.initLocalState) {
params.initLocalState(localRecord, source, env);
}
@@ -45,6 +46,8 @@ export default function create(params: CreateParams) {
postMessage: new PostMessageService(),
browserInfo: { ios: false },
uuidGenerator: createUUIDGenerator(),
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
clearSession: noop,
};
const testRenderer = TestRenderer.create(
+2 -5
View File
@@ -3,12 +3,9 @@ import { Environment, ROOT_ID } from "relay-runtime";
export default function getMe(environment: Environment) {
const source = environment.getStore().getSource();
const root = source.get(ROOT_ID)!;
const meKey = Object.keys(root)
.reverse()
.find(s => s.startsWith("me("))!;
if (!root[meKey]) {
if (!root.me) {
return null;
}
const meID = root[meKey].__ref;
const meID = root.me.__ref;
return source.get(meID)!;
}
@@ -1,3 +1,4 @@
import { EventEmitter2 } from "eventemitter2";
import { LocalizationProvider } from "fluent-react/compat";
import { FluentBundle } from "fluent/compat";
import { Child as PymChild } from "pym.js";
@@ -52,6 +53,12 @@ export interface TalkContext {
/** Generates uuids. */
uuidGenerator: () => string;
/** A event emitter */
eventEmitter: EventEmitter2;
/** Clear session data. */
clearSession: () => void;
}
const { Provider, Consumer } = React.createContext<TalkContext>({} as any);
@@ -1,147 +0,0 @@
import { EventEmitter2 } from "eventemitter2";
import { Localized } from "fluent-react/compat";
import { noop } from "lodash";
import { Child as PymChild } from "pym.js";
import React from "react";
import { Formatter } from "react-timeago";
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import uuid from "uuid/v4";
import { getBrowserInfo } from "talk-framework/lib/browserInfo";
import { LOCAL_ID } from "talk-framework/lib/relay";
import {
createLocalStorage,
createPromisifiedStorage,
createPymStorage,
createSessionStorage,
} from "talk-framework/lib/storage";
import { RestClient } from "talk-framework/lib/rest";
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
import { createFetch, TokenGetter } from "../network";
import { PostMessageService } from "../postMessage";
import { TalkContext } from "./TalkContext";
interface CreateContextArguments {
/** Locales that the user accepts, usually `navigator.languages`. */
userLocales: ReadonlyArray<string>;
/** Locales data that is returned by our `locales-loader`. */
localesData: LocalesData;
/** Init will be called after the context has been created. */
init?: ((context: TalkContext) => void | Promise<void>);
/** A pym child that interacts with the pym parent. */
pym?: PymChild;
/** Supports emitting and listening to events. */
eventEmitter?: EventEmitter2;
}
/**
* timeagoFormatter integrates timeago into our translation
* framework. It gets injected into the UIContext.
*/
export const timeagoFormatter: Formatter = (value, unit, suffix) => {
// We use 'in' instead of 'from now' for language consistency
const ourSuffix = suffix === "from now" ? "in" : suffix;
return (
<Localized
id="framework-timeago"
$value={value}
$unit={unit}
$suffix={ourSuffix}
>
<span>now</span>
</Localized>
);
};
function areWeInIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
/**
* `createContext` manages the dependencies of our framework
* and returns a `TalkContext` that can be passed to the
* `TalkContextProvider`.
*/
export default async function createContext({
init = noop,
userLocales,
localesData,
pym,
eventEmitter = new EventEmitter2({ wildcard: true }),
}: CreateContextArguments): Promise<TalkContext> {
const inIframe = areWeInIframe();
// Initialize Relay.
const source = new RecordSource();
const tokenGetter: TokenGetter = () => {
const localState = source.get(LOCAL_ID);
if (localState) {
return localState.authToken || "";
}
return "";
};
const relayEnvironment = new Environment({
network: Network.create(createFetch(tokenGetter)),
store: new Store(source),
});
// Listen for outside clicks.
let registerClickFarAway: ClickFarAwayRegister | undefined;
if (pym) {
registerClickFarAway = cb => {
pym.onMessage("click", cb);
// Return unlisten callback.
return () => {
const index = pym.messageHandlers.click.indexOf(cb);
if (index > -1) {
pym.messageHandlers.click.splice(index, 1);
}
};
};
}
// Initialize i18n.
const locales = negotiateLanguages(userLocales, localesData);
if (process.env.NODE_ENV !== "production") {
// tslint:disable:next-line: no-console
console.log(`Negotiated locales ${JSON.stringify(locales)}`);
}
const localeBundles = await generateBundles(locales, localesData);
// Assemble context.
const context = {
relayEnvironment,
localeBundles,
timeagoFormatter,
pym,
eventEmitter,
registerClickFarAway,
rest: new RestClient("/api", tokenGetter),
postMessage: new PostMessageService(),
localStorage:
(pym && inIframe && createPymStorage(pym, "localStorage")) ||
createPromisifiedStorage(createLocalStorage()),
sessionStorage:
(pym && inIframe && createPymStorage(pym, "sessionStorage")) ||
createPromisifiedStorage(createSessionStorage()),
browserInfo: getBrowserInfo(),
uuidGenerator: uuid,
};
// Run custom initializations.
await init(context);
return context;
}
@@ -0,0 +1,261 @@
import { EventEmitter2 } from "eventemitter2";
import { Localized } from "fluent-react/compat";
import { noop } from "lodash";
import { Child as PymChild } from "pym.js";
import React, { Component, ComponentType } from "react";
import { Formatter } from "react-timeago";
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import uuid from "uuid/v4";
import { getBrowserInfo } from "talk-framework/lib/browserInfo";
import { LOCAL_ID } from "talk-framework/lib/relay";
import {
createLocalStorage,
createPromisifiedStorage,
createPymStorage,
createSessionStorage,
PromisifiedStorage,
} from "talk-framework/lib/storage";
import { RestClient } from "talk-framework/lib/rest";
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
import { createFetch, TokenGetter } from "../network";
import { PostMessageService } from "../postMessage";
import { TalkContext, TalkContextProvider } from "./TalkContext";
export type InitLocalState = ((
environment: Environment,
context: TalkContext
) => void | Promise<void>);
interface CreateContextArguments {
/** Locales that the user accepts, usually `navigator.languages`. */
userLocales: ReadonlyArray<string>;
/** Locales data that is returned by our `locales-loader`. */
localesData: LocalesData;
/** Init will be called after the context has been created. */
initLocalState?: InitLocalState;
/** A pym child that interacts with the pym parent. */
pym?: PymChild;
/** Supports emitting and listening to events. */
eventEmitter?: EventEmitter2;
}
/**
* timeagoFormatter integrates timeago into our translation
* framework. It gets injected into the UIContext.
*/
export const timeagoFormatter: Formatter = (value, unit, suffix) => {
// We use 'in' instead of 'from now' for language consistency
const ourSuffix = suffix === "from now" ? "noSuffix" : suffix;
if (unit === "second" && suffix === "ago") {
return (
<Localized id="framework-timeago-just-now">
<span>Just now</span>
</Localized>
);
}
return (
<Localized
id="framework-timeago"
$value={value}
$unit={unit}
$suffix={ourSuffix}
>
<span>now</span>
</Localized>
);
};
/**
* Returns true if we are in an iframe.
*/
function areWeInIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
function createRelayEnvironment() {
// Initialize Relay.
const source = new RecordSource();
const tokenGetter: TokenGetter = () => {
const localState = source.get(LOCAL_ID);
if (localState) {
return localState.authToken || "";
}
return "";
};
const environment = new Environment({
network: Network.create(createFetch(tokenGetter)),
store: new Store(source),
});
return { environment, tokenGetter };
}
function createRestAPI(tokenGetter: (() => string)) {
return new RestClient("/api", tokenGetter);
}
/**
* Returns a managed TalkContextProvider, that includes given context
* and handles context changes, e.g. when a user session changes.
*/
function createMangedTalkContextProvider(
context: TalkContext,
initLocalState: InitLocalState
) {
const ManagedTalkContextProvider = class extends Component<
{},
{ context: TalkContext }
> {
constructor(props: {}) {
super(props);
this.state = {
context: {
...context,
clearSession: this.clearSession,
},
};
}
// This is called every time a user session starts or ends.
private clearSession = async () => {
// Clear session storage.
this.state.context.sessionStorage.clear();
// Create a new context with a new Relay Environment.
const {
environment: newEnvironment,
tokenGetter: newTokenGetter,
} = createRelayEnvironment();
const newContext = {
...this.state.context,
relayEnvironment: newEnvironment,
rest: createRestAPI(newTokenGetter),
};
// Initialize local state.
await initLocalState(newContext.relayEnvironment, newContext);
// Propagate new context.
this.setState({
context: newContext,
});
};
public render() {
return (
<TalkContextProvider value={this.state.context}>
{this.props.children}
</TalkContextProvider>
);
}
};
return ManagedTalkContextProvider;
}
/**
* resolveLocalStorage decides which local storage to use in the context
*/
function resolveLocalStorage(pym?: PymChild): PromisifiedStorage {
if (pym && areWeInIframe()) {
// Use local storage over pym when we have pym and are in an iframe.
return createPymStorage(pym, "localStorage");
}
// Use promisified, prefixed local storage.
return createPromisifiedStorage(createLocalStorage());
}
/**
* resolveSessionStorage decides which session storage to use in the context
*/
function resolveSessionStorage(pym?: PymChild): PromisifiedStorage {
if (pym && areWeInIframe()) {
// Use session storage over pym when we have pym and are in an iframe.
return createPymStorage(pym, "sessionStorage");
}
// Use promisified, prefixed session storage.
return createPromisifiedStorage(createSessionStorage());
}
/**
* `createManaged` establishes the dependencies of our framework
* and returns a `ManagedTalkContextProvider` that provides the context
* to the rest of the application.
*/
export default async function createManaged({
initLocalState = noop,
userLocales,
localesData,
pym,
eventEmitter = new EventEmitter2({ wildcard: true, maxListeners: 20 }),
}: CreateContextArguments): Promise<ComponentType> {
// Listen for outside clicks.
let registerClickFarAway: ClickFarAwayRegister | undefined;
if (pym) {
registerClickFarAway = cb => {
pym.onMessage("click", cb);
// Return unlisten callback.
return () => {
const index = pym.messageHandlers.click.indexOf(cb);
if (index > -1) {
pym.messageHandlers.click.splice(index, 1);
}
};
};
}
// Initialize i18n.
const locales = negotiateLanguages(userLocales, localesData);
if (process.env.NODE_ENV !== "production") {
// tslint:disable:next-line: no-console
console.log(`Negotiated locales ${JSON.stringify(locales)}`);
}
const localeBundles = await generateBundles(locales, localesData);
const localStorage = resolveLocalStorage(pym);
const sessionStorage = resolveSessionStorage(pym);
const { environment, tokenGetter } = createRelayEnvironment();
// Assemble context.
const context: TalkContext = {
relayEnvironment: environment,
localeBundles,
timeagoFormatter,
pym,
eventEmitter,
registerClickFarAway,
rest: createRestAPI(tokenGetter),
postMessage: new PostMessageService(),
localStorage,
sessionStorage,
browserInfo: getBrowserInfo(),
uuidGenerator: uuid,
// Noop, this is later replaced by the
// managed TalkContextProvider.
clearSession: noop,
};
// Initialize local state.
await initLocalState(context.relayEnvironment, context);
// Returns a managed TalkContextProvider, that includes the above
// context and handles context changes, e.g. when a user session changes.
return createMangedTalkContextProvider(context, initLocalState);
}
@@ -1,3 +1,7 @@
export * from "./TalkContext";
export { default as createContext } from "./createContext";
export {
TalkContext,
TalkContextConsumer,
TalkContextProvider,
} from "./TalkContext";
export { default as createManaged } from "./createManaged";
export { default as withContext } from "./withContext";
@@ -0,0 +1,18 @@
import {
commitLocalUpdate,
Environment,
RecordSourceProxy,
} from "relay-runtime";
export default function commitLocalUpdatePromisified(
environment: Environment,
updater: (store: RecordSourceProxy) => Promise<void>
) {
return new Promise((resolve, reject) => {
commitLocalUpdate(environment, store => {
updater(store)
.then(() => resolve())
.catch(err => reject(err));
});
});
}
@@ -42,7 +42,7 @@ function createMutationContainer<T extends string, I, R>(
};
public render() {
const { relayEnvironment: _, ...rest } = this.props;
const { context: _, ...rest } = this.props;
const inject = {
[propName]: this.commit,
};
@@ -13,3 +13,6 @@ export {
commitMutationPromiseNormalized,
} from "./commitMutationPromise";
export { graphql } from "react-relay";
export {
default as commitLocalUpdatePromisified,
} from "./commitLocalUpdatePromisified";
@@ -39,6 +39,7 @@ export const LOCAL_ID = "client:root.local";
function withLocalStateContainer(
fragmentSpec: GraphQLTaggedNode
): InferableComponentEnhancer<{ local: _RefType<any> }> {
const fragment = (fragmentSpec as any).data().default;
return compose(
withContext(({ relayEnvironment }) => ({ relayEnvironment })),
hoistStatics((BaseComponent: React.ComponentType<any>) => {
@@ -49,9 +50,7 @@ function withLocalStateContainer(
);
private subscription: Disposable;
constructor(props: Props) {
super(props);
const fragment = (fragmentSpec as any).data().default;
private subscribe(environment: Environment) {
if (fragment.kind !== "Fragment") {
throw new Error("Expected fragment");
}
@@ -65,24 +64,44 @@ function withLocalStateContainer(
node: { selections: fragment.selections },
variables: {},
};
const snapshot = props.relayEnvironment.lookup(selector);
this.subscription = props.relayEnvironment.subscribe(
const snapshot = environment.lookup(selector);
this.subscription = environment.subscribe(
snapshot,
this.updateSnapshot
);
this.state = {
data: snapshot.data,
};
this.updateSnapshot(snapshot);
}
constructor(props: Props) {
super(props);
this.subscribe(props.relayEnvironment);
}
private updateSnapshot = (snapshot: CSnapshot<any>) => {
this.setState({ data: snapshot.data });
const nextState = { data: snapshot.data };
// State has not been initialized yet.
if (!this.state) {
this.state = nextState;
return;
}
this.setState(nextState);
};
public componentWillUnmount() {
private unsubscribe() {
this.subscription.dispose();
}
public componentWillReceiveProps(next: Props) {
if (this.props.relayEnvironment !== next.relayEnvironment) {
this.unsubscribe();
this.subscribe(next.relayEnvironment);
}
}
public componentWillUnmount() {
this.unsubscribe();
}
public render() {
const { relayEnvironment: _, ...rest } = this.props;
return <BaseComponent {...rest} local={this.state.data} />;
@@ -1,7 +1,9 @@
import { Environment, RecordSource } from "relay-runtime";
import sinon from "sinon";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createInMemoryStorage } from "talk-framework/lib/storage";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import { commit } from "./SetAuthTokenMutation";
@@ -15,21 +17,29 @@ beforeAll(() => {
});
});
it("Sets auth token to localStorage", () => {
const context = {
localStorage: createInMemoryStorage(),
it("Sets auth token to localStorage", async () => {
const clearSessionStub = sinon.stub();
const context: Partial<TalkContext> = {
localStorage: createPromisifiedStorage(),
clearSession: clearSessionStub,
};
const authToken = "auth token";
commit(environment, { authToken }, context as any);
await commit(environment, { authToken }, context as any);
expect(source.get(LOCAL_ID)!.authToken).toEqual(authToken);
expect(context.localStorage.getItem("authToken")).toEqual(authToken);
await expect(context.localStorage!.getItem("authToken")).resolves.toEqual(
authToken
);
expect(clearSessionStub.calledOnce).toBe(true);
});
it("Removes auth token from localStorage", () => {
const context = {
localStorage: createInMemoryStorage(),
it("Removes auth token from localStorage", async () => {
const clearSessionStub = sinon.stub();
const context: Partial<TalkContext> = {
localStorage: createPromisifiedStorage(),
clearSession: clearSessionStub,
};
localStorage.setItem("authToken", "tmp");
commit(environment, { authToken: null }, context as any);
expect(context.localStorage.getItem("authToken")).toBeNull();
await commit(environment, { authToken: null }, context as any);
await expect(context.localStorage!.getItem("authToken")).resolves.toBeNull();
expect(clearSessionStub.calledOnce).toBe(true);
});
@@ -1,7 +1,10 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { Environment } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { createMutationContainer } from "talk-framework/lib/relay";
import {
commitLocalUpdatePromisified,
createMutationContainer,
} from "talk-framework/lib/relay";
import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer";
export interface SetAuthTokenInput {
@@ -13,27 +16,18 @@ export type SetAuthTokenMutation = (input: SetAuthTokenInput) => Promise<void>;
export async function commit(
environment: Environment,
input: SetAuthTokenInput,
{ localStorage }: TalkContext
{ localStorage, clearSession }: TalkContext
) {
return commitLocalUpdate(environment, store => {
return await commitLocalUpdatePromisified(environment, async store => {
const record = store.get(LOCAL_ID)!;
record.setValue(input.authToken, "authToken");
if (input.authToken) {
localStorage.setItem("authToken", input.authToken);
await localStorage.setItem("authToken", input.authToken);
} else {
localStorage.removeItem("authToken");
await localStorage.removeItem("authToken");
}
// Increment auth revision to indicate a change in auth state.
record.setValue(record.getValue("authRevision") + 1, "authRevision");
// Force gc to trigger.
environment
.retain({
dataID: "tmp",
node: { selections: [] },
variables: {},
})
.dispose();
// Clear current session, as we are starting a new one.
clearSession();
});
}
+2 -10
View File
@@ -5,17 +5,9 @@ import { PropTypesOf } from "talk-framework/types";
import App from "./App";
it("renders stream", () => {
it("renders comments", () => {
const props: PropTypesOf<typeof App> = {
showPermalinkView: false,
};
const wrapper = shallow(<App {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders permalink view", () => {
const props: PropTypesOf<typeof App> = {
showPermalinkView: true,
activeTab: "COMMENTS",
};
const wrapper = shallow(<App {...props} />);
expect(wrapper).toMatchSnapshot();
+10 -8
View File
@@ -3,20 +3,22 @@ import { StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import PermalinkViewQuery from "../queries/PermalinkViewQuery";
import StreamQuery from "../queries/StreamQuery";
import CommentsPaneContainer from "../tabs/comments/containers/CommentsPaneContainer";
import * as styles from "./App.css";
export interface AppProps {
showPermalinkView: boolean;
activeTab: "COMMENTS" | "%future added value";
}
const App: StatelessComponent<AppProps> = props => {
const view = props.showPermalinkView ? (
<PermalinkViewQuery />
) : (
<StreamQuery />
);
let view: React.ReactElement<any>;
switch (props.activeTab) {
case "COMMENTS":
view = <CommentsPaneContainer />;
break;
default:
throw new Error(`Unknown tab ${props.activeTab}`);
}
return (
<Flex justifyContent="center" className={styles.root}>
{view}
@@ -1,38 +0,0 @@
import React, { ReactElement, StatelessComponent } from "react";
import { Flex } from "talk-ui/components";
import * as styles from "./Comment.css";
import HTMLContent from "./HTMLContent";
import Timestamp from "./Timestamp";
import TopBar from "./TopBar";
import Username from "./Username";
export interface CommentProps {
id: string;
className?: string;
author: {
username: string | null;
} | null;
body: string | null;
createdAt: string;
footer?: ReactElement<any> | Array<ReactElement<any>>;
}
const Comment: StatelessComponent<CommentProps> = props => {
return (
<div role="article" className={styles.root}>
<TopBar className={styles.topBar}>
{props.author &&
props.author.username && <Username>{props.author.username}</Username>}
<Timestamp>{props.createdAt}</Timestamp>
</TopBar>
<HTMLContent>{props.body || ""}</HTMLContent>
<Flex className={styles.footer} direction="row" itemGutter="half">
{props.footer}
</Flex>
</div>
);
};
export default Comment;
@@ -1,22 +0,0 @@
import React from "react";
import dompurify from "dompurify";
import styles from "./HTMLContent.css";
interface ContentProps {
children: string;
}
class HTMLContent extends React.Component<ContentProps> {
public render() {
const { children } = this.props;
return (
<div
className={styles.root}
dangerouslySetInnerHTML={{ __html: dompurify.sanitize(children) }}
/>
);
}
}
export default HTMLContent;
@@ -1,27 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders username and body 1`] = `
<div
className="Comment-root"
role="article"
>
<TopBar
className="Comment-topBar"
>
<Username>
Marvin
</Username>
<Timestamp>
1995-12-17T03:24:00.000Z
</Timestamp>
</TopBar>
<HTMLContent>
Woof
</HTMLContent>
<withPropsOnChange(Flex)
className="Comment-footer"
direction="row"
itemGutter="half"
/>
</div>
`;
@@ -1 +0,0 @@
export { default, default as IndentedComment } from "./IndentedComment";
@@ -0,0 +1,17 @@
import dompurify from "dompurify";
import React, { StatelessComponent } from "react";
import styles from "./HTMLContent.css";
interface HTMLContentProps {
children: string;
}
const HTMLContent: StatelessComponent<HTMLContentProps> = ({ children }) => (
<div
className={styles.root}
dangerouslySetInnerHTML={{ __html: dompurify.sanitize(children) }}
/>
);
export default HTMLContent;
@@ -20,7 +20,7 @@ const UserBoxAuthenticated: StatelessComponent<
return (
<Flex itemGutter="half" wrap>
<Localized
id="comments-userBoxAuthenticated-signedInAs"
id="general-userBoxAuthenticated-signedInAs"
username={<Username />}
>
<Typography variant="bodyCopy" container="div">
@@ -28,7 +28,7 @@ const UserBoxAuthenticated: StatelessComponent<
</Typography>
</Localized>
<Localized
id="comments-userBoxAuthenticated-notYou"
id="general-userBoxAuthenticated-notYou"
button={
<Button
color="primary"
@@ -17,7 +17,7 @@ const UserBoxUnauthenticated: StatelessComponent<
return (
<Flex itemGutter alignItems="center">
<MatchMedia gteWidth="sm">
<Localized id="comments-userBoxUnauthenticated-joinTheConversation">
<Localized id="general-userBoxUnauthenticated-joinTheConversation">
<Typography
className={styles.joinText}
variant="bodyCopyBold"
@@ -30,7 +30,7 @@ const UserBoxUnauthenticated: StatelessComponent<
|
</Typography>
</MatchMedia>
<Localized id="comments-userBoxUnauthenticated-signIn">
<Localized id="general-userBoxUnauthenticated-signIn">
<Button
color="primary"
size="small"
@@ -40,7 +40,7 @@ const UserBoxUnauthenticated: StatelessComponent<
Sign in
</Button>
</Localized>
<Localized id="comments-userBoxUnauthenticated-register">
<Localized id="general-userBoxUnauthenticated-register">
<Button
color="primary"
size="small"
@@ -1,19 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders permalink view 1`] = `
exports[`renders comments 1`] = `
<withPropsOnChange(Flex)
className="App-root"
justifyContent="center"
>
<withContext(withLocalStateContainer(PermalinkViewQuery)) />
</withPropsOnChange(Flex)>
`;
exports[`renders stream 1`] = `
<withPropsOnChange(Flex)
className="App-root"
justifyContent="center"
>
<withContext(withLocalStateContainer(StreamQuery)) />
<withContext(withLocalStateContainer(CommentsPaneContainer)) />
</withPropsOnChange(Flex)>
`;
@@ -6,7 +6,7 @@ exports[`renders correctly 1`] = `
wrap={true}
>
<Localized
id="comments-userBoxAuthenticated-signedInAs"
id="general-userBoxAuthenticated-signedInAs"
username={<Username />}
>
<withPropsOnChange(Typography)
@@ -25,7 +25,7 @@ exports[`renders correctly 1`] = `
variant="underlined"
/>
}
id="comments-userBoxAuthenticated-notYou"
id="general-userBoxAuthenticated-notYou"
>
<withPropsOnChange(Typography)
container={[Function]}
@@ -9,7 +9,7 @@ exports[`renders correctly 1`] = `
gteWidth="sm"
>
<Localized
id="comments-userBoxUnauthenticated-joinTheConversation"
id="general-userBoxUnauthenticated-joinTheConversation"
>
<withPropsOnChange(Typography)
className="UserBoxUnauthenticated-joinText"
@@ -27,7 +27,7 @@ exports[`renders correctly 1`] = `
</withPropsOnChange(Typography)>
</MatchMediaWithContext>
<Localized
id="comments-userBoxUnauthenticated-signIn"
id="general-userBoxUnauthenticated-signIn"
>
<withPropsOnChange(Button)
color="primary"
@@ -39,7 +39,7 @@ exports[`renders correctly 1`] = `
</withPropsOnChange(Button)>
</Localized>
<Localized
id="comments-userBoxUnauthenticated-register"
id="general-userBoxUnauthenticated-register"
>
<withPropsOnChange(Button)
color="primary"
@@ -11,15 +11,15 @@ interface InnerProps {
}
const AppContainer: StatelessComponent<InnerProps> = ({
local: { commentID },
local: { activeTab },
}) => {
return <App showPermalinkView={!!commentID} />;
return <App activeTab={activeTab} />;
};
const enhanced = withLocalStateContainer(
graphql`
fragment AppContainerLocal on Local {
commentID
activeTab
}
`
)(AppContainer);
+19 -25
View File
@@ -3,46 +3,40 @@ import React from "react";
import { StatelessComponent } from "react";
import ReactDOM from "react-dom";
import {
createContext,
TalkContext,
TalkContextProvider,
} from "talk-framework/lib/bootstrap";
import { createManaged } from "talk-framework/lib/bootstrap";
import AppContainer from "./containers/AppContainer";
import {
onPostMessageAuthError,
onPostMessageSetAuthToken,
onPymSetCommentID,
OnPostMessageAuthError,
OnPostMessageSetAuthToken,
OnPymSetCommentID,
} from "./listeners";
import { initLocalState } from "./local";
import localesData from "./locales";
const listeners = [
onPymSetCommentID,
onPostMessageSetAuthToken,
onPostMessageAuthError,
];
// This is called when the context is first initialized.
async function init(context: TalkContext) {
await initLocalState(context.relayEnvironment, context);
listeners.forEach(f => f(context));
}
const listeners = (
<>
<OnPymSetCommentID />
<OnPostMessageSetAuthToken />
<OnPostMessageAuthError />
</>
);
async function main() {
// Bootstrap our context.
const context = await createContext({
init,
const ManagedTalkContextProvider = await createManaged({
initLocalState,
localesData,
userLocales: navigator.languages,
pym: new PymChild({ polling: 100 }),
});
const Index: StatelessComponent = () => (
<TalkContextProvider value={context}>
<AppContainer />
</TalkContextProvider>
<ManagedTalkContextProvider>
<>
{listeners}
<AppContainer />
</>
</ManagedTalkContextProvider>
);
ReactDOM.render(<Index />, document.getElementById("app"));
@@ -0,0 +1,28 @@
import { Component } from "react";
import { withContext } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
interface Props {
postMessage: PostMessageService;
}
class OnPostMessageAuthError extends Component<Props> {
constructor(props: Props) {
super(props);
// Auth popup will use this to send back errors during login.
props.postMessage!.on("authError", error => {
// tslint:disable-next-line:no-console
console.error(error);
});
}
public render() {
return null;
}
}
const enhanced = withContext(({ postMessage }) => ({ postMessage }))(
OnPostMessageAuthError
);
export default enhanced;
@@ -1,10 +1,14 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import React from "react";
import { Environment, RecordSource } from "relay-runtime";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createInMemoryStorage } from "talk-framework/lib/storage";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import onPostMessageSetAuthToken from "./onPostMessageSetAuthToken";
import { OnPostMessageSetAuthToken } from "./OnPostMessageSetAuthToken";
let relayEnvironment: Environment;
const source: RecordSource = new RecordSource();
@@ -17,16 +21,17 @@ beforeAll(() => {
it("Sets auth token", () => {
const token = "auth-token";
const context = {
const context: Partial<TalkContext> = {
postMessage: {
on: (name: string, cb: (token: string) => void) => {
expect(name).toBe("setAuthToken");
cb(token);
},
},
} as any,
relayEnvironment,
localStorage: createInMemoryStorage(),
localStorage: createPromisifiedStorage(),
clearSession: noop,
};
onPostMessageSetAuthToken(context as any);
shallow(<OnPostMessageSetAuthToken context={context as any} />);
expect(source.get(LOCAL_ID)!.authToken).toEqual(token);
});
@@ -0,0 +1,31 @@
import { Component } from "react";
import { TalkContext, withContext } from "talk-framework/lib/bootstrap";
import { commit as setAuthToken } from "talk-framework/mutations/SetAuthTokenMutation";
interface Props {
context: TalkContext;
}
export class OnPostMessageSetAuthToken extends Component<Props> {
constructor(props: Props) {
super(props);
// Auth popup will use this to handle a successful login.
props.context.postMessage!.on("setAuthToken", (authToken: string) => {
setAuthToken(
this.props.context.relayEnvironment,
{ authToken },
this.props.context
);
});
}
public render() {
return null;
}
}
const enhanced = withContext(context => ({ context }))(
OnPostMessageSetAuthToken
);
export default enhanced;
@@ -1,9 +1,11 @@
import { shallow } from "enzyme";
import React from "react";
import { Environment, RecordSource } from "relay-runtime";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import onPymSetCommentID from "./onPymSetCommentID";
import { OnPymSetCommentID } from "./OnPymSetCommentID";
let relayEnvironment: Environment;
const source: RecordSource = new RecordSource();
@@ -16,30 +18,30 @@ beforeAll(() => {
it("Sets comment id", () => {
const id = "comment1-id";
const context = {
const props = {
pym: {
onMessage: (eventName: string, cb: (id: string) => void) => {
expect(eventName).toBe("setCommentID");
cb(id);
},
},
} as any,
relayEnvironment,
};
onPymSetCommentID(context as any);
shallow(<OnPymSetCommentID {...props} />);
expect(source.get(LOCAL_ID)!.commentID).toEqual(id);
});
it("Sets comment id to null when empty", () => {
const id = "";
const context = {
const props = {
pym: {
onMessage: (eventName: string, cb: (data: string) => void) => {
expect(eventName).toBe("setCommentID");
cb(id);
},
},
} as any,
relayEnvironment,
};
onPymSetCommentID(context as any);
shallow(<OnPymSetCommentID {...props} />);
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
});
@@ -0,0 +1,39 @@
import { Child } from "pym.js";
import { Component } from "react";
import { commitLocalUpdate } from "react-relay";
import { Environment } from "relay-runtime";
import { withContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
interface Props {
relayEnvironment: Environment;
pym: Child;
}
export class OnPymSetCommentID extends Component<Props> {
constructor(props: Props) {
super(props);
// Sets comment id through pym.
props.pym!.onMessage("setCommentID", raw => {
commitLocalUpdate(this.props.relayEnvironment, s => {
const id = raw || null;
if (s.get(LOCAL_ID)!.getValue("commentID") !== id) {
s.get(LOCAL_ID)!.setValue(id, "commentID");
}
});
});
}
public render() {
return null;
}
}
const enhanced = withContext(({ relayEnvironment, pym }) => ({
relayEnvironment,
pym,
}))(OnPymSetCommentID);
export default enhanced;
+4 -4
View File
@@ -1,5 +1,5 @@
export { default as onPymSetCommentID } from "./onPymSetCommentID";
export { default as OnPymSetCommentID } from "./OnPymSetCommentID";
export {
default as onPostMessageSetAuthToken,
} from "./onPostMessageSetAuthToken";
export { default as onPostMessageAuthError } from "./onPostMessageAuthError";
default as OnPostMessageSetAuthToken,
} from "./OnPostMessageSetAuthToken";
export { default as OnPostMessageAuthError } from "./OnPostMessageAuthError";
@@ -1,11 +0,0 @@
import { TalkContext } from "talk-framework/lib/bootstrap";
export default function onPostMessageSetAuthToken({
postMessage,
}: TalkContext) {
// Auth popup will use this to send back errors during login.
postMessage!.on("authError", error => {
// tslint:disable-next-line:no-console
console.error(error);
});
}
@@ -1,11 +0,0 @@
import { TalkContext } from "talk-framework/lib/bootstrap";
import { commit as setAuthToken } from "talk-framework/mutations/SetAuthTokenMutation";
export default function onPostMessageSetAuthToken(ctx: TalkContext) {
const { relayEnvironment, postMessage } = ctx;
// Auth popup will use this to handle a successful login.
postMessage!.on("setAuthToken", (authToken: string) => {
setAuthToken(relayEnvironment, { authToken }, ctx);
});
}
@@ -1,19 +0,0 @@
import { commitLocalUpdate } from "react-relay";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
export default function onPymSetCommentID({
relayEnvironment,
pym,
}: TalkContext) {
// Sets comment id through pym.
pym!.onMessage("setCommentID", raw => {
commitLocalUpdate(relayEnvironment, s => {
const id = raw || null;
if (s.get(LOCAL_ID)!.getValue("commentID") !== id) {
s.get(LOCAL_ID)!.setValue(id, "commentID");
}
});
});
}
@@ -13,13 +13,13 @@ exports[`init local state 1`] = `
\\"__id\\": \\"client:root.local\\",
\\"__typename\\": \\"Local\\",
\\"authToken\\": \\"\\",
\\"authRevision\\": 0,
\\"network\\": {
\\"__ref\\": \\"client:root.local.network\\"
},
\\"authPopup\\": {
\\"__ref\\": \\"client:root.local.authPopup\\"
}
},
\\"activeTab\\": \\"COMMENTS\\"
},
\\"client:root.local.network\\": {
\\"__id\\": \\"client:root.local.network\\",
@@ -35,9 +35,6 @@ export default async function initLocalState(
// Set auth token
localRecord.setValue(authToken || "", "authToken");
// Set initial auth revision, this is increment whenenver auth state might have changed.
localRecord.setValue(0, "authRevision");
// Parse query params
const query = qs.parse(location.search);
@@ -78,5 +75,8 @@ export default async function initLocalState(
authPopupRecord.setValue(false, "focus");
authPopupRecord.setValue("", "href");
localRecord.setLinkedRecord(authPopupRecord, "authPopup");
// Set active tab
localRecord.setValue("COMMENTS", "activeTab");
});
}
+5 -4
View File
@@ -9,6 +9,10 @@ enum View {
FORGOT_PASSWORD
}
enum Tab {
COMMENTS
}
type AuthPopup {
open: Boolean!
focus: Boolean!
@@ -26,10 +30,7 @@ type Local {
commentID: String
authPopup: AuthPopup!
authToken: String
# Used to invalidate the `me` endpoint.
# This is incremented whenever the auth status
# might have changed.
authRevision: Int!
activeTab: Tab!
}
extend type Query {
@@ -93,6 +93,9 @@ function commit(
username: me.username,
},
body: input.body,
editing: {
editableUntil: new Date(Date.now() + 10000),
},
},
},
clientMutationId: (clientMutationId++).toString(),
@@ -0,0 +1,62 @@
import { graphql } from "react-relay";
import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
} from "talk-framework/lib/relay";
import { Omit } from "talk-framework/types";
import { EditCommentMutation as MutationTypes } from "talk-stream/__generated__/EditCommentMutation.graphql";
export type EditCommentInput = Omit<
MutationTypes["variables"]["input"],
"clientMutationId"
>;
const mutation = graphql`
mutation EditCommentMutation($input: EditCommentInput!) {
editComment(input: $input) {
comment {
body
editing {
edited
}
}
clientMutationId
}
}
`;
let clientMutationId = 0;
function commit(environment: Environment, input: EditCommentInput) {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation,
variables: {
input: {
...input,
clientMutationId: clientMutationId.toString(),
},
},
optimisticResponse: {
editComment: {
id: input.commentID,
body: input.body,
editing: {
edited: true,
},
clientMutationId: (clientMutationId++).toString(),
},
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
});
}
export const withEditCommentMutation = createMutationContainer(
"editComment",
commit
);
export type EditCommentMutation = (
input: EditCommentInput
) => Promise<MutationTypes["response"]["editComment"]>;
@@ -0,0 +1,21 @@
import { Environment, RecordSource } from "relay-runtime";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { createRelayEnvironment } from "talk-framework/testHelpers";
import { commit } from "./SetActiveTabMutation";
let environment: Environment;
const source: RecordSource = new RecordSource();
beforeAll(() => {
environment = createRelayEnvironment({
source,
});
});
it("Sets activeTab", () => {
const tab = "COMMENTS";
commit(environment, { tab });
expect(source.get(LOCAL_ID)!.activeTab).toEqual(tab);
});
@@ -0,0 +1,24 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { createMutationContainer, LOCAL_ID } from "talk-framework/lib/relay";
export interface SetActiveTabInput {
tab: "COMMENTS";
}
export type SetActiveTabMutation = (input: SetActiveTabInput) => Promise<void>;
export async function commit(
environment: Environment,
input: SetActiveTabInput
) {
return commitLocalUpdate(environment, store => {
const record = store.get(LOCAL_ID)!;
record.setValue(input.tab, "activeTab");
});
}
export const withSetActiveTabMutation = createMutationContainer(
"setActiveTab",
commit
);
@@ -3,6 +3,11 @@ export {
CreateCommentMutation,
CreateCommentInput,
} from "./CreateCommentMutation";
export {
withEditCommentMutation,
EditCommentMutation,
EditCommentInput,
} from "./EditCommentMutation";
export {
withSetNetworkStatusMutation,
SetNetworkStatusMutation,
@@ -21,3 +26,7 @@ export {
withSetAuthPopupStateMutation,
SetAuthPopupStateMutation,
} from "./SetAuthPopupStateMutation";
export {
withSetActiveTabMutation,
SetActiveTabMutation,
} from "./SetActiveTabMutation";
@@ -6,6 +6,3 @@
.footer:not(:empty) {
margin-top: var(--spacing-unit);
}
.reply {
margin-top: var(--spacing-unit);
}
@@ -7,12 +7,14 @@ import Comment from "./Comment";
it("renders username and body", () => {
const props: PropTypesOf<typeof Comment> = {
id: "comment-id",
author: {
username: "Marvin",
},
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
topBarRight: "topBarRight",
footer: "footer",
showEditedMarker: true,
};
const wrapper = shallow(<Comment {...props} />);
expect(wrapper).toMatchSnapshot();
@@ -0,0 +1,52 @@
import React, { StatelessComponent } from "react";
import HTMLContent from "talk-stream/components/HTMLContent";
import Timestamp from "talk-stream/components/Timestamp";
import { Flex } from "talk-ui/components";
import * as styles from "./Comment.css";
import EditedMarker from "./EditedMarker";
import TopBarLeft from "./TopBarLeft";
import Username from "./Username";
export interface CommentProps {
className?: string;
author: {
username: string | null;
} | null;
body: string | null;
createdAt: string;
topBarRight?: React.ReactNode;
footer?: React.ReactNode;
showEditedMarker?: boolean;
}
const Comment: StatelessComponent<CommentProps> = props => {
return (
<div role="article" className={styles.root}>
<Flex
className={styles.topBar}
direction="row"
justifyContent="space-between"
>
<TopBarLeft>
{props.author &&
props.author.username && (
<Username>{props.author.username}</Username>
)}
<Flex direction="row" alignItems="baseline" itemGutter>
<Timestamp>{props.createdAt}</Timestamp>
{props.showEditedMarker && <EditedMarker />}
</Flex>
</TopBarLeft>
{props.topBarRight && <div>{props.topBarRight}</div>}
</Flex>
<HTMLContent>{props.body || ""}</HTMLContent>
<Flex className={styles.footer} direction="row" itemGutter="half">
{props.footer}
</Flex>
</div>
);
};
export default Comment;
@@ -0,0 +1,3 @@
.root {
composes: detail from "talk-ui/shared/typography.css";
}
@@ -0,0 +1,9 @@
import { shallow } from "enzyme";
import React from "react";
import EditedMarker from "./EditedMarker";
it("renders correctly", () => {
const wrapper = shallow(<EditedMarker />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,16 @@
import { Localized } from "fluent-react/compat";
import React, { StatelessComponent } from "react";
import styles from "./EditedMarker.css";
const EditedMarker: StatelessComponent = () => (
<div className={styles.root}>
(
<Localized id="comments-editedMarker-edited">
<span>Edited</span>
</Localized>
)
</div>
);
export default EditedMarker;
@@ -8,7 +8,6 @@ import IndentedComment from "./IndentedComment";
it("renders correctly", () => {
const props: PropTypesOf<typeof IndentedComment> = {
indentLevel: 1,
id: "comment-id",
author: {
username: "Marvin",
},
@@ -4,10 +4,10 @@ import TestRenderer from "react-test-renderer";
import { PropTypesOf } from "talk-framework/types";
import { UIContext, UIContextProps } from "talk-ui/components";
import TopBar from "./TopBar";
import TopBarLeft from "./TopBarLeft";
it("renders correctly on small screens", () => {
const props: PropTypesOf<typeof TopBar> = {
const props: PropTypesOf<typeof TopBarLeft> = {
children: <div>Hello World</div>,
};
@@ -19,14 +19,14 @@ it("renders correctly on small screens", () => {
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<TopBar {...props} />
<TopBarLeft {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("renders correctly on big screens", () => {
const props: PropTypesOf<typeof TopBar> = {
const props: PropTypesOf<typeof TopBarLeft> = {
children: <div>Hello World</div>,
};
@@ -38,7 +38,7 @@ it("renders correctly on big screens", () => {
const testRenderer = TestRenderer.create(
<UIContext.Provider value={context}>
<TopBar {...props} />
<TopBarLeft {...props} />
</UIContext.Provider>
);
expect(testRenderer.toJSON()).toMatchSnapshot();
@@ -4,12 +4,12 @@ import { StatelessComponent } from "react";
import { Flex, MatchMedia } from "talk-ui/components";
export interface TopBarProps {
export interface TopBarLeftProps {
className?: string;
children: React.ReactNode;
}
const TopBar: StatelessComponent<TopBarProps> = props => {
const TopBarLeft: StatelessComponent<TopBarLeftProps> = props => {
const rootClassName = cn(props.className);
return (
<MatchMedia gtWidth="xs">
@@ -27,4 +27,4 @@ const TopBar: StatelessComponent<TopBarProps> = props => {
);
};
export default TopBar;
export default TopBarLeft;
@@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders username and body 1`] = `
<div
className="Comment-root"
role="article"
>
<withPropsOnChange(Flex)
className="Comment-topBar"
direction="row"
justifyContent="space-between"
>
<TopBarLeft>
<Username>
Marvin
</Username>
<withPropsOnChange(Flex)
alignItems="baseline"
direction="row"
itemGutter={true}
>
<Timestamp>
1995-12-17T03:24:00.000Z
</Timestamp>
<EditedMarker />
</withPropsOnChange(Flex)>
</TopBarLeft>
<div>
topBarRight
</div>
</withPropsOnChange(Flex)>
<HTMLContent>
Woof
</HTMLContent>
<withPropsOnChange(Flex)
className="Comment-footer"
direction="row"
itemGutter="half"
>
footer
</withPropsOnChange(Flex)>
</div>
`;
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div
className="EditedMarker-root"
>
(
<Localized
id="comments-editedMarker-edited"
>
<span>
Edited
</span>
</Localized>
)
</div>
`;
@@ -13,7 +13,6 @@ exports[`renders correctly 1`] = `
}
body="Woof"
createdAt="1995-12-17T03:24:00.000Z"
id="comment-id"
/>
</Indent>
`;
@@ -0,0 +1,3 @@
export { default, default as IndentedComment } from "./IndentedComment";
export { default as TopBarLeft } from "./TopBarLeft";
export { default as Username } from "./Username";
@@ -0,0 +1,22 @@
import { shallow } from "enzyme";
import React from "react";
import { PropTypesOf } from "talk-framework/types";
import CommentsPane from "./CommentsPane";
it("renders stream", () => {
const props: PropTypesOf<typeof CommentsPane> = {
showPermalinkView: false,
};
const wrapper = shallow(<CommentsPane {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders permalink view", () => {
const props: PropTypesOf<typeof CommentsPane> = {
showPermalinkView: true,
};
const wrapper = shallow(<CommentsPane {...props} />);
expect(wrapper).toMatchSnapshot();
});
@@ -0,0 +1,15 @@
import * as React from "react";
import { StatelessComponent } from "react";
import PermalinkViewQuery from "../queries/PermalinkViewQuery";
import StreamQuery from "../queries/StreamQuery";
export interface CommentsPaneProps {
showPermalinkView: boolean;
}
const CommentsPane: StatelessComponent<CommentsPaneProps> = props => {
return props.showPermalinkView ? <PermalinkViewQuery /> : <StreamQuery />;
};
export default CommentsPane;
@@ -0,0 +1,162 @@
import { CoralRTE } from "@coralproject/rte";
import { Localized } from "fluent-react/compat";
import React, {
EventHandler,
MouseEvent,
Ref,
StatelessComponent,
} from "react";
import { Field, Form } from "react-final-form";
import { OnSubmit } from "talk-framework/lib/form";
import { required } from "talk-framework/lib/validation";
import Timestamp from "talk-stream/components/Timestamp";
import {
AriaInfo,
Button,
Flex,
HorizontalGutter,
Message,
MessageIcon,
RelativeTime,
Typography,
ValidationMessage,
} from "talk-ui/components";
import { TopBarLeft, Username } from "./Comment";
import RTE from "./RTE";
interface FormProps {
body: string;
}
export interface EditCommentFormProps {
id: string;
className?: string;
author: {
username: string | null;
} | null;
createdAt: string;
editableUntil: string;
onSubmit: OnSubmit<FormProps>;
onCancel?: EventHandler<MouseEvent<any>>;
onClose?: EventHandler<MouseEvent<any>>;
initialValues?: FormProps;
rteRef?: Ref<CoralRTE>;
expired?: boolean;
}
const EditCommentForm: StatelessComponent<EditCommentFormProps> = props => {
const inputID = `comments-editCommentForm-rte-${props.id}`;
return (
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
{({ handleSubmit, submitting, hasValidationErrors, pristine }) => (
<form
className={props.className}
autoComplete="off"
onSubmit={handleSubmit}
id={`comments-editCommentForm-form-${props.id}`}
>
<HorizontalGutter>
<div>
<TopBarLeft>
{props.author &&
props.author.username && (
<Username>{props.author.username}</Username>
)}
<Timestamp>{props.createdAt}</Timestamp>
</TopBarLeft>
</div>
<Field name="body" validate={required}>
{({ input, meta }) => (
<div>
<Localized id="comments-editCommentForm-rteLabel">
<AriaInfo component="label" htmlFor={inputID}>
Edit comment
</AriaInfo>
</Localized>
<Localized
id="comments-editCommentForm-rte"
attrs={{ placeholder: true }}
>
<RTE
inputId={inputID}
onChange={({ html }) => input.onChange(html)}
value={input.value}
placeholder="Edit comment"
forwardRef={props.rteRef}
disabled={submitting || props.expired}
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<Typography align="right" color="error" gutterBottom>
{meta.error || meta.submitError}
</Typography>
)}
</div>
)}
</Field>
{props.expired ? (
<Localized id="comments-editCommentForm-editTimeExpired">
<ValidationMessage fullWidth>
Edit time has expired. You can no longer edit this comment.
Why not post another one?
</ValidationMessage>
</Localized>
) : (
<Message fullWidth>
<MessageIcon>alarm</MessageIcon>
<Localized
id="comments-editCommentForm-editRemainingTime"
time={<RelativeTime date={props.editableUntil} live />}
>
<span>{"Edit: <time></time> remaining"}</span>
</Localized>
</Message>
)}
<Flex direction="row" justifyContent="flex-end" itemGutter="half">
{props.expired ? (
<Localized id="comments-editCommentForm-close">
<Button
id={`comments-editCommentForm-closeButton-${props.id}`}
variant="outlined"
disabled={submitting}
onClick={props.onClose}
>
Close
</Button>
</Localized>
) : (
<>
<Localized id="comments-editCommentForm-cancel">
<Button
id={`comments-editCommentForm-cancelButton-${props.id}`}
variant="outlined"
disabled={submitting}
onClick={props.onCancel}
>
Cancel
</Button>
</Localized>
<Localized id="comments-editCommentForm-saveChanges">
<Button
color="primary"
variant="filled"
disabled={submitting || hasValidationErrors || pristine}
type="submit"
>
Save Changes
</Button>
</Localized>
</>
)}
</Flex>
</HorizontalGutter>
</form>
)}
</Form>
);
};
export default EditCommentForm;
@@ -14,11 +14,7 @@ const PoweredBy: StatelessComponent<Props> = props => {
id="comments-poweredBy"
logo={<Typography variant="heading4" container="span" />}
>
<Typography
variant="inputDescription"
container="span"
color="textSecondary"
>
<Typography variant="detail" container="span" color="textSecondary">
{"Powered by <logo>The Coral Project</logo>"}
</Typography>
</Localized>

Some files were not shown because too many files have changed in this diff Show More