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 277ec34ec..a05f22b0c 100644 --- a/config/watcher.ts +++ b/config/watcher.ts @@ -31,6 +31,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", { @@ -59,9 +76,15 @@ const config: Config = { "runWebpackDevServer", "compileCSSTypes", "compileRelayStream", + "compileRelayAuth", ], docz: ["runDocz", "compileCSSTypes"], - compile: ["compileSchema", "compileCSSTypes", "compileRelayStream"], + compile: [ + "compileSchema", + "compileCSSTypes", + "compileRelayStream", + "compileRelayAuth", + ], }, }; diff --git a/package-lock.json b/package-lock.json index a95a25b4e..f89274a76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15001,9 +15001,9 @@ "dev": true }, "matchmediaquery": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.2.1.tgz", - "integrity": "sha1-IjxwBXk94D5HzpKxMoWnLEStos8=", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.0.tgz", + "integrity": "sha512-u0dlv+VENJ+3YepvwSPBieuvnA6DWfaYa/ctwysAR13y4XLJNyt7bEVKzNj/Nvjo+50d88Pj+xL9xaSo6JmX/w==", "dev": true, "requires": { "css-mediaquery": "^0.1.2" @@ -19584,13 +19584,13 @@ } }, "react-responsive": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-4.1.0.tgz", - "integrity": "sha512-ZuDraf0qsJlyiTwzeva+foHx83IP6SIhru9o7BvMwQ4ZHjRIL5WjdgVNNrKSRbmeWO9rEJoMpabei/5lJn8KaA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-5.0.0.tgz", + "integrity": "sha512-oEimZ0FTCC3/pjGDEBHOz06nWbBNDIbMGOdRYp6K9SBUmrqgNAX77hTiqvmRQeLyI97zz4F4kiaFRxFspDxE+w==", "dev": true, "requires": { "hyphenate-style-name": "^1.0.0", - "matchmediaquery": "^0.2.1", + "matchmediaquery": "^0.3.0", "prop-types": "^15.6.1" } }, diff --git a/package.json b/package.json index 78668ab86..9283410b5 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", @@ -175,7 +176,7 @@ "react-final-form": "^3.6.4", "react-popper": "^1.0.0", "react-relay": "github:coralproject/patched#react-relay", - "react-responsive": "^4.1.0", + "react-responsive": "^5.0.0", "react-test-renderer": "^16.4.1", "react-timeago": "^4.1.9", "react-with-state-props": "^2.0.4", diff --git a/src/core/build/createWebpackConfig.ts b/src/core/build/createWebpackConfig.ts index e14cb250a..ea6d4b196 100644 --- a/src/core/build/createWebpackConfig.ts +++ b/src/core/build/createWebpackConfig.ts @@ -80,6 +80,28 @@ export default function createWebpackConfig({ }, ]; + const localesOptions = { + pathToLocales: paths.appLocales, + + // Default locale if non could be negotiated. + defaultLocale: "en-US", + + // Fallback locale if a translation was not found. + // If not set, will use the text that is already + // in the code base. + fallbackLocale: "en-US", + + // Common fluent files are always included in the locale bundles. + commonFiles: ["framework.ftl", "common.ftl"], + + // Locales that come with the main bundle. Others are loaded on demand. + bundled: ["en-US"], + + // All available locales can be loadable on demand. + // To restrict available locales set: + // availableLocales: ["en-US"], + }; + const additionalPlugins = isProduction ? [ // Minify the code. @@ -214,29 +236,26 @@ export default function createWebpackConfig({ { loader: "locales-loader", options: { - pathToLocales: paths.appLocales, - - // Default locale if non could be negotiated. - defaultLocale: "en-US", - - // Fallback locale if a translation was not found. - // If not set, will use the text that is already - // in the code base. - fallbackLocale: "en-US", - - // Common fluent files are always included in the locale bundles. - commonFiles: ["framework.ftl", "common.ftl"], - - // Locales that come with the main bundle. Others are loaded on demand. - bundled: ["en-US"], - + ...localesOptions, // Target specifies the prefix for fluent files to be loaded. // ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales. target: "stream", - - // All available locales can be loadable on demand. - // To restrict available locales set: - // availableLocales: ["en-US"], + }, + }, + ], + }, + { + test: paths.appAuthLocalesTemplate, + use: [ + // This is the locales loader that loads available locales + // from a particular target. + { + loader: "locales-loader", + options: { + ...localesOptions, + // Target specifies the prefix for fluent files to be loaded. + // ${target}-xyz.ftl and ${†arget}.ftl are loaded into the locales. + target: "auth", }, }, ], @@ -380,6 +399,24 @@ export default function createWebpackConfig({ paths.appStreamIndex, // Remove deactivated entries. ].filter(s => s), + auth: [ + // We ship polyfills by default + paths.appPolyfill, + // Include an alternative client for WebpackDevServer. A client's job is to + // connect to WebpackDevServer by a socket and get notified about changes. + // When you save a file, the client will either apply hot updates (in case + // of CSS changes), or refresh the page (in case of JS changes). When you + // make a syntax error, this client will display a syntax error overlay. + // Note: instead of the default WebpackDevServer client, we use a custom one + // to bring better experience for Create React App users. You can replace + // the line below with these two lines if you prefer the stock client: + // require.resolve('webpack-dev-server/client') + '?/', + // require.resolve('webpack/hot/dev-server'), + (isProduction && "") || + require.resolve("react-dev-utils/webpackHotDevClient"), + paths.appAuthIndex, + // Remove deactivated entries. + ].filter(s => s), }, plugins: [ ...baseConfig.plugins!, @@ -391,6 +428,14 @@ export default function createWebpackConfig({ inject: "body", ...htmlWebpackConfig, }), + // Generates an `auth.html` file with the diff --git a/src/core/client/framework/lib/bootstrap/TalkContext.tsx b/src/core/client/framework/lib/bootstrap/TalkContext.tsx index 90ed10530..75ddc974c 100644 --- a/src/core/client/framework/lib/bootstrap/TalkContext.tsx +++ b/src/core/client/framework/lib/bootstrap/TalkContext.tsx @@ -2,6 +2,7 @@ import { LocalizationProvider } from "fluent-react/compat"; import { MessageContext } from "fluent/compat"; import { Child as PymChild } from "pym.js"; import React, { StatelessComponent } from "react"; +import { MediaQueryMatchers } from "react-responsive"; import { Formatter } from "react-timeago"; import { Environment } from "relay-runtime"; @@ -18,6 +19,9 @@ export interface TalkContext { /** formatter for timeago. */ timeagoFormatter?: Formatter; + /** media query values for testing purposes */ + mediaQueryValues?: MediaQueryMatchers; + /** * A way to listen for clicks that are e.g. outside of the * current frame for `ClickOutside` @@ -48,6 +52,7 @@ export const TalkContextProvider: StatelessComponent<{ value={{ timeagoFormatter: value.timeagoFormatter, registerClickFarAway: value.registerClickFarAway, + mediaQueryValues: value.mediaQueryValues, }} > {children} 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/Comment/Comment.css b/src/core/client/stream/components/Comment/Comment.css index c4bf8c532..5c0f67095 100644 --- a/src/core/client/stream/components/Comment/Comment.css +++ b/src/core/client/stream/components/Comment/Comment.css @@ -1,3 +1,6 @@ +.root { + width: 100%; +} .footer { margin-top: var(--spacing-unit); } diff --git a/src/core/client/stream/components/Comment/Comment.tsx b/src/core/client/stream/components/Comment/Comment.tsx index 932cd7155..b47c8ac02 100644 --- a/src/core/client/stream/components/Comment/Comment.tsx +++ b/src/core/client/stream/components/Comment/Comment.tsx @@ -20,7 +20,7 @@ export interface CommentProps { const Comment: StatelessComponent = props => { return ( -
+
{props.author && props.author.username && {props.author.username}} diff --git a/src/core/client/stream/components/Comment/TopBar.tsx b/src/core/client/stream/components/Comment/TopBar.tsx index 2dfe6419d..a82dd0a28 100644 --- a/src/core/client/stream/components/Comment/TopBar.tsx +++ b/src/core/client/stream/components/Comment/TopBar.tsx @@ -14,7 +14,7 @@ export interface TopBarProps { const TopBar: StatelessComponent = props => { const rootClassName = cn(styles.root, props.className); return ( - + {matches => ( = props => { return ( - + {matches => ( 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..2e6694137 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 @@ -2,20 +2,28 @@ 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..ec1be799f 100644 --- a/src/core/client/stream/components/PermalinkButton/PermalinkButton.tsx +++ b/src/core/client/stream/components/PermalinkButton/PermalinkButton.tsx @@ -57,10 +57,10 @@ class Permalink extends React.Component { active={visible} size="small" > - + 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 ef1ed5bda..7f1ea183c 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -7,6 +7,7 @@ import { Button, Flex } from "talk-ui/components"; 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 { @@ -20,7 +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..9d838a510 --- /dev/null +++ b/src/core/client/stream/components/UserBoxUnauthenticated.tsx @@ -0,0 +1,52 @@ +import { Localized } from "fluent-react/compat"; +import React, { StatelessComponent } from "react"; + +import { Button, Flex, Typography } from "talk-ui/components"; + +import MatchMedia from "talk-ui/components/MatchMedia"; +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..9cf950ec6 --- /dev/null +++ b/src/core/client/stream/components/__snapshots__/UserBoxUnauthenticated.spec.tsx.snap @@ -0,0 +1,50 @@ +// 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/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/index.tsx b/src/core/client/stream/index.tsx index 6d622216e..4ac024d6d 100644 --- a/src/core/client/stream/index.tsx +++ b/src/core/client/stream/index.tsx @@ -32,7 +32,9 @@ async function main() { }); const Index: StatelessComponent = () => ( - + ); 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..d3310dc45 100644 --- a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap @@ -2,140 +2,212 @@ exports[`loads more comments 1`] = `
- -
-