diff --git a/src/core/client/auth/index.tsx b/src/core/client/auth/index.tsx
index b30487719..d6057f5a3 100644
--- a/src/core/client/auth/index.tsx
+++ b/src/core/client/auth/index.tsx
@@ -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 = () => (
-
+
-
+
);
ReactDOM.render(, document.getElementById("app"));
diff --git a/src/core/client/auth/test/create.tsx b/src/core/client/auth/test/create.tsx
index a0b9d9da9..fc03cd354 100644
--- a/src/core/client/auth/test/create.tsx
+++ b/src/core/client/auth/test/create.tsx
@@ -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(
diff --git a/src/core/client/framework/helpers/getMe.ts b/src/core/client/framework/helpers/getMe.ts
index fb5bafcf9..d989b5be2 100644
--- a/src/core/client/framework/helpers/getMe.ts
+++ b/src/core/client/framework/helpers/getMe.ts
@@ -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)!;
}
diff --git a/src/core/client/framework/lib/bootstrap/TalkContext.tsx b/src/core/client/framework/lib/bootstrap/TalkContext.tsx
index d855a884e..ece409674 100644
--- a/src/core/client/framework/lib/bootstrap/TalkContext.tsx
+++ b/src/core/client/framework/lib/bootstrap/TalkContext.tsx
@@ -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({} as any);
diff --git a/src/core/client/framework/lib/bootstrap/createContext.tsx b/src/core/client/framework/lib/bootstrap/createContext.tsx
deleted file mode 100644
index 839cee453..000000000
--- a/src/core/client/framework/lib/bootstrap/createContext.tsx
+++ /dev/null
@@ -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;
-
- /** 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);
-
- /** 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 (
-
- now
-
- );
-};
-
-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 {
- 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;
-}
diff --git a/src/core/client/framework/lib/bootstrap/createManaged.tsx b/src/core/client/framework/lib/bootstrap/createManaged.tsx
new file mode 100644
index 000000000..f2d86e7c8
--- /dev/null
+++ b/src/core/client/framework/lib/bootstrap/createManaged.tsx
@@ -0,0 +1,252 @@
+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);
+
+interface CreateContextArguments {
+ /** Locales that the user accepts, usually `navigator.languages`. */
+ userLocales: ReadonlyArray;
+
+ /** 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" ? "in" : suffix;
+ return (
+
+ now
+
+ );
+};
+
+/**
+ * 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 (
+
+ {this.props.children}
+
+ );
+ }
+ };
+
+ 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 {
+ // 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);
+}
diff --git a/src/core/client/framework/lib/bootstrap/index.ts b/src/core/client/framework/lib/bootstrap/index.ts
index a6bd57eaa..0ef2ee79b 100644
--- a/src/core/client/framework/lib/bootstrap/index.ts
+++ b/src/core/client/framework/lib/bootstrap/index.ts
@@ -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";
diff --git a/src/core/client/framework/lib/relay/commitLocalUpdatePromisified.ts b/src/core/client/framework/lib/relay/commitLocalUpdatePromisified.ts
new file mode 100644
index 000000000..92d341cef
--- /dev/null
+++ b/src/core/client/framework/lib/relay/commitLocalUpdatePromisified.ts
@@ -0,0 +1,18 @@
+import {
+ commitLocalUpdate,
+ Environment,
+ RecordSourceProxy,
+} from "relay-runtime";
+
+export default function commitLocalUpdatePromisified(
+ environment: Environment,
+ updater: (store: RecordSourceProxy) => Promise
+) {
+ return new Promise((resolve, reject) => {
+ commitLocalUpdate(environment, store => {
+ updater(store)
+ .then(() => resolve())
+ .catch(err => reject(err));
+ });
+ });
+}
diff --git a/src/core/client/framework/lib/relay/index.ts b/src/core/client/framework/lib/relay/index.ts
index 43dca5178..31bae3d63 100644
--- a/src/core/client/framework/lib/relay/index.ts
+++ b/src/core/client/framework/lib/relay/index.ts
@@ -13,3 +13,6 @@ export {
commitMutationPromiseNormalized,
} from "./commitMutationPromise";
export { graphql } from "react-relay";
+export {
+ default as commitLocalUpdatePromisified,
+} from "./commitLocalUpdatePromisified";
diff --git a/src/core/client/framework/mutations/SetAuthTokenMutation.spec.ts b/src/core/client/framework/mutations/SetAuthTokenMutation.spec.ts
index 887e6fd84..158dd0854 100644
--- a/src/core/client/framework/mutations/SetAuthTokenMutation.spec.ts
+++ b/src/core/client/framework/mutations/SetAuthTokenMutation.spec.ts
@@ -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 = {
+ 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 = {
+ 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);
});
diff --git a/src/core/client/framework/mutations/SetAuthTokenMutation.ts b/src/core/client/framework/mutations/SetAuthTokenMutation.ts
index 90e520155..f0da6df92 100644
--- a/src/core/client/framework/mutations/SetAuthTokenMutation.ts
+++ b/src/core/client/framework/mutations/SetAuthTokenMutation.ts
@@ -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;
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();
});
}
diff --git a/src/core/client/stream/index.tsx b/src/core/client/stream/index.tsx
index 31fade961..7dde43c00 100644
--- a/src/core/client/stream/index.tsx
+++ b/src/core/client/stream/index.tsx
@@ -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 = (
+ <>
+
+
+
+ >
+);
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 = () => (
-
-
-
+
+ <>
+ {listeners}
+
+ >
+
);
ReactDOM.render(, document.getElementById("app"));
diff --git a/src/core/client/stream/listeners/OnPostMessageAuthError.tsx b/src/core/client/stream/listeners/OnPostMessageAuthError.tsx
new file mode 100644
index 000000000..3bb851965
--- /dev/null
+++ b/src/core/client/stream/listeners/OnPostMessageAuthError.tsx
@@ -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 {
+ 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;
diff --git a/src/core/client/stream/listeners/onPostMessageSetAuthToken.spec.ts b/src/core/client/stream/listeners/OnPostMessageSetAuthToken.spec.tsx
similarity index 57%
rename from src/core/client/stream/listeners/onPostMessageSetAuthToken.spec.ts
rename to src/core/client/stream/listeners/OnPostMessageSetAuthToken.spec.tsx
index c35823c8a..da2eb5816 100644
--- a/src/core/client/stream/listeners/onPostMessageSetAuthToken.spec.ts
+++ b/src/core/client/stream/listeners/OnPostMessageSetAuthToken.spec.tsx
@@ -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 = {
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();
expect(source.get(LOCAL_ID)!.authToken).toEqual(token);
});
diff --git a/src/core/client/stream/listeners/OnPostMessageSetAuthToken.ts b/src/core/client/stream/listeners/OnPostMessageSetAuthToken.ts
new file mode 100644
index 000000000..553e4fa5b
--- /dev/null
+++ b/src/core/client/stream/listeners/OnPostMessageSetAuthToken.ts
@@ -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 {
+ 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;
diff --git a/src/core/client/stream/listeners/onPymSetCommentID.spec.ts b/src/core/client/stream/listeners/OnPymSetCommentID.spec.tsx
similarity index 77%
rename from src/core/client/stream/listeners/onPymSetCommentID.spec.ts
rename to src/core/client/stream/listeners/OnPymSetCommentID.spec.tsx
index 6b5e67ea2..20132c1f7 100644
--- a/src/core/client/stream/listeners/onPymSetCommentID.spec.ts
+++ b/src/core/client/stream/listeners/OnPymSetCommentID.spec.tsx
@@ -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();
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();
expect(source.get(LOCAL_ID)!.commentID).toEqual(null);
});
diff --git a/src/core/client/stream/listeners/OnPymSetCommentID.ts b/src/core/client/stream/listeners/OnPymSetCommentID.ts
new file mode 100644
index 000000000..f9ca0c6f5
--- /dev/null
+++ b/src/core/client/stream/listeners/OnPymSetCommentID.ts
@@ -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 {
+ 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;
diff --git a/src/core/client/stream/listeners/index.ts b/src/core/client/stream/listeners/index.ts
index 7901544e0..1c315d81f 100644
--- a/src/core/client/stream/listeners/index.ts
+++ b/src/core/client/stream/listeners/index.ts
@@ -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";
diff --git a/src/core/client/stream/listeners/onPostMessageAuthError.ts b/src/core/client/stream/listeners/onPostMessageAuthError.ts
deleted file mode 100644
index 668a4ba1f..000000000
--- a/src/core/client/stream/listeners/onPostMessageAuthError.ts
+++ /dev/null
@@ -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);
- });
-}
diff --git a/src/core/client/stream/listeners/onPostMessageSetAuthToken.ts b/src/core/client/stream/listeners/onPostMessageSetAuthToken.ts
deleted file mode 100644
index 767af51f7..000000000
--- a/src/core/client/stream/listeners/onPostMessageSetAuthToken.ts
+++ /dev/null
@@ -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);
- });
-}
diff --git a/src/core/client/stream/listeners/onPymSetCommentID.ts b/src/core/client/stream/listeners/onPymSetCommentID.ts
deleted file mode 100644
index 4b5547173..000000000
--- a/src/core/client/stream/listeners/onPymSetCommentID.ts
+++ /dev/null
@@ -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");
- }
- });
- });
-}
diff --git a/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap b/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap
index b95cc19ce..c8dda11c7 100644
--- a/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap
+++ b/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap
@@ -13,7 +13,6 @@ exports[`init local state 1`] = `
\\"__id\\": \\"client:root.local\\",
\\"__typename\\": \\"Local\\",
\\"authToken\\": \\"\\",
- \\"authRevision\\": 0,
\\"network\\": {
\\"__ref\\": \\"client:root.local.network\\"
},
diff --git a/src/core/client/stream/local/initLocalState.ts b/src/core/client/stream/local/initLocalState.ts
index 2d953e6ae..5b127755d 100644
--- a/src/core/client/stream/local/initLocalState.ts
+++ b/src/core/client/stream/local/initLocalState.ts
@@ -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);
diff --git a/src/core/client/stream/local/local.graphql b/src/core/client/stream/local/local.graphql
index 9e04c476a..37615ec9d 100644
--- a/src/core/client/stream/local/local.graphql
+++ b/src/core/client/stream/local/local.graphql
@@ -26,10 +26,6 @@ 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!
}
extend type Query {
diff --git a/src/core/client/stream/queries/PermalinkViewQuery.tsx b/src/core/client/stream/queries/PermalinkViewQuery.tsx
index fc098dafe..4d1ac4611 100644
--- a/src/core/client/stream/queries/PermalinkViewQuery.tsx
+++ b/src/core/client/stream/queries/PermalinkViewQuery.tsx
@@ -45,19 +45,12 @@ export const render = ({
};
const PermalinkViewQuery: StatelessComponent = ({
- local: { commentID, assetID, authRevision },
+ local: { commentID, assetID },
}) => (
query={graphql`
- query PermalinkViewQuery(
- $commentID: ID!
- $assetID: ID!
- $authRevision: Int!
- ) {
- # authRevision is increment every time auth state has changed.
- # This is basically a cache invalidation and causes relay
- # to automatically update this query.
- me(clientAuthRevision: $authRevision) {
+ query PermalinkViewQuery($commentID: ID!, $assetID: ID!) {
+ me {
...PermalinkViewContainer_me
}
asset(id: $assetID) {
@@ -71,7 +64,6 @@ const PermalinkViewQuery: StatelessComponent = ({
variables={{
assetID: assetID!,
commentID: commentID!,
- authRevision,
}}
render={render}
/>
@@ -81,7 +73,6 @@ const enhanced = withLocalStateContainer(
graphql`
fragment PermalinkViewQueryLocal on Local {
assetID
- authRevision
commentID
}
`
diff --git a/src/core/client/stream/queries/StreamQuery.tsx b/src/core/client/stream/queries/StreamQuery.tsx
index b0f1f0c3f..ec61ec723 100644
--- a/src/core/client/stream/queries/StreamQuery.tsx
+++ b/src/core/client/stream/queries/StreamQuery.tsx
@@ -38,15 +38,12 @@ export const render = ({
};
const StreamQuery: StatelessComponent = ({
- local: { assetID, authRevision },
+ local: { assetID },
}) => (
query={graphql`
- query StreamQuery($assetID: ID!, $authRevision: Int!) {
- # authRevision is increment every time auth state has changed.
- # This is basically a cache invalidation and causes relay
- # to automatically update this query.
- me(clientAuthRevision: $authRevision) {
+ query StreamQuery($assetID: ID!) {
+ me {
...StreamContainer_me
}
asset(id: $assetID) {
@@ -56,7 +53,6 @@ const StreamQuery: StatelessComponent = ({
`}
variables={{
assetID: assetID!,
- authRevision,
}}
render={render}
/>
@@ -66,7 +62,6 @@ const enhanced = withLocalStateContainer(
graphql`
fragment StreamQueryLocal on Local {
assetID
- authRevision
}
`
)(StreamQuery);
diff --git a/src/core/client/stream/test/create.tsx b/src/core/client/stream/test/create.tsx
index 49d885c66..2eecbc5a1 100644
--- a/src/core/client/stream/test/create.tsx
+++ b/src/core/client/stream/test/create.tsx
@@ -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";
@@ -30,7 +32,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);
}
@@ -46,6 +47,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(
diff --git a/src/core/client/stream/test/postComment.spec.tsx b/src/core/client/stream/test/postComment.spec.tsx
index 658a9d036..78120589d 100644
--- a/src/core/client/stream/test/postComment.spec.tsx
+++ b/src/core/client/stream/test/postComment.spec.tsx
@@ -11,14 +11,8 @@ let testRenderer: ReactTestRenderer;
beforeEach(() => {
const resolvers = {
Query: {
- asset: createSinonStub(
- s => s.throws(),
- s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0])
- ),
- me: createSinonStub(
- s => s.throws(),
- s => s.withArgs(undefined, { clientAuthRevision: 0 }).returns(users[0])
- ),
+ asset: createSinonStub(s => s.throws(), s => s.returns(assets[0])),
+ me: createSinonStub(s => s.throws(), s => s.returns(users[0])),
},
Mutation: {
createComment: createSinonStub(
diff --git a/src/core/client/stream/test/postReply.spec.tsx b/src/core/client/stream/test/postReply.spec.tsx
index 3c64bc098..dd7f0eb6e 100644
--- a/src/core/client/stream/test/postReply.spec.tsx
+++ b/src/core/client/stream/test/postReply.spec.tsx
@@ -11,14 +11,8 @@ let testRenderer: ReactTestRenderer;
beforeEach(() => {
const resolvers = {
Query: {
- asset: createSinonStub(
- s => s.throws(),
- s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0])
- ),
- me: createSinonStub(
- s => s.throws(),
- s => s.withArgs(undefined, { clientAuthRevision: 0 }).returns(users[0])
- ),
+ asset: createSinonStub(s => s.throws(), s => s.returns(assets[0])),
+ me: createSinonStub(s => s.throws(), s => s.returns(users[0])),
},
Mutation: {
createComment: createSinonStub(
diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql
index dc689f403..3dcdb0894 100644
--- a/src/core/server/graph/tenant/schema/schema.graphql
+++ b/src/core/server/graph/tenant/schema/schema.graphql
@@ -809,12 +809,8 @@ type Query {
"""
me is the current logged in User.
-
- clientAuthRevision is an implementation detail that is only
- used on the client to invalidate the cache.
- TODO: This should move to a client side directive if this becomes possible.
"""
- me(clientAuthRevision: Int): User
+ me: User
"""
settings is the Settings for a given Tenant.