mirror of
https://github.com/wassname/talk.git
synced 2026-07-07 06:24:34 +08:00
Merge branch 'next' into next-respect
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
+11
-6
@@ -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;
|
||||
+9
-7
@@ -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;
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
-3
@@ -6,6 +6,3 @@
|
||||
.footer:not(:empty) {
|
||||
margin-top: var(--spacing-unit);
|
||||
}
|
||||
.reply {
|
||||
margin-top: var(--spacing-unit);
|
||||
}
|
||||
+3
-1
@@ -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;
|
||||
-1
@@ -8,7 +8,6 @@ import IndentedComment from "./IndentedComment";
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof IndentedComment> = {
|
||||
indentLevel: 1,
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: "Marvin",
|
||||
},
|
||||
+5
-5
@@ -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();
|
||||
+3
-3
@@ -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;
|
||||
+43
@@ -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>
|
||||
`;
|
||||
+17
@@ -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>
|
||||
`;
|
||||
-1
@@ -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;
|
||||
+1
-5
@@ -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
Reference in New Issue
Block a user