diff --git a/config/jest/client.config.js b/config/jest/client.config.js index d5cc83ce3..507257368 100644 --- a/config/jest/client.config.js +++ b/config/jest/client.config.js @@ -24,6 +24,7 @@ module.exports = { ], moduleNameMapper: { "^talk-admin/(.*)$": "/src/core/client/admin/$1", + "^talk-auth/(.*)$": "/src/core/client/auth/$1", "^talk-ui/(.*)$": "/src/core/client/ui/$1", "^talk-stream/(.*)$": "/src/core/client/stream/$1", "^talk-framework/(.*)$": "/src/core/client/framework/$1", diff --git a/config/watcher.ts b/config/watcher.ts index 55b11c4ce..299e744db 100644 --- a/config/watcher.ts +++ b/config/watcher.ts @@ -34,6 +34,23 @@ const config: Config = { runOnInit: true, }), }, + compileRelayAuth: { + paths: [ + "core/client/auth/**/*.ts", + "core/client/auth/**/*.tsx", + "core/client/auth/**/*.graphql", + "core/server/**/*.graphql", + ], + ignore: [ + "core/**/*.d.ts", + "core/**/*.graphql.ts", + "**/test/**/*", + "core/**/*.spec.*", + ], + executor: new CommandExecutor("npm run compile:relay-auth", { + runOnInit: true, + }), + }, compileCSSTypes: { paths: ["**/*.css"], executor: new CommandExecutor("npm run compile:css-types", { @@ -62,9 +79,15 @@ const config: Config = { "runWebpackDevServer", "compileCSSTypes", "compileRelayStream", + "compileRelayAuth", ], docz: ["runDocz", "compileCSSTypes"], - compile: ["compileSchema", "compileCSSTypes", "compileRelayStream"], + compile: [ + "compileSchema", + "compileCSSTypes", + "compileRelayStream", + "compileRelayAuth", + ], }, }; diff --git a/package.json b/package.json index 78668ab86..b697e4f15 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "compile": "npm-run-all --parallel compile:*", "compile:css-types": "tcm src/core/client/", "compile:relay-stream": "ts-node ./scripts/compileRelay --src ./src/core/client/stream --schema tenant", + "compile:relay-auth": "ts-node ./scripts/compileRelay --src ./src/core/client/auth --schema tenant", "compile:schema": "node ./scripts/generateSchemaTypes.js", "docz": "docz", "start": "node dist/index.js", diff --git a/src/core/build/paths.ts b/src/core/build/paths.ts index 680008813..bebff0969 100644 --- a/src/core/build/paths.ts +++ b/src/core/build/paths.ts @@ -25,12 +25,13 @@ export default { appStreamLocalesTemplate: resolveSrc("core/client/stream/locales.ts"), appStreamIndex: resolveSrc("core/client/stream/index.tsx"), + appAuthHTML: resolveSrc("core/client/auth/index.html"), + appAuthLocalesTemplate: resolveSrc("core/client/auth/locales.ts"), + appAuthIndex: resolveSrc("core/client/auth/index.tsx"), + appEmbedIndex: resolveSrc("core/client/embed/index.ts"), appEmbedHTML: resolveSrc("core/client/embed/index.html"), - appAuthIndex: resolveSrc("core/client/auth/index.tsx"), - appAuthHTML: resolveSrc("core/client/auth/index.html"), - appDistStatic: resolveApp("dist/static"), appPublic: resolveApp("public"), appPackageJson: resolveApp("package.json"), diff --git a/src/core/client/auth/components/App.css b/src/core/client/auth/components/App.css new file mode 100644 index 000000000..3f61a7cae --- /dev/null +++ b/src/core/client/auth/components/App.css @@ -0,0 +1,15 @@ +/* Here we add global stylings for body and document */ +:global { + body { + margin: "0"; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + } +} + +.root { + width: 100%; +} diff --git a/src/core/client/auth/components/App.tsx b/src/core/client/auth/components/App.tsx index 3e8c91e42..368c6d716 100644 --- a/src/core/client/auth/components/App.tsx +++ b/src/core/client/auth/components/App.tsx @@ -3,8 +3,20 @@ import { StatelessComponent } from "react"; import { Flex } from "talk-ui/components"; -const App: StatelessComponent = () => { - return Auth; +import * as styles from "./App.css"; + +export interface AppProps { + // TODO: (cvle) Remove %future added value when we have Relay 1.6 + // https://github.com/facebook/relay/commit/1e87e43add7667a494f7ff4cfa7f03f1ab8d81a2 + view: "SIGN_UP" | "SIGN_IN" | "FORGOT_PASSWORD" | "%future added value"; +} + +const App: StatelessComponent = props => { + return ( + + Current View: {props.view} + + ); }; export default App; diff --git a/src/core/client/auth/containers/AppContainer.tsx b/src/core/client/auth/containers/AppContainer.tsx index d1cf2e5b9..b100bfc74 100644 --- a/src/core/client/auth/containers/AppContainer.tsx +++ b/src/core/client/auth/containers/AppContainer.tsx @@ -1,10 +1,25 @@ import * as React from "react"; import { StatelessComponent } from "react"; +import { AppContainerLocal as Local } from "talk-auth/__generated__/AppContainerLocal.graphql"; +import { graphql, withLocalStateContainer } from "talk-framework/lib/relay"; + import App from "../components/App"; -const AppContainer: StatelessComponent = () => { - return ; +interface InnerProps { + local: Local; +} + +const AppContainer: StatelessComponent = ({ local: { view } }) => { + return ; }; -export default AppContainer; +const enhanced = withLocalStateContainer( + graphql` + fragment AppContainerLocal on Local { + view + } + ` +)(AppContainer); + +export default enhanced; diff --git a/src/core/client/auth/index.tsx b/src/core/client/auth/index.tsx index 12bb6f8ea..44970a3fa 100644 --- a/src/core/client/auth/index.tsx +++ b/src/core/client/auth/index.tsx @@ -1,17 +1,26 @@ -import React, { StatelessComponent } from "react"; +import React from "react"; +import { StatelessComponent } from "react"; import ReactDOM from "react-dom"; import { createContext, + TalkContext, TalkContextProvider, } from "talk-framework/lib/bootstrap"; import AppContainer from "./containers/AppContainer"; +import { initLocalState } from "./local"; import localesData from "./locales"; +// 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, localesData, userLocales: navigator.languages, }); diff --git a/src/core/client/auth/local/__snapshots__/initLocalState.spec.ts.snap b/src/core/client/auth/local/__snapshots__/initLocalState.spec.ts.snap new file mode 100644 index 000000000..25e900e0c --- /dev/null +++ b/src/core/client/auth/local/__snapshots__/initLocalState.spec.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`init local state 1`] = ` +"{ + \\"client:root\\": { + \\"__id\\": \\"client:root\\", + \\"__typename\\": \\"__Root\\", + \\"local\\": { + \\"__ref\\": \\"client:root.local\\" + } + }, + \\"client:root.local\\": { + \\"__id\\": \\"client:root.local\\", + \\"__typename\\": \\"Local\\", + \\"view\\": \\"SIGN_IN\\" + } +}" +`; diff --git a/src/core/client/auth/local/index.ts b/src/core/client/auth/local/index.ts new file mode 100644 index 000000000..2b196f8c4 --- /dev/null +++ b/src/core/client/auth/local/index.ts @@ -0,0 +1 @@ +export { default as initLocalState } from "./initLocalState"; diff --git a/src/core/client/auth/local/initLocalState.spec.ts b/src/core/client/auth/local/initLocalState.spec.ts new file mode 100644 index 000000000..bcf88006f --- /dev/null +++ b/src/core/client/auth/local/initLocalState.spec.ts @@ -0,0 +1,36 @@ +import { Environment, RecordSource } from "relay-runtime"; + +import { LOCAL_ID } from "talk-framework/lib/relay"; +import { createRelayEnvironment } from "talk-framework/testHelpers"; + +import initLocalState from "./initLocalState"; + +let environment: Environment; +let source: RecordSource; + +beforeEach(() => { + source = new RecordSource(); + environment = createRelayEnvironment({ + source, + initLocalState: false, + }); +}); + +it("init local state", () => { + initLocalState(environment); + expect(JSON.stringify(source.toJSON(), null, 2)).toMatchSnapshot(); +}); + +it("set view from query", () => { + const view = "SIGN_UP"; + const previousLocation = location.toString(); + const previousState = window.history.state; + window.history.replaceState( + previousState, + document.title, + `http://localhost/?view=${view}` + ); + initLocalState(environment); + expect(source.get(LOCAL_ID)!.view).toBe(view); + window.history.replaceState(previousState, document.title, previousLocation); +}); diff --git a/src/core/client/auth/local/initLocalState.ts b/src/core/client/auth/local/initLocalState.ts new file mode 100644 index 000000000..15e9edfbc --- /dev/null +++ b/src/core/client/auth/local/initLocalState.ts @@ -0,0 +1,28 @@ +import qs from "query-string"; +import { commitLocalUpdate, Environment } from "relay-runtime"; + +import { + createAndRetain, + LOCAL_ID, + LOCAL_TYPE, +} from "talk-framework/lib/relay"; + +/** + * Initializes the local state, before we start the App. + */ +export default async function initLocalState(environment: Environment) { + commitLocalUpdate(environment, s => { + const root = s.getRoot(); + + // Create the Local Record which is the Root for the client states. + const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE); + + // Parse query params + const query = qs.parse(location.search); + + // Set default view. + localRecord.setValue(query.view || "SIGN_IN", "view"); + + root.setLinkedRecord(localRecord, "local"); + }); +} diff --git a/src/core/client/auth/local/local.graphql b/src/core/client/auth/local/local.graphql new file mode 100644 index 000000000..c536037d3 --- /dev/null +++ b/src/core/client/auth/local/local.graphql @@ -0,0 +1,13 @@ +enum View { + SIGN_UP + SIGN_IN + FORGOT_PASSWORD +} + +type Local { + view: View! +} + +extend type Query { + local: Local! +} diff --git a/src/core/client/auth/mutations/SetViewMutation.spec.ts b/src/core/client/auth/mutations/SetViewMutation.spec.ts new file mode 100644 index 000000000..bb610981e --- /dev/null +++ b/src/core/client/auth/mutations/SetViewMutation.spec.ts @@ -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 "./SetViewMutation"; + +let environment: Environment; +const source: RecordSource = new RecordSource(); + +beforeAll(() => { + environment = createRelayEnvironment({ + source, + }); +}); + +it("Sets view", () => { + const view = "SIGN_UP"; + commit(environment, { view }, {} as any); + expect(source.get(LOCAL_ID)!.view).toEqual(view); +}); diff --git a/src/core/client/auth/mutations/SetViewMutation.ts b/src/core/client/auth/mutations/SetViewMutation.ts new file mode 100644 index 000000000..c5bb851fd --- /dev/null +++ b/src/core/client/auth/mutations/SetViewMutation.ts @@ -0,0 +1,25 @@ +import { commitLocalUpdate, Environment } from "relay-runtime"; + +import { TalkContext } from "talk-framework/lib/bootstrap"; +import { createMutationContainer } from "talk-framework/lib/relay"; +import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer"; + +export interface SetViewInput { + // TODO: replace with generated typescript types. + view: "SIGN_IN" | "SIGN_UP" | "FORGOT_PASSWORD"; +} + +export type SetViewMutation = (input: SetViewInput) => Promise; + +export async function commit( + environment: Environment, + input: SetViewInput, + { pym }: TalkContext +) { + return commitLocalUpdate(environment, store => { + const record = store.get(LOCAL_ID)!; + record.setValue(input.view, "view"); + }); +} + +export const withSetViewMutation = createMutationContainer("setView", commit); diff --git a/src/core/client/auth/mutations/index.ts b/src/core/client/auth/mutations/index.ts new file mode 100644 index 000000000..50d8d059b --- /dev/null +++ b/src/core/client/auth/mutations/index.ts @@ -0,0 +1 @@ +export { withSetViewMutation, SetViewMutation } from "./SetViewMutation"; diff --git a/src/core/client/embed/index.html b/src/core/client/embed/index.html index 297768c75..10106ff8c 100644 --- a/src/core/client/embed/index.html +++ b/src/core/client/embed/index.html @@ -6,6 +6,12 @@ + diff --git a/src/core/client/framework/testHelpers/createRelayEnvironment.ts b/src/core/client/framework/testHelpers/createRelayEnvironment.ts index 717c037ad..f7e035ddc 100644 --- a/src/core/client/framework/testHelpers/createRelayEnvironment.ts +++ b/src/core/client/framework/testHelpers/createRelayEnvironment.ts @@ -31,12 +31,19 @@ export interface CreateRelayEnvironmentNetworkParams { export interface CreateRelayEnvironmentParams { /** If set, creates a network to a local graphql server with a local schema */ network?: CreateRelayEnvironmentNetworkParams; - /** Allows to set initial state for Local state */ - initLocalState?: ( - local: RecordProxy, - source: RecordSourceProxy, - environment: Environment - ) => void; + /** + * Initializes an empty local state if true. + * When passing in a function it gets executed after + * the intialization. Defaults to true. + */ + initLocalState?: + | (( + local: RecordProxy, + source: RecordSourceProxy, + environment: Environment + ) => void) + | false + | true; /** Use this source for creating the environment */ source?: RecordSource; } @@ -61,18 +68,20 @@ export default function createRelayEnvironment( network, store: new Store(params.source || new RecordSource()), }); - commitLocalUpdate(environment, sourceProxy => { - const root = sourceProxy.getRoot(); - const localRecord = createAndRetain( - environment, - sourceProxy, - LOCAL_ID, - LOCAL_TYPE - ); - root.setLinkedRecord(localRecord, "local"); - if (params.initLocalState) { - params.initLocalState!(localRecord, sourceProxy, environment); - } - }); + if (params.initLocalState !== false) { + commitLocalUpdate(environment, sourceProxy => { + const root = sourceProxy.getRoot(); + const localRecord = createAndRetain( + environment, + sourceProxy, + LOCAL_ID, + LOCAL_TYPE + ); + root.setLinkedRecord(localRecord, "local"); + if (typeof params.initLocalState === "function") { + params.initLocalState!(localRecord, sourceProxy, environment); + } + }); + } return environment; } diff --git a/src/core/client/stream/components/App.css b/src/core/client/stream/components/App.css index 3f61a7cae..2bcabb67f 100644 --- a/src/core/client/stream/components/App.css +++ b/src/core/client/stream/components/App.css @@ -1,7 +1,8 @@ /* Here we add global stylings for body and document */ :global { body { - margin: "0"; + margin: 0; + padding: 2px 8px; /* Support for all WebKit browsers. */ -webkit-font-smoothing: antialiased; diff --git a/src/core/client/stream/components/Auth.css b/src/core/client/stream/components/Auth.css deleted file mode 100644 index aa42b9550..000000000 --- a/src/core/client/stream/components/Auth.css +++ /dev/null @@ -1,3 +0,0 @@ -.root { - padding: var(--spacing-unit) 0; -} diff --git a/src/core/client/stream/components/Auth.tsx b/src/core/client/stream/components/Auth.tsx deleted file mode 100644 index 168b30d8d..000000000 --- a/src/core/client/stream/components/Auth.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from "react"; -import { StatelessComponent } from "react"; -import * as styles from "./Auth.css"; - -import { Button, Flex, Popup, Typography } from "talk-ui/components"; - -interface AuthProps { - open: boolean; - focus: boolean; - openPopup: () => void; - closePopup: () => void; - setFocus: (focus: boolean) => void; -} - -const Auth: StatelessComponent = props => { - return ( -
- props.setFocus(true)} - onBlur={() => props.setFocus(false)} - onClose={props.closePopup} - /> - - Join the conversation - | - - - -
- ); -}; - -export default Auth; diff --git a/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap b/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap index 9253404bd..6c5348d0b 100644 --- a/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap +++ b/src/core/client/stream/components/Comment/__snapshots__/TopBar.spec.tsx.snap @@ -1,21 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly on big screens 1`] = ` -
-
- Hello World +
+
+
+ Hello World +
`; exports[`renders correctly on small screens 1`] = ` -
-
- Hello World +
+
+
+ Hello World +
`; diff --git a/src/core/client/stream/components/PermalinkButton/PermalinkButton.tsx b/src/core/client/stream/components/PermalinkButton/PermalinkButton.tsx index 493625e2b..aedf477e5 100644 --- a/src/core/client/stream/components/PermalinkButton/PermalinkButton.tsx +++ b/src/core/client/stream/components/PermalinkButton/PermalinkButton.tsx @@ -60,7 +60,7 @@ class Permalink extends React.Component { share - + Share diff --git a/src/core/client/stream/components/PermalinkButton/PermalinkPopover.tsx b/src/core/client/stream/components/PermalinkButton/PermalinkPopover.tsx index 218bd78f6..9e7afcc9d 100644 --- a/src/core/client/stream/components/PermalinkButton/PermalinkPopover.tsx +++ b/src/core/client/stream/components/PermalinkButton/PermalinkPopover.tsx @@ -38,15 +38,19 @@ class PermalinkPopover extends React.Component { const { copied } = this.state; return ( - + + + + + )} + {!comment && ( + + Comment not found + )} - {!comment && Comment not found} {comment && ( diff --git a/src/core/client/stream/components/PostCommentForm.css b/src/core/client/stream/components/PostCommentForm.css index 7e3ef2e15..e813f4ea7 100644 --- a/src/core/client/stream/components/PostCommentForm.css +++ b/src/core/client/stream/components/PostCommentForm.css @@ -1,3 +1,7 @@ +.root { + width: 100%; +} + .textarea { composes: bodyCopy from "talk-ui/shared/typography.css"; diff --git a/src/core/client/stream/components/PostCommentForm.tsx b/src/core/client/stream/components/PostCommentForm.tsx index badcbaca8..72bc0ad70 100644 --- a/src/core/client/stream/components/PostCommentForm.tsx +++ b/src/core/client/stream/components/PostCommentForm.tsx @@ -20,7 +20,7 @@ export interface PostCommentFormProps { const PostCommentForm: StatelessComponent = props => (
{({ handleSubmit, submitting }) => ( - + {({ input, meta }) => (
diff --git a/src/core/client/stream/components/Stream.tsx b/src/core/client/stream/components/Stream.tsx index 21f5a0fde..7f1ea183c 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -4,10 +4,10 @@ import { StatelessComponent } from "react"; import { Button, Flex } from "talk-ui/components"; -import AuthContainer from "../containers/AuthContainer"; import CommentContainer from "../containers/CommentContainer"; import PostCommentFormContainer from "../containers/PostCommentFormContainer"; import ReplyListContainer from "../containers/ReplyListContainer"; +import UserBoxContainer from "../containers/UserBoxContainer"; import * as styles from "./Stream.css"; export interface StreamProps { @@ -21,8 +21,8 @@ export interface StreamProps { const Stream: StatelessComponent = props => { return ( -
- + + = props => { )} -
+ ); }; diff --git a/src/core/client/stream/components/UserBoxUnauthenticated.css b/src/core/client/stream/components/UserBoxUnauthenticated.css new file mode 100644 index 000000000..272b33bdb --- /dev/null +++ b/src/core/client/stream/components/UserBoxUnauthenticated.css @@ -0,0 +1,3 @@ +.joinText { + margin-right: var(--spacing-unit); +} diff --git a/src/core/client/stream/components/UserBoxUnauthenticated.spec.tsx b/src/core/client/stream/components/UserBoxUnauthenticated.spec.tsx new file mode 100644 index 000000000..f9683c1e8 --- /dev/null +++ b/src/core/client/stream/components/UserBoxUnauthenticated.spec.tsx @@ -0,0 +1,16 @@ +import { shallow } from "enzyme"; +import { noop } from "lodash"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import UserBoxUnauthenticated from "./UserBoxUnauthenticated"; + +it("renders correctly", () => { + const props: PropTypesOf = { + onSignIn: noop, + onRegister: noop, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/components/UserBoxUnauthenticated.tsx b/src/core/client/stream/components/UserBoxUnauthenticated.tsx new file mode 100644 index 000000000..51e1e11b7 --- /dev/null +++ b/src/core/client/stream/components/UserBoxUnauthenticated.tsx @@ -0,0 +1,54 @@ +import { Localized } from "fluent-react/compat"; +import React, { StatelessComponent } from "react"; + +import { Button, Flex, Typography } from "talk-ui/components"; + +import * as styles from "./UserBoxUnauthenticated.css"; + +export interface UserBoxUnauthenticatedProps { + onSignIn: () => void; + onRegister: () => void; +} + +const UserBoxUnauthenticated: StatelessComponent< + UserBoxUnauthenticatedProps +> = props => { + return ( + + + + Join the conversation + + + + | + + + + + + + + + ); +}; + +export default UserBoxUnauthenticated; diff --git a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap index 5da869b02..84cb64ea1 100644 --- a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap +++ b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap @@ -1,9 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly 1`] = ` -
+ @@ -55,13 +58,16 @@ exports[`renders correctly 1`] = ` /> -
+ `; exports[`when there is more disables load more button 1`] = ` -
+ @@ -127,13 +133,16 @@ exports[`when there is more disables load more button 1`] = ` -
+ `; exports[`when there is more renders a load more button 1`] = ` -
+ @@ -199,5 +208,5 @@ exports[`when there is more renders a load more button 1`] = ` -
+ `; diff --git a/src/core/client/stream/components/__snapshots__/UserBoxUnauthenticated.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/UserBoxUnauthenticated.spec.tsx.snap new file mode 100644 index 000000000..e5b84fc16 --- /dev/null +++ b/src/core/client/stream/components/__snapshots__/UserBoxUnauthenticated.spec.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + + Join the conversation + + + + | + + + + Sign in + + + + + Register + + + +`; diff --git a/src/core/client/stream/containers/AuthContainer.tsx b/src/core/client/stream/containers/AuthContainer.tsx deleted file mode 100644 index 825e50493..000000000 --- a/src/core/client/stream/containers/AuthContainer.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from "react"; - -import Auth from "../components/Auth"; - -class AuthContainer extends React.Component { - public state = { - open: false, - focus: false, - }; - - public openPopup = () => { - this.setState({ - open: true, - }); - }; - - public closePopup = () => { - this.setState({ - open: false, - }); - }; - - public setFocus = (focus: boolean) => { - this.setState({ - focus, - }); - }; - - public render() { - const { open, focus } = this.state; - return ( - - ); - } -} - -export default AuthContainer; diff --git a/src/core/client/stream/containers/UserBoxContainer.spec.tsx b/src/core/client/stream/containers/UserBoxContainer.spec.tsx new file mode 100644 index 000000000..122b4e987 --- /dev/null +++ b/src/core/client/stream/containers/UserBoxContainer.spec.tsx @@ -0,0 +1,24 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import { UserBoxContainer } from "./UserBoxContainer"; + +it("renders correctly", () => { + const props: PropTypesOf = { + local: { + authPopup: { + open: false, + focus: false, + view: "SIGN_IN", + }, + }, + // tslint:disable-next-line:no-empty + showAuthPopup: async () => {}, + // tslint:disable-next-line:no-empty + setAuthPopupState: async () => {}, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/containers/UserBoxContainer.tsx b/src/core/client/stream/containers/UserBoxContainer.tsx new file mode 100644 index 000000000..359c93d97 --- /dev/null +++ b/src/core/client/stream/containers/UserBoxContainer.tsx @@ -0,0 +1,72 @@ +import * as React from "react"; +import { Component } from "react"; + +import { graphql, withLocalStateContainer } from "talk-framework/lib/relay"; +import { UserBoxContainerLocal as Local } from "talk-stream/__generated__/UserBoxContainerLocal.graphql"; +import { + SetAuthPopupStateMutation, + ShowAuthPopupMutation, + withSetAuthPopupStateMutation, + withShowAuthPopupMutation, +} from "talk-stream/mutations"; +import { Popup } from "talk-ui/components"; + +import UserBoxUnauthenticated from "../components/UserBoxUnauthenticated"; + +interface InnerProps { + local: Local; + showAuthPopup: ShowAuthPopupMutation; + setAuthPopupState: SetAuthPopupStateMutation; +} + +export class UserBoxContainer extends Component { + private handleFocus = () => this.props.setAuthPopupState({ focus: true }); + private handleBlur = () => this.props.setAuthPopupState({ focus: false }); + private handleClose = () => this.props.setAuthPopupState({ open: false }); + private handleSignIn = () => this.props.showAuthPopup({ view: "SIGN_IN" }); + private handleRegister = () => this.props.showAuthPopup({ view: "SIGN_UP" }); + + public render() { + const { + local: { + authPopup: { open, focus, view }, + }, + } = this.props; + return ( + <> + + + + ); + } +} + +const enhanced = withSetAuthPopupStateMutation( + withShowAuthPopupMutation( + withLocalStateContainer( + graphql` + fragment UserBoxContainerLocal on Local { + authPopup { + open + focus + view + } + } + ` + )(UserBoxContainer) + ) +); + +export default enhanced; diff --git a/src/core/client/stream/containers/__snapshots__/UserBoxContainer.spec.tsx.snap b/src/core/client/stream/containers/__snapshots__/UserBoxContainer.spec.tsx.snap new file mode 100644 index 000000000..c1fdd3d4c --- /dev/null +++ b/src/core/client/stream/containers/__snapshots__/UserBoxContainer.spec.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` + + + + +`; diff --git a/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap b/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap new file mode 100644 index 000000000..52ffec402 --- /dev/null +++ b/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`init local state 1`] = ` +"{ + \\"client:root\\": { + \\"__id\\": \\"client:root\\", + \\"__typename\\": \\"__Root\\", + \\"local\\": { + \\"__ref\\": \\"client:root.local\\" + } + }, + \\"client:root.local\\": { + \\"__id\\": \\"client:root.local\\", + \\"__typename\\": \\"Local\\", + \\"network\\": { + \\"__ref\\": \\"client:root.local.network\\" + }, + \\"authPopup\\": { + \\"__ref\\": \\"client:root.local.authPopup\\" + } + }, + \\"client:root.local.network\\": { + \\"__id\\": \\"client:root.local.network\\", + \\"__typename\\": \\"Network\\", + \\"isOffline\\": false + }, + \\"client:root.local.authPopup\\": { + \\"__id\\": \\"client:root.local.authPopup\\", + \\"__typename\\": \\"AuthPopup\\", + \\"open\\": false, + \\"focus\\": false, + \\"href\\": \\"\\" + } +}" +`; diff --git a/src/core/client/stream/local/constants.ts b/src/core/client/stream/local/constants.ts index 599c79497..d4f4bd473 100644 --- a/src/core/client/stream/local/constants.ts +++ b/src/core/client/stream/local/constants.ts @@ -4,3 +4,6 @@ export const NETWORK_TYPE = "Network"; export const NETWORK_ID = "client:root.local.network"; + +export const AUTH_POPUP_TYPE = "AuthPopup"; +export const AUTH_POPUP_ID = "client:root.local.authPopup"; diff --git a/src/core/client/stream/local/initLocalState.spec.ts b/src/core/client/stream/local/initLocalState.spec.ts new file mode 100644 index 000000000..d1842c9a5 --- /dev/null +++ b/src/core/client/stream/local/initLocalState.spec.ts @@ -0,0 +1,52 @@ +import { Environment, RecordSource } from "relay-runtime"; + +import { timeout } from "talk-common/utils"; +import { LOCAL_ID } from "talk-framework/lib/relay"; +import { createRelayEnvironment } from "talk-framework/testHelpers"; + +import initLocalState from "./initLocalState"; + +let environment: Environment; +let source: RecordSource; + +beforeEach(() => { + source = new RecordSource(); + environment = createRelayEnvironment({ + source, + initLocalState: false, + }); +}); + +it("init local state", async () => { + initLocalState(environment); + await timeout(); + expect(JSON.stringify(source.toJSON(), null, 2)).toMatchSnapshot(); +}); + +it("set assetID from query", () => { + const assetID = "asset-id"; + const previousLocation = location.toString(); + const previousState = window.history.state; + window.history.replaceState( + previousState, + document.title, + `http://localhost/?assetID=${assetID}` + ); + initLocalState(environment); + expect(source.get(LOCAL_ID)!.assetID).toBe(assetID); + window.history.replaceState(previousState, document.title, previousLocation); +}); + +it("set commentID from query", () => { + const commentID = "comment-id"; + const previousLocation = location.toString(); + const previousState = window.history.state; + window.history.replaceState( + previousState, + document.title, + `http://localhost/?commentID=${commentID}` + ); + initLocalState(environment); + expect(source.get(LOCAL_ID)!.commentID).toBe(commentID); + window.history.replaceState(previousState, document.title, previousLocation); +}); diff --git a/src/core/client/stream/local/initLocalState.ts b/src/core/client/stream/local/initLocalState.ts index 434c60e32..827e90c50 100644 --- a/src/core/client/stream/local/initLocalState.ts +++ b/src/core/client/stream/local/initLocalState.ts @@ -7,7 +7,12 @@ import { LOCAL_TYPE, } from "talk-framework/lib/relay"; -import { NETWORK_ID, NETWORK_TYPE } from "./constants"; +import { + AUTH_POPUP_ID, + AUTH_POPUP_TYPE, + NETWORK_ID, + NETWORK_TYPE, +} from "./constants"; /** * Initializes the local state, before we start the App. @@ -18,6 +23,7 @@ export default async function initLocalState(environment: Environment) { // Create the Local Record which is the Root for the client states. const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE); + root.setLinkedRecord(localRecord, "local"); // Parse query params const query = qs.parse(location.search); @@ -47,6 +53,17 @@ export default async function initLocalState(environment: Environment) { ); networkRecord.setValue(false, "isOffline"); localRecord.setLinkedRecord(networkRecord, "network"); - root.setLinkedRecord(localRecord, "local"); + + // Create authPopup Record + const authPopupRecord = createAndRetain( + environment, + s, + AUTH_POPUP_ID, + AUTH_POPUP_TYPE + ); + authPopupRecord.setValue(false, "open"); + authPopupRecord.setValue(false, "focus"); + authPopupRecord.setValue("", "href"); + localRecord.setLinkedRecord(authPopupRecord, "authPopup"); }); } diff --git a/src/core/client/stream/local/local.graphql b/src/core/client/stream/local/local.graphql index 9a2991607..52604d35b 100644 --- a/src/core/client/stream/local/local.graphql +++ b/src/core/client/stream/local/local.graphql @@ -3,11 +3,24 @@ type Network { isOffline: Boolean! } +enum View { + SIGN_UP + SIGN_IN + FORGOT_PASSWORD +} + +type AuthPopup { + open: Boolean! + focus: Boolean! + view: View +} + type Local { network: Network! assetID: String - commentID: String assetURL: String + commentID: String + authPopup: AuthPopup! } extend type Query { diff --git a/src/core/client/stream/mutations/SetAuthPopupStateMutation.spec.ts b/src/core/client/stream/mutations/SetAuthPopupStateMutation.spec.ts new file mode 100644 index 000000000..2e03b736c --- /dev/null +++ b/src/core/client/stream/mutations/SetAuthPopupStateMutation.spec.ts @@ -0,0 +1,52 @@ +import { Environment, RecordSource } from "relay-runtime"; + +import { createRelayEnvironment } from "talk-framework/testHelpers"; + +import { AUTH_POPUP_ID, AUTH_POPUP_TYPE } from "../local"; +import { commit } from "./SetAuthPopupStateMutation"; + +let environment: Environment; +const source: RecordSource = new RecordSource(); + +beforeAll(() => { + environment = createRelayEnvironment({ + source, + initLocalState: (localRecord, sourceProxy) => { + const record = sourceProxy.create(AUTH_POPUP_ID, AUTH_POPUP_TYPE); + record.setValue(false, "open"); + record.setValue(false, "focus"); + localRecord.setLinkedRecord(record, "authPopup"); + }, + }); +}); + +it("sets state", () => { + commit(environment, { open: true, focus: false }); + expect(source.get(AUTH_POPUP_ID)!.open).toEqual(true); + expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(false); +}); + +it("sets state", () => { + commit(environment, { + open: false, + focus: true, + href: "https://coralproject.net", + }); + expect(source.get(AUTH_POPUP_ID)!.open).toEqual(false); + expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(true); + expect(source.get(AUTH_POPUP_ID)!.href).toEqual("https://coralproject.net"); +}); + +it("keep previous state", () => { + commit(environment, { open: false, focus: true }); + commit(environment, {}); + expect(source.get(AUTH_POPUP_ID)!.open).toEqual(false); + expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(true); +}); + +it("change only one", () => { + commit(environment, { open: false, focus: true }); + commit(environment, { open: true }); + expect(source.get(AUTH_POPUP_ID)!.open).toEqual(true); + expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(true); +}); diff --git a/src/core/client/stream/mutations/SetAuthPopupStateMutation.ts b/src/core/client/stream/mutations/SetAuthPopupStateMutation.ts new file mode 100644 index 000000000..f3984a69a --- /dev/null +++ b/src/core/client/stream/mutations/SetAuthPopupStateMutation.ts @@ -0,0 +1,38 @@ +import { commitLocalUpdate, Environment } from "relay-runtime"; + +import { createMutationContainer } from "talk-framework/lib/relay"; + +import { AUTH_POPUP_ID } from "../local"; + +export interface SetAuthPopupStateInput { + open?: boolean; + focus?: boolean; + href?: string; +} + +export type SetAuthPopupStateMutation = ( + input: SetAuthPopupStateInput +) => Promise; + +export async function commit( + environment: Environment, + input: SetAuthPopupStateInput +) { + return commitLocalUpdate(environment, store => { + const record = store.get(AUTH_POPUP_ID)!; + if (input.open !== undefined) { + record.setValue(input.open, "open"); + } + if (input.focus !== undefined) { + record.setValue(input.focus, "focus"); + } + if (input.href !== undefined) { + record.setValue(input.href, "href"); + } + }); +} + +export const withSetAuthPopupStateMutation = createMutationContainer( + "setAuthPopupState", + commit +); diff --git a/src/core/client/stream/mutations/SetNetworkStatusMutation.spec.ts b/src/core/client/stream/mutations/SetNetworkStatusMutation.spec.ts index 1a6196c45..2ca97bea9 100644 --- a/src/core/client/stream/mutations/SetNetworkStatusMutation.spec.ts +++ b/src/core/client/stream/mutations/SetNetworkStatusMutation.spec.ts @@ -3,7 +3,6 @@ import { Environment, RecordSource } from "relay-runtime"; import { createRelayEnvironment } from "talk-framework/testHelpers"; import { NETWORK_ID, NETWORK_TYPE } from "../local"; - import { commit } from "./SetNetworkStatusMutation"; let environment: Environment; diff --git a/src/core/client/stream/mutations/ShowAuthPopupMutation.spec.ts b/src/core/client/stream/mutations/ShowAuthPopupMutation.spec.ts new file mode 100644 index 000000000..17d612d89 --- /dev/null +++ b/src/core/client/stream/mutations/ShowAuthPopupMutation.spec.ts @@ -0,0 +1,42 @@ +import { Environment, RecordSource } from "relay-runtime"; + +import { createRelayEnvironment } from "talk-framework/testHelpers"; + +import { AUTH_POPUP_ID, AUTH_POPUP_TYPE } from "../local"; +import { commit } from "./ShowAuthPopupMutation"; + +let environment: Environment; +const source: RecordSource = new RecordSource(); + +beforeAll(() => { + environment = createRelayEnvironment({ + source, + initLocalState: (localRecord, sourceProxy) => { + const record = sourceProxy.create(AUTH_POPUP_ID, AUTH_POPUP_TYPE); + record.setValue(false, "open"); + record.setValue(false, "focus"); + localRecord.setLinkedRecord(record, "authPopup"); + }, + }); +}); + +it("opens popup", () => { + commit(environment, { view: "SIGN_IN" }); + expect(source.get(AUTH_POPUP_ID)!.open).toEqual(true); + expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(false); + expect(source.get(AUTH_POPUP_ID)!.view).toEqual("SIGN_IN"); +}); + +it("focuses popup", () => { + commit(environment, { view: "SIGN_IN" }); + expect(source.get(AUTH_POPUP_ID)!.open).toEqual(true); + expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(true); + expect(source.get(AUTH_POPUP_ID)!.view).toEqual("SIGN_IN"); +}); + +it("only change view when opened and focused", () => { + commit(environment, { view: "FORGOT_PASSWORD" }); + expect(source.get(AUTH_POPUP_ID)!.open).toEqual(true); + expect(source.get(AUTH_POPUP_ID)!.focus).toEqual(true); + expect(source.get(AUTH_POPUP_ID)!.view).toEqual("FORGOT_PASSWORD"); +}); diff --git a/src/core/client/stream/mutations/ShowAuthPopupMutation.ts b/src/core/client/stream/mutations/ShowAuthPopupMutation.ts new file mode 100644 index 000000000..2d6936d53 --- /dev/null +++ b/src/core/client/stream/mutations/ShowAuthPopupMutation.ts @@ -0,0 +1,36 @@ +import { commitLocalUpdate, Environment } from "relay-runtime"; + +import { createMutationContainer } from "talk-framework/lib/relay"; + +import { AUTH_POPUP_ID } from "../local"; + +export interface ShowAuthPopupInput { + view: "SIGN_IN" | "SIGN_UP" | "FORGOT_PASSWORD"; +} + +export type ShowAuthPopupMutation = ( + input: ShowAuthPopupInput +) => Promise; + +export async function commit( + environment: Environment, + input: ShowAuthPopupInput +) { + return commitLocalUpdate(environment, store => { + const record = store.get(AUTH_POPUP_ID)!; + record.setValue(input.view, "view"); + if (!record.getValue("open")) { + record.setValue(true, "open"); + return; + } + if (!record.getValue("focus")) { + record.setValue(true, "focus"); + return; + } + }); +} + +export const withShowAuthPopupMutation = createMutationContainer( + "showAuthPopup", + commit +); diff --git a/src/core/client/stream/mutations/index.ts b/src/core/client/stream/mutations/index.ts index 1b71335e7..324fddbdc 100644 --- a/src/core/client/stream/mutations/index.ts +++ b/src/core/client/stream/mutations/index.ts @@ -13,3 +13,11 @@ export { SetCommentIDMutation, SetCommentIDInput, } from "./SetCommentIDMutation"; +export { + withShowAuthPopupMutation, + ShowAuthPopupMutation, +} from "./ShowAuthPopupMutation"; +export { + withSetAuthPopupStateMutation, + SetAuthPopupStateMutation, +} from "./SetAuthPopupStateMutation"; diff --git a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap index 619f3269d..16b8d5fb6 100644 --- a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap @@ -1,141 +1,200 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`loads more comments 1`] = ` -
+
- -
-