mirror of
https://github.com/wassname/talk.git
synced 2026-10-04 13:10:23 +08:00
Merge branch 'next' into next-respect
This commit is contained in:
208 files changed
+5834
-1187
No files matched your search
@@ -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";
|
||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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)>
|
||||
`;
|
||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
-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",
|
||||
},
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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>
|
||||
`;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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>
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 208 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user