mirror of
https://github.com/wassname/talk.git
synced 2026-07-05 08:14:56 +08:00
Merge branch 'next' of github.com:coralproject/talk into ui-tab
* 'next' of github.com:coralproject/talk: (26 commits) Adapt snapshots Disable RTE when submitting Wrap long words Disable submit button when empty Fix test Fix types and tests Remove outdated workarounds Remove accidently commited files Move uuid generation to TalkContext Full PromisifiedStorage + Simplifications Update package-lock Return null cursor when creating comment Change commentEdge to edge Update snapshots Fix types Reply opens auth popup when not logged in Better tests Stream should work outside of iframe for debugging Add test Focus RTE when opening reply ...
This commit is contained in:
@@ -16,6 +16,7 @@ module.exports = {
|
||||
transform: {
|
||||
"^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest",
|
||||
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||
"^.+\\.ftl$": "<rootDir>/config/jest/contentTransform.js",
|
||||
"^(?!.*\\.(js|jsx|mjs|ts|tsx|css|json|ftl)$)":
|
||||
"<rootDir>/config/jest/fileTransform.js",
|
||||
},
|
||||
@@ -30,7 +31,7 @@ module.exports = {
|
||||
"^talk-framework/(.*)$": "<rootDir>/src/core/client/framework/$1",
|
||||
"^talk-common/(.*)$": "<rootDir>/src/core/common/$1",
|
||||
},
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node", "ftl"],
|
||||
snapshotSerializers: ["enzyme-to-json/serializer"],
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
|
||||
// This is a custom Jest transformer that returns the content of a file
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
return `module.exports = ${JSON.stringify(
|
||||
fs.readFileSync(filename).toString()
|
||||
)};`;
|
||||
},
|
||||
};
|
||||
Generated
+3
-7
@@ -2162,13 +2162,9 @@
|
||||
}
|
||||
},
|
||||
"@types/recompose": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/recompose/-/recompose-0.26.1.tgz",
|
||||
"integrity": "sha512-S5fkitL277yWCEHDzgb/3aJ4RySeqSC7L3Xo+7AlDk6+DAPpQAfF0iwgfjAqbP49JAjVCY+asPQxFPiw1+4CYg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
"version": "github:coralproject/patched#3ca0deec868322739fc0d750398fd97735d27aac",
|
||||
"from": "github:coralproject/patched#types/recompose",
|
||||
"dev": true
|
||||
},
|
||||
"@types/relateurl": {
|
||||
"version": "0.2.28",
|
||||
|
||||
+2
-1
@@ -133,7 +133,7 @@
|
||||
"@types/react-relay": "^1.3.9",
|
||||
"@types/react-responsive": "^3.0.1",
|
||||
"@types/react-test-renderer": "^16.0.1",
|
||||
"@types/recompose": "^0.26.1",
|
||||
"@types/recompose": "github:coralproject/patched#types/recompose",
|
||||
"@types/relay-runtime": "^1.3.6",
|
||||
"@types/sane": "^2.0.0",
|
||||
"@types/sinon": "^5.0.1",
|
||||
@@ -151,6 +151,7 @@
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
"babel-plugin-relay": "^1.7.0-rc.1",
|
||||
"babel-preset-react-optimize": "^1.0.1",
|
||||
"bowser": "^1.9.4",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"chalk": "^2.4.1",
|
||||
"chokidar": "^2.0.4",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { IResolvers } from "graphql-tools";
|
||||
import React from "react";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
|
||||
|
||||
import AppContainer from "talk-auth/containers/AppContainer";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import { PostMessageService } from "talk-framework/lib/postMessage";
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { createPromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { createUUIDGenerator } from "talk-framework/testHelpers";
|
||||
|
||||
import createEnvironment from "./createEnvironment";
|
||||
import createFluentBundle from "./createFluentBundle";
|
||||
|
||||
interface CreateParams {
|
||||
logNetwork?: boolean;
|
||||
resolvers?: IResolvers<any, any>;
|
||||
initLocalState?: (
|
||||
local: RecordProxy,
|
||||
source: RecordSourceProxy,
|
||||
environment: Environment
|
||||
) => void;
|
||||
}
|
||||
|
||||
export default function create(params: CreateParams) {
|
||||
const environment = createEnvironment({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: params.logNetwork,
|
||||
resolvers: params.resolvers,
|
||||
initLocalState: (localRecord, source, env) => {
|
||||
localRecord.setValue(0, "authRevision");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, env);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const context: TalkContext = {
|
||||
relayEnvironment: environment,
|
||||
localeBundles: [createFluentBundle()],
|
||||
localStorage: createPromisifiedStorage(),
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
rest: new RestClient("http://localhost/api"),
|
||||
postMessage: new PostMessageService(),
|
||||
browserInfo: { ios: false },
|
||||
uuidGenerator: createUUIDGenerator(),
|
||||
};
|
||||
|
||||
const testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
return { context, testRenderer };
|
||||
}
|
||||
@@ -1,38 +1,16 @@
|
||||
// Enable after this is solved: https://github.com/projectfluent/fluent.js/issues/280
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
|
||||
import React from "react";
|
||||
import TestRenderer, { ReactTestRenderer } from "react-test-renderer";
|
||||
import { RecordProxy } from "relay-runtime";
|
||||
|
||||
import AppContainer from "talk-auth/containers/AppContainer";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import { PostMessageService } from "talk-framework/lib/postMessage";
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { createInMemoryStorage } from "talk-framework/lib/storage";
|
||||
|
||||
import createEnvironment from "./createEnvironment";
|
||||
import createFluentBundle from "./createFluentBundle";
|
||||
import create from "./create";
|
||||
|
||||
function createTestRenderer(initialView: string): ReactTestRenderer {
|
||||
const environment = createEnvironment({
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
const { testRenderer } = create({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(initialView, "view");
|
||||
},
|
||||
});
|
||||
|
||||
const context: TalkContext = {
|
||||
relayEnvironment: environment,
|
||||
localeBundles: [createFluentBundle()],
|
||||
localStorage: createInMemoryStorage(),
|
||||
sessionStorage: createInMemoryStorage(),
|
||||
rest: new RestClient("http://localhost/api"),
|
||||
postMessage: new PostMessageService(),
|
||||
};
|
||||
return TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
return testRenderer;
|
||||
}
|
||||
|
||||
it("renders sign in form", async () => {
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import React from "react";
|
||||
import TestRenderer, {
|
||||
ReactTestInstance,
|
||||
ReactTestRenderer,
|
||||
} from "react-test-renderer";
|
||||
import { RecordProxy } from "relay-runtime";
|
||||
import { ReactTestInstance, ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import AppContainer from "talk-auth/containers/AppContainer";
|
||||
import { animationFrame, timeout } from "talk-common/utils";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import { PostMessageService } from "talk-framework/lib/postMessage";
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { createInMemoryStorage } from "talk-framework/lib/storage";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
|
||||
import createEnvironment from "./createEnvironment";
|
||||
import createFluentBundle from "./createFluentBundle";
|
||||
import create from "./create";
|
||||
|
||||
const inputPredicate = (name: string) => (n: ReactTestInstance) => {
|
||||
return n.props.name === name && n.props.onChange;
|
||||
@@ -24,26 +14,13 @@ let context: TalkContext;
|
||||
let testRenderer: ReactTestRenderer;
|
||||
let form: ReactTestInstance;
|
||||
beforeEach(() => {
|
||||
const environment = createEnvironment({
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
({ testRenderer, context } = create({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue("SIGN_IN", "view");
|
||||
},
|
||||
});
|
||||
|
||||
context = {
|
||||
relayEnvironment: environment,
|
||||
localeBundles: [createFluentBundle()],
|
||||
localStorage: createInMemoryStorage(),
|
||||
sessionStorage: createInMemoryStorage(),
|
||||
rest: new RestClient("http://localhost/api"),
|
||||
postMessage: new PostMessageService(),
|
||||
};
|
||||
|
||||
testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
}));
|
||||
form = testRenderer.root.findByType("form");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
import React from "react";
|
||||
import TestRenderer, {
|
||||
ReactTestInstance,
|
||||
ReactTestRenderer,
|
||||
} from "react-test-renderer";
|
||||
import { RecordProxy } from "relay-runtime";
|
||||
import { ReactTestInstance, ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import AppContainer from "talk-auth/containers/AppContainer";
|
||||
import { animationFrame, timeout } from "talk-common/utils";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import { PostMessageService } from "talk-framework/lib/postMessage";
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { createInMemoryStorage } from "talk-framework/lib/storage";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
|
||||
import createEnvironment from "./createEnvironment";
|
||||
import createFluentBundle from "./createFluentBundle";
|
||||
import create from "./create";
|
||||
|
||||
const inputPredicate = (name: string) => (n: ReactTestInstance) => {
|
||||
return n.props.name === name && n.props.onChange;
|
||||
@@ -24,27 +14,13 @@ let context: TalkContext;
|
||||
let testRenderer: ReactTestRenderer;
|
||||
let form: ReactTestInstance;
|
||||
beforeEach(() => {
|
||||
const environment = createEnvironment({
|
||||
initLocalState: (localRecord: RecordProxy) => {
|
||||
({ testRenderer, context } = create({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue("SIGN_UP", "view");
|
||||
},
|
||||
});
|
||||
|
||||
context = {
|
||||
relayEnvironment: environment,
|
||||
localeBundles: [createFluentBundle()],
|
||||
localStorage: createInMemoryStorage(),
|
||||
sessionStorage: createInMemoryStorage(),
|
||||
rest: new RestClient("http://localhost/api"),
|
||||
postMessage: new PostMessageService(),
|
||||
};
|
||||
|
||||
testRenderer = TestRenderer.create(
|
||||
<TalkContextProvider value={context}>
|
||||
<AppContainer />
|
||||
</TalkContextProvider>
|
||||
);
|
||||
|
||||
}));
|
||||
form = testRenderer.root.findByType("form");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,51 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`withPymStorage should handle handle errors 1`] = `"[{\\"key\\":\\"pymStorage.localStorage.error\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\",\\\\\\"error\\\\\\":\\\\\\"error\\\\\\"}\\"}]"`;
|
||||
exports[`withPymStorage should clear storage 1`] = `"{}"`;
|
||||
|
||||
exports[`withPymStorage should clear storage 2`] = `
|
||||
Array [
|
||||
Object {
|
||||
"key": "pymStorage.localStorage.response",
|
||||
"value": "{\\"id\\":\\"0\\"}",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`withPymStorage should get key of storage 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"key": "pymStorage.localStorage.response",
|
||||
"value": "{\\"id\\":\\"0\\",\\"result\\":\\"b\\"}",
|
||||
},
|
||||
Object {
|
||||
"key": "pymStorage.localStorage.response",
|
||||
"value": "{\\"id\\":\\"0\\",\\"result\\":null}",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`withPymStorage should get length of storage 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"key": "pymStorage.localStorage.response",
|
||||
"value": "{\\"id\\":\\"0\\",\\"result\\":3}",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`withPymStorage should handle handle errors 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"key": "pymStorage.localStorage.error",
|
||||
"value": "{\\"id\\":\\"0\\",\\"error\\":\\"error\\"}",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`withPymStorage should handle unknown method 1`] = `"[{\\"key\\":\\"pymStorage.localStorage.error\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\",\\\\\\"error\\\\\\":\\\\\\"Unknown method unknown\\\\\\"}\\"}]"`;
|
||||
|
||||
exports[`withPymStorage should set, get and remove item 1`] = `
|
||||
Object {
|
||||
"talkPymStorage:key": "test",
|
||||
}
|
||||
`;
|
||||
exports[`withPymStorage should set, get and remove item 1`] = `"{\\"talk:key\\":\\"test\\"}"`;
|
||||
|
||||
exports[`withPymStorage should set, get and remove item 2`] = `Object {}`;
|
||||
exports[`withPymStorage should set, get and remove item 2`] = `"{}"`;
|
||||
|
||||
exports[`withPymStorage should set, get and remove item 3`] = `"[{\\"key\\":\\"pymStorage.localStorage.response\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"0\\\\\\"}\\"},{\\"key\\":\\"pymStorage.localStorage.response\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"1\\\\\\",\\\\\\"result\\\\\\":\\\\\\"test\\\\\\"}\\"},{\\"key\\":\\"pymStorage.localStorage.response\\",\\"value\\":\\"{\\\\\\"id\\\\\\":\\\\\\"2\\\\\\"}\\"}]"`;
|
||||
|
||||
@@ -1,23 +1,8 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { createInMemoryStorage } from "../testUtils";
|
||||
import withPymStorage from "./withPymStorage";
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
|
||||
class FakeStorage {
|
||||
public store: Record<string, string> = {};
|
||||
|
||||
public setItem(key: string, value: string) {
|
||||
this.store[key] = value;
|
||||
}
|
||||
public removeItem(key: string) {
|
||||
delete this.store[key];
|
||||
}
|
||||
public getItem(key: string) {
|
||||
return this.store[key];
|
||||
}
|
||||
}
|
||||
|
||||
class PymStub {
|
||||
public listeners: Record<string, ((msg: string) => void)> = {};
|
||||
public messages: Array<{ key: string; value: string }> = [];
|
||||
@@ -38,10 +23,8 @@ class PymStub {
|
||||
describe("withPymStorage", () => {
|
||||
it("should set, get and remove item", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
const storage = new FakeStorage();
|
||||
withPymStorage(storage as any, "localStorage", "talkPymStorage:")(
|
||||
pym as any
|
||||
);
|
||||
const storage = createInMemoryStorage();
|
||||
withPymStorage(storage, "localStorage", "talk:")(pym as any);
|
||||
pym.listeners["pymStorage.localStorage.request"](
|
||||
JSON.stringify({
|
||||
id: "0",
|
||||
@@ -49,7 +32,7 @@ describe("withPymStorage", () => {
|
||||
parameters: { key: "key", value: "test" },
|
||||
})
|
||||
);
|
||||
expect(storage.store).toMatchSnapshot();
|
||||
expect(storage.toString()).toMatchSnapshot();
|
||||
pym.listeners["pymStorage.localStorage.request"](
|
||||
JSON.stringify({
|
||||
id: "1",
|
||||
@@ -64,15 +47,72 @@ describe("withPymStorage", () => {
|
||||
parameters: { key: "key" },
|
||||
})
|
||||
);
|
||||
expect(storage.store).toMatchSnapshot();
|
||||
expect(storage.toString()).toMatchSnapshot();
|
||||
expect(JSON.stringify(pym.messages)).toMatchSnapshot();
|
||||
});
|
||||
it("should get key of storage", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
const storage = createInMemoryStorage({
|
||||
a: "1",
|
||||
b: "2",
|
||||
c: "3",
|
||||
});
|
||||
withPymStorage(storage, "localStorage", "")(pym as any);
|
||||
pym.listeners["pymStorage.localStorage.request"](
|
||||
JSON.stringify({
|
||||
id: "0",
|
||||
method: "key",
|
||||
parameters: { n: 1 },
|
||||
})
|
||||
);
|
||||
pym.listeners["pymStorage.localStorage.request"](
|
||||
JSON.stringify({
|
||||
id: "0",
|
||||
method: "key",
|
||||
parameters: { n: 3 },
|
||||
})
|
||||
);
|
||||
expect(pym.messages).toMatchSnapshot();
|
||||
});
|
||||
it("should get length of storage", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
const storage = createInMemoryStorage({
|
||||
a: "1",
|
||||
b: "2",
|
||||
c: "3",
|
||||
});
|
||||
withPymStorage(storage, "localStorage", "")(pym as any);
|
||||
pym.listeners["pymStorage.localStorage.request"](
|
||||
JSON.stringify({
|
||||
id: "0",
|
||||
method: "length",
|
||||
parameters: {},
|
||||
})
|
||||
);
|
||||
expect(pym.messages).toMatchSnapshot();
|
||||
});
|
||||
it("should clear storage", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
const storage = createInMemoryStorage({
|
||||
a: "1",
|
||||
b: "2",
|
||||
c: "3",
|
||||
});
|
||||
withPymStorage(storage, "localStorage", "")(pym as any);
|
||||
pym.listeners["pymStorage.localStorage.request"](
|
||||
JSON.stringify({
|
||||
id: "0",
|
||||
method: "clear",
|
||||
parameters: {},
|
||||
})
|
||||
);
|
||||
expect(storage.toString()).toMatchSnapshot();
|
||||
expect(pym.messages).toMatchSnapshot();
|
||||
});
|
||||
it("should handle unknown method", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
const storage = new FakeStorage();
|
||||
withPymStorage(storage as any, "localStorage", "talkPymStorage:")(
|
||||
pym as any
|
||||
);
|
||||
const storage = createInMemoryStorage();
|
||||
withPymStorage(storage, "localStorage", "talk:")(pym as any);
|
||||
pym.listeners["pymStorage.localStorage.request"](
|
||||
JSON.stringify({
|
||||
id: "0",
|
||||
@@ -84,14 +124,12 @@ describe("withPymStorage", () => {
|
||||
});
|
||||
it("should handle handle errors", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
const storage = new FakeStorage();
|
||||
const storage = createInMemoryStorage();
|
||||
sinon
|
||||
.mock(storage)
|
||||
.expects("getItem")
|
||||
.throws("error");
|
||||
withPymStorage(storage as any, "localStorage", "talkPymStorage:")(
|
||||
pym as any
|
||||
);
|
||||
withPymStorage(storage, "localStorage", "talk:")(pym as any);
|
||||
pym.listeners["pymStorage.localStorage.request"](
|
||||
JSON.stringify({
|
||||
id: "0",
|
||||
@@ -99,6 +137,6 @@ describe("withPymStorage", () => {
|
||||
parameters: {},
|
||||
})
|
||||
);
|
||||
expect(JSON.stringify(pym.messages)).toMatchSnapshot();
|
||||
expect(pym.messages).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { prefixStorage } from "../utils";
|
||||
import { Decorator } from "./types";
|
||||
|
||||
const withPymStorage = (
|
||||
storage: Storage,
|
||||
type: "localStorage" | "sessionStorage",
|
||||
prefix = "talkPymStorage:"
|
||||
prefix = "talk:"
|
||||
): Decorator => pym => {
|
||||
pym.onMessage(`pymStorage.${type}.request`, (msg: any) => {
|
||||
const { id, method, parameters } = JSON.parse(msg);
|
||||
const { key, value } = parameters;
|
||||
const prefixedKey = `${prefix}${key}`;
|
||||
const { n, key, value } = parameters;
|
||||
const prefixedStorage = prefixStorage(storage, prefix);
|
||||
|
||||
// Variable for the method return value.
|
||||
let result;
|
||||
@@ -25,13 +26,22 @@ const withPymStorage = (
|
||||
try {
|
||||
switch (method) {
|
||||
case "setItem":
|
||||
result = storage.setItem(prefixedKey, value);
|
||||
result = prefixedStorage.setItem(key, value);
|
||||
break;
|
||||
case "getItem":
|
||||
result = storage.getItem(prefixedKey);
|
||||
result = prefixedStorage.getItem(key);
|
||||
break;
|
||||
case "removeItem":
|
||||
result = storage.removeItem(prefixedKey);
|
||||
result = prefixedStorage.removeItem(key);
|
||||
break;
|
||||
case "key":
|
||||
result = prefixedStorage.key(n);
|
||||
break;
|
||||
case "length":
|
||||
result = prefixedStorage.length;
|
||||
break;
|
||||
case "clear":
|
||||
result = prefixedStorage.clear();
|
||||
break;
|
||||
default:
|
||||
sendError(`Unknown method ${method}`);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import createInMemoryStorage from "./InMemoryStorage";
|
||||
|
||||
it("should set and unset values", () => {
|
||||
const storage = createInMemoryStorage();
|
||||
storage.setItem("test", "value");
|
||||
expect(storage.getItem("test")).toBe("value");
|
||||
storage.removeItem("test");
|
||||
expect(storage.getItem("test")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return length", () => {
|
||||
const storage = createInMemoryStorage();
|
||||
storage.setItem("a", "value");
|
||||
storage.setItem("b", "value");
|
||||
storage.setItem("c", "value");
|
||||
expect(storage.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should nth key", () => {
|
||||
const storage = createInMemoryStorage();
|
||||
storage.setItem("a", "0");
|
||||
storage.setItem("b", "1");
|
||||
storage.setItem("c", "2");
|
||||
expect(storage.key(2)).toBe("c");
|
||||
});
|
||||
|
||||
it("accepts predefined data", () => {
|
||||
const storage = createInMemoryStorage({
|
||||
a: "0",
|
||||
b: "1",
|
||||
c: "2",
|
||||
});
|
||||
expect(storage.toString()).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* InMemoryStorage is a dumb implementation of the Storage interface that will
|
||||
* not persist the data at all. It implements the Storage interface found:
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Storage
|
||||
*/
|
||||
class InMemoryStorage implements Storage {
|
||||
private data: Record<string, string>;
|
||||
|
||||
constructor(data: Record<string, string> = {}) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Object.keys(this.data).length;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
public key(n: number) {
|
||||
if (this.length <= n) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.keys(this.data)[n];
|
||||
}
|
||||
|
||||
public getItem(key: string) {
|
||||
return this.data[key] || null;
|
||||
}
|
||||
|
||||
public setItem(key: string, value: string) {
|
||||
this.data[key] = value;
|
||||
}
|
||||
|
||||
public removeItem(key: string) {
|
||||
delete this.data[key];
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return JSON.stringify(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
export default function createInMemoryStorage(data?: Record<string, string>) {
|
||||
return new InMemoryStorage(data);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`accepts predefined data 1`] = `"{\\"a\\":\\"0\\",\\"b\\":\\"1\\",\\"c\\":\\"2\\"}"`;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as createInMemoryStorage } from "./InMemoryStorage";
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should call clear 1`] = `"{\\"a\\":\\"0\\",\\"b\\":\\"1\\",\\"d\\":\\"3\\"}"`;
|
||||
@@ -1,2 +1,4 @@
|
||||
export { default as buildURL } from "./buildURL";
|
||||
export { default as ensureEndSlash } from "./ensureEndSlash";
|
||||
export { default as startsWith } from "./startsWith";
|
||||
export { default as prefixStorage } from "./prefixStorage";
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import sinon from "sinon";
|
||||
import { createInMemoryStorage } from "../testUtils";
|
||||
import prefixStorage from "./prefixStorage";
|
||||
|
||||
it("should get nth key", () => {
|
||||
const storage = createInMemoryStorage({
|
||||
a: "0",
|
||||
b: "1",
|
||||
"talk:c": "2",
|
||||
d: "3",
|
||||
"talk:e": "4",
|
||||
});
|
||||
|
||||
const prefixed = prefixStorage(storage, "talk:");
|
||||
expect(prefixed.key(0)).toBe("talk:c");
|
||||
expect(prefixed.key(1)).toBe("talk:e");
|
||||
expect(prefixed.key(2)).toBeNull();
|
||||
});
|
||||
|
||||
it("should call clear", () => {
|
||||
const storage = createInMemoryStorage({
|
||||
a: "0",
|
||||
b: "1",
|
||||
"talk:c": "2",
|
||||
d: "3",
|
||||
"talk:e": "4",
|
||||
});
|
||||
|
||||
const prefixed = prefixStorage(storage, "talk:");
|
||||
prefixed.clear();
|
||||
expect(storage.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call length", () => {
|
||||
const storage = createInMemoryStorage({
|
||||
a: "0",
|
||||
b: "1",
|
||||
"talk:c": "2",
|
||||
d: "3",
|
||||
"talk:e": "4",
|
||||
});
|
||||
|
||||
const prefixed = prefixStorage(storage, "talk:");
|
||||
expect(prefixed.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should prefix setItem", () => {
|
||||
const storage = {
|
||||
setItem: sinon.mock().withArgs("talk:key", "value"),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk:");
|
||||
prefixed.setItem("key", "value");
|
||||
storage.setItem.verify();
|
||||
});
|
||||
|
||||
it("should prefix removeItem", () => {
|
||||
const storage = {
|
||||
removeItem: sinon.mock().withArgs("talk:key"),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk:");
|
||||
prefixed.removeItem("key");
|
||||
storage.removeItem.verify();
|
||||
});
|
||||
|
||||
it("should prefix getItem", () => {
|
||||
const ret = "value";
|
||||
const storage = {
|
||||
getItem: sinon
|
||||
.mock()
|
||||
.withArgs("talk:key")
|
||||
.returns(ret),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk:");
|
||||
expect(prefixed.getItem("key")).toBe(ret);
|
||||
(storage.getItem as any).verify();
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import startsWith from "./startsWith";
|
||||
|
||||
/**
|
||||
* PrefixedStorage decorates a Storage and prefixes keys in
|
||||
* getItem, setItem and removeItem with given prefix.
|
||||
*/
|
||||
class PrefixedStorage implements Storage {
|
||||
private storage: Storage;
|
||||
private prefix: string;
|
||||
|
||||
constructor(storage: Storage, prefix: string) {
|
||||
this.storage = storage;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
get length() {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
if (startsWith(this.storage.key(i)!, this.prefix)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
const toBeDeleted = [];
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
const key = this.storage.key(i)!;
|
||||
if (startsWith(key, this.prefix)) {
|
||||
toBeDeleted.push(key);
|
||||
}
|
||||
}
|
||||
toBeDeleted.forEach(key => this.storage.removeItem(key));
|
||||
}
|
||||
|
||||
public key(n: number) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
const key = this.storage.key(i)!;
|
||||
if (startsWith(key, this.prefix)) {
|
||||
if (count === n) {
|
||||
return key;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getItem(key: string) {
|
||||
return this.storage.getItem(`${this.prefix}${key}`);
|
||||
}
|
||||
|
||||
public setItem(key: string, value: string) {
|
||||
return this.storage.setItem(`${this.prefix}${key}`, value);
|
||||
}
|
||||
|
||||
public removeItem(key: string) {
|
||||
return this.storage.removeItem(`${this.prefix}${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default function prefixStorage(storage: Storage, prefix: string) {
|
||||
return new PrefixedStorage(storage, prefix);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import startsWith from "./startsWith";
|
||||
|
||||
it("should work correctly", () => {
|
||||
const str1 = "Saturday night plans";
|
||||
expect(startsWith(str1, "Sat")).toBe(true);
|
||||
expect(startsWith(str1, "Sat", 3)).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
/** A substitute for string.startsWith */
|
||||
export default function startsWith(str: string, search: string, pos?: number) {
|
||||
return str.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
|
||||
}
|
||||
@@ -3,7 +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).find(s => s.startsWith("me("))!;
|
||||
const meKey = Object.keys(root)
|
||||
.reverse()
|
||||
.find(s => s.startsWith("me("))!;
|
||||
if (!root[meKey]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { MediaQueryMatchers } from "react-responsive";
|
||||
import { Formatter } from "react-timeago";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { BrowserInfo } from "talk-framework/lib/browserInfo";
|
||||
import { PostMessageService } from "talk-framework/lib/postMessage";
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { PymStorage } from "talk-framework/lib/storage";
|
||||
import { PromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { UIContext } from "talk-ui/components";
|
||||
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
|
||||
|
||||
@@ -23,16 +24,10 @@ export interface TalkContext {
|
||||
timeagoFormatter?: Formatter;
|
||||
|
||||
/** Local Storage */
|
||||
localStorage: Storage;
|
||||
localStorage: PromisifiedStorage;
|
||||
|
||||
/** Session storage */
|
||||
sessionStorage: Storage;
|
||||
|
||||
/** Local Storage over pym */
|
||||
pymLocalStorage?: PymStorage;
|
||||
|
||||
/** Session storage over pym */
|
||||
pymSessionStorage?: PymStorage;
|
||||
sessionStorage: PromisifiedStorage;
|
||||
|
||||
/** media query values for testing purposes */
|
||||
mediaQueryValues?: MediaQueryMatchers;
|
||||
@@ -51,6 +46,12 @@ export interface TalkContext {
|
||||
|
||||
/** A pym child that interacts with the pym parent. */
|
||||
pym?: PymChild;
|
||||
|
||||
/** Browser detection. */
|
||||
browserInfo: BrowserInfo;
|
||||
|
||||
/** Generates uuids. */
|
||||
uuidGenerator: () => string;
|
||||
}
|
||||
|
||||
const { Provider, Consumer } = React.createContext<TalkContext>({} as any);
|
||||
|
||||
@@ -5,9 +5,13 @@ 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";
|
||||
@@ -56,6 +60,14 @@ export const timeagoFormatter: Formatter = (value, unit, suffix) => {
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
@@ -68,6 +80,7 @@ export default async function createContext({
|
||||
pym,
|
||||
eventEmitter = new EventEmitter2({ wildcard: true }),
|
||||
}: CreateContextArguments): Promise<TalkContext> {
|
||||
const inIframe = areWeInIframe();
|
||||
// Initialize Relay.
|
||||
const source = new RecordSource();
|
||||
const tokenGetter: TokenGetter = () => {
|
||||
@@ -117,10 +130,14 @@ export default async function createContext({
|
||||
registerClickFarAway,
|
||||
rest: new RestClient("/api", tokenGetter),
|
||||
postMessage: new PostMessageService(),
|
||||
localStorage: createLocalStorage(),
|
||||
sessionStorage: createSessionStorage(),
|
||||
pymLocalStorage: pym && createPymStorage(pym, "localStorage"),
|
||||
pymSessionStorage: pym && createPymStorage(pym, "sessionStorage"),
|
||||
localStorage:
|
||||
(pym && inIframe && createPymStorage(pym, "localStorage")) ||
|
||||
createPromisifiedStorage(createLocalStorage()),
|
||||
sessionStorage:
|
||||
(pym && inIframe && createPymStorage(pym, "sessionStorage")) ||
|
||||
createPromisifiedStorage(createSessionStorage()),
|
||||
browserInfo: getBrowserInfo(),
|
||||
uuidGenerator: uuid,
|
||||
};
|
||||
|
||||
// Run custom initializations.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import bowser from "bowser";
|
||||
|
||||
export interface BrowserInfo {
|
||||
ios: boolean;
|
||||
}
|
||||
|
||||
export function getBrowserInfo(): BrowserInfo {
|
||||
return {
|
||||
ios: bowser.ios,
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@ it("should set and unset values", () => {
|
||||
storage.setItem("test", "value");
|
||||
expect(storage.getItem("test")).toBe("value");
|
||||
storage.removeItem("test");
|
||||
expect(storage.getItem("test")).toBeUndefined();
|
||||
expect(storage.getItem("test")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return length", () => {
|
||||
@@ -16,10 +16,19 @@ it("should return length", () => {
|
||||
expect(storage.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should nth value", () => {
|
||||
it("should nth key", () => {
|
||||
const storage = createInMemoryStorage();
|
||||
storage.setItem("a", "a");
|
||||
storage.setItem("b", "b");
|
||||
storage.setItem("c", "c");
|
||||
storage.setItem("a", "0");
|
||||
storage.setItem("b", "1");
|
||||
storage.setItem("c", "2");
|
||||
expect(storage.key(2)).toBe("c");
|
||||
});
|
||||
|
||||
it("accepts predefined data", () => {
|
||||
const storage = createInMemoryStorage({
|
||||
a: "0",
|
||||
b: "1",
|
||||
c: "2",
|
||||
});
|
||||
expect(storage.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Storage
|
||||
*/
|
||||
class InMemoryStorage implements Storage {
|
||||
private storage: Record<string, string>;
|
||||
private data: Record<string, string>;
|
||||
|
||||
constructor() {
|
||||
this.storage = {};
|
||||
constructor(data: Record<string, string> = {}) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Object.keys(this.storage).length;
|
||||
return Object.keys(this.data).length;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.storage = {};
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
public key(n: number) {
|
||||
@@ -24,26 +24,26 @@ class InMemoryStorage implements Storage {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.storage[Object.keys(this.storage)[n]];
|
||||
return Object.keys(this.data)[n];
|
||||
}
|
||||
|
||||
public getItem(key: string) {
|
||||
return this.storage[key];
|
||||
return this.data[key] || null;
|
||||
}
|
||||
|
||||
public setItem(key: string, value: string) {
|
||||
this.storage[key] = value;
|
||||
this.data[key] = value;
|
||||
}
|
||||
|
||||
public removeItem(key: string) {
|
||||
delete this.storage[key];
|
||||
delete this.data[key];
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return JSON.stringify(this.storage);
|
||||
return JSON.stringify(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
export default function createInMemoryStorage() {
|
||||
return new InMemoryStorage();
|
||||
export default function createInMemoryStorage(data?: Record<string, string>) {
|
||||
return new InMemoryStorage(data);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import prefixStorage from "./prefixStorage";
|
||||
|
||||
export default function createLocalStorage(): Storage {
|
||||
return prefixStorage(window.localStorage, "talk");
|
||||
export default function createLocalStorage(prefix = "talk:"): Storage {
|
||||
return prefixStorage(window.localStorage, prefix);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import createInMemoryStorage from "./InMemoryStorage";
|
||||
import createPromisifiedStorage from "./PromisifiedStorage";
|
||||
|
||||
it("should set and unset values", async () => {
|
||||
const storage = createPromisifiedStorage(createInMemoryStorage());
|
||||
await expect(storage.setItem("test", "value")).resolves.toBeUndefined();
|
||||
await expect(storage.getItem("test")).resolves.toBe("value");
|
||||
storage.removeItem("test");
|
||||
await expect(storage.getItem("test")).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("should return length", async () => {
|
||||
const storage = createPromisifiedStorage(createInMemoryStorage());
|
||||
storage.setItem("a", "value");
|
||||
storage.setItem("b", "value");
|
||||
storage.setItem("c", "value");
|
||||
await expect(storage.length).resolves.toBe(3);
|
||||
});
|
||||
|
||||
it("should nth value", async () => {
|
||||
const storage = createPromisifiedStorage(createInMemoryStorage());
|
||||
storage.setItem("a", "a");
|
||||
storage.setItem("b", "b");
|
||||
storage.setItem("c", "c");
|
||||
await expect(storage.key(2)).resolves.toBe("c");
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import createInMemoryStorage from "./InMemoryStorage";
|
||||
|
||||
export interface PromisifiedStorage {
|
||||
length: Promise<number>;
|
||||
|
||||
clear(): Promise<void>;
|
||||
|
||||
key(n: number): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* value = storage[key]
|
||||
*/
|
||||
getItem(key: string): Promise<string | null>;
|
||||
/**
|
||||
* delete storage[key]
|
||||
*/
|
||||
removeItem(key: string): Promise<void>;
|
||||
/**
|
||||
* storage[key] = value
|
||||
*/
|
||||
setItem(key: string, value: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* BackedPromisifedStorage.
|
||||
*/
|
||||
class BackedPromisifedStorage implements PromisifiedStorage {
|
||||
private storage: Storage;
|
||||
|
||||
constructor(storage: Storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Promise.resolve(this.storage.length);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
return Promise.resolve(this.storage.clear());
|
||||
}
|
||||
|
||||
public key(n: number) {
|
||||
return Promise.resolve(this.storage.key(n));
|
||||
}
|
||||
|
||||
public getItem(key: string) {
|
||||
return Promise.resolve(this.storage.getItem(key));
|
||||
}
|
||||
|
||||
public setItem(key: string, value: string) {
|
||||
return Promise.resolve(this.storage.setItem(key, value));
|
||||
}
|
||||
|
||||
public removeItem(key: string) {
|
||||
return Promise.resolve(this.storage.removeItem(key));
|
||||
}
|
||||
}
|
||||
|
||||
export default function createPromisifiedStorage(
|
||||
storage: Storage = createInMemoryStorage()
|
||||
) {
|
||||
return new BackedPromisifedStorage(storage);
|
||||
}
|
||||
@@ -59,6 +59,49 @@ describe("PymStorage", () => {
|
||||
expect(promise).resolves.toBe("value");
|
||||
});
|
||||
|
||||
it("should get length", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
const storage = createPymStorage(pym as any, "localStorage");
|
||||
const promise = storage.length;
|
||||
const { key, value } = pym.messages.pop()!;
|
||||
expect(key).toBe(`pymStorage.localStorage.request`);
|
||||
const { id, method, parameters } = JSON.parse(value);
|
||||
expect(method).toBe("length");
|
||||
expect(parameters).toEqual({});
|
||||
pym.listeners["pymStorage.localStorage.response"](
|
||||
JSON.stringify({ id, result: 3 })
|
||||
);
|
||||
expect(promise).resolves.toBe(3);
|
||||
});
|
||||
|
||||
it("should get key", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
const storage = createPymStorage(pym as any, "localStorage");
|
||||
const promise = storage.key(2);
|
||||
const { key, value } = pym.messages.pop()!;
|
||||
expect(key).toBe(`pymStorage.localStorage.request`);
|
||||
const { id, method, parameters } = JSON.parse(value);
|
||||
expect(method).toBe("key");
|
||||
expect(parameters).toEqual({ n: 2 });
|
||||
pym.listeners["pymStorage.localStorage.response"](
|
||||
JSON.stringify({ id, result: "myKey" })
|
||||
);
|
||||
expect(promise).resolves.toBe("myKey");
|
||||
});
|
||||
|
||||
it("should clear", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
const storage = createPymStorage(pym as any, "localStorage");
|
||||
const promise = storage.clear();
|
||||
const { key, value } = pym.messages.pop()!;
|
||||
expect(key).toBe(`pymStorage.localStorage.request`);
|
||||
const { id, method, parameters } = JSON.parse(value);
|
||||
expect(method).toBe("clear");
|
||||
expect(parameters).toEqual({});
|
||||
pym.listeners["pymStorage.localStorage.response"](JSON.stringify({ id }));
|
||||
expect(promise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
describe("on error", () => {
|
||||
it("should reject set item", () => {
|
||||
const pym = new PymStub("localStorage");
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
import { Child, Parent } from "pym.js";
|
||||
import uuid from "uuid/v4";
|
||||
import { PromisifiedStorage } from "./PromisifiedStorage";
|
||||
|
||||
type Pym = Child | Parent;
|
||||
|
||||
export interface PymStorage {
|
||||
/**
|
||||
* value = storage[key]
|
||||
*/
|
||||
getItem(key: string): Promise<string | null>;
|
||||
/**
|
||||
* delete storage[key]
|
||||
*/
|
||||
removeItem(key: string): Promise<void>;
|
||||
/**
|
||||
* storage[key] = value
|
||||
*/
|
||||
setItem(key: string, value: string): Promise<void>;
|
||||
}
|
||||
|
||||
class PymStorageImpl implements PymStorage {
|
||||
class PymStorage implements PromisifiedStorage {
|
||||
/** Instance to pym */
|
||||
private pym: Pym;
|
||||
|
||||
@@ -34,7 +20,7 @@ class PymStorageImpl implements PymStorage {
|
||||
/** Requests method with parameters over pym. */
|
||||
private call<T>(
|
||||
method: string,
|
||||
parameters: { key: string; value?: string }
|
||||
parameters: Record<string, any> = {}
|
||||
): Promise<T> {
|
||||
const id = uuid();
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -69,6 +55,15 @@ class PymStorageImpl implements PymStorage {
|
||||
this.listen();
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.call<number>("length");
|
||||
}
|
||||
public key(n: number) {
|
||||
return this.call<string | null>("key", { n });
|
||||
}
|
||||
public clear() {
|
||||
return this.call<void>("clear");
|
||||
}
|
||||
public setItem(key: string, value: string) {
|
||||
return this.call<void>("setItem", { key, value });
|
||||
}
|
||||
@@ -90,5 +85,5 @@ export default function createPymStorage(
|
||||
pym: Pym,
|
||||
type: "localStorage" | "sessionStorage"
|
||||
): PymStorage {
|
||||
return new PymStorageImpl(pym, type);
|
||||
return new PymStorage(pym, type);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import prefixStorage from "./prefixStorage";
|
||||
|
||||
export default function createSessionStorage(): Storage {
|
||||
return prefixStorage(window.sessionStorage, "talk");
|
||||
export default function createSessionStorage(prefix = "talk:"): Storage {
|
||||
return prefixStorage(window.sessionStorage, prefix);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`accepts predefined data 1`] = `"{\\"a\\":\\"0\\",\\"b\\":\\"1\\",\\"c\\":\\"2\\"}"`;
|
||||
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should call clear 1`] = `"{\\"a\\":\\"0\\",\\"b\\":\\"1\\",\\"d\\":\\"3\\"}"`;
|
||||
@@ -1,4 +1,8 @@
|
||||
export { default as createInMemoryStorage } from "./InMemoryStorage";
|
||||
export { default as createLocalStorage } from "./LocalStorage";
|
||||
export { default as createSessionStorage } from "./SessionStorage";
|
||||
export { default as createPymStorage, PymStorage } from "./PymStorage";
|
||||
export { default as createPymStorage } from "./PymStorage";
|
||||
export {
|
||||
default as createPromisifiedStorage,
|
||||
PromisifiedStorage,
|
||||
} from "./PromisifiedStorage";
|
||||
|
||||
@@ -1,54 +1,47 @@
|
||||
import sinon from "sinon";
|
||||
import createInMemoryStorage from "./InMemoryStorage";
|
||||
import prefixStorage from "./prefixStorage";
|
||||
|
||||
it("should call clear", () => {
|
||||
const storage = {
|
||||
clear: sinon.mock().once(),
|
||||
};
|
||||
it("should get nth key", () => {
|
||||
const storage = createInMemoryStorage({
|
||||
a: "0",
|
||||
b: "1",
|
||||
"talk:c": "2",
|
||||
d: "3",
|
||||
"talk:e": "4",
|
||||
});
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
const prefixed = prefixStorage(storage, "talk:");
|
||||
expect(prefixed.key(0)).toBe("talk:c");
|
||||
expect(prefixed.key(1)).toBe("talk:e");
|
||||
expect(prefixed.key(2)).toBeNull();
|
||||
});
|
||||
|
||||
it("should call clear", () => {
|
||||
const storage = createInMemoryStorage({
|
||||
a: "0",
|
||||
b: "1",
|
||||
"talk:c": "2",
|
||||
d: "3",
|
||||
"talk:e": "4",
|
||||
});
|
||||
|
||||
const prefixed = prefixStorage(storage, "talk:");
|
||||
prefixed.clear();
|
||||
storage.clear.verify();
|
||||
expect(storage.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should call length", () => {
|
||||
const ret = 10;
|
||||
const storage = {
|
||||
get length() {
|
||||
return ret;
|
||||
},
|
||||
};
|
||||
const storage = createInMemoryStorage({
|
||||
a: "0",
|
||||
b: "1",
|
||||
"talk:c": "2",
|
||||
d: "3",
|
||||
"talk:e": "4",
|
||||
});
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
expect(prefixed.length).toBe(ret);
|
||||
});
|
||||
|
||||
it("should call key", () => {
|
||||
const ret = "value";
|
||||
const storage = {
|
||||
key: sinon
|
||||
.mock()
|
||||
.withArgs(3)
|
||||
.returns(ret),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
expect(prefixed.key(3)).toBe(ret);
|
||||
(storage.key as any).verify();
|
||||
});
|
||||
|
||||
it("should call key", () => {
|
||||
const ret = "value";
|
||||
const storage = {
|
||||
key: sinon
|
||||
.mock()
|
||||
.withArgs(3)
|
||||
.returns(ret),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
expect(prefixed.key(3)).toBe(ret);
|
||||
(storage.key as any).verify();
|
||||
const prefixed = prefixStorage(storage, "talk:");
|
||||
expect(prefixed.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should prefix setItem", () => {
|
||||
@@ -56,7 +49,7 @@ it("should prefix setItem", () => {
|
||||
setItem: sinon.mock().withArgs("talk:key", "value"),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
const prefixed = prefixStorage(storage as any, "talk:");
|
||||
prefixed.setItem("key", "value");
|
||||
storage.setItem.verify();
|
||||
});
|
||||
@@ -66,7 +59,7 @@ it("should prefix removeItem", () => {
|
||||
removeItem: sinon.mock().withArgs("talk:key"),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
const prefixed = prefixStorage(storage as any, "talk:");
|
||||
prefixed.removeItem("key");
|
||||
storage.removeItem.verify();
|
||||
});
|
||||
@@ -80,7 +73,7 @@ it("should prefix getItem", () => {
|
||||
.returns(ret),
|
||||
};
|
||||
|
||||
const prefixed = prefixStorage(storage as any, "talk");
|
||||
const prefixed = prefixStorage(storage as any, "talk:");
|
||||
expect(prefixed.getItem("key")).toBe(ret);
|
||||
(storage.getItem as any).verify();
|
||||
});
|
||||
|
||||
@@ -12,27 +12,50 @@ class PrefixedStorage implements Storage {
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this.storage.length;
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
if (this.storage.key(i)!.startsWith(this.prefix)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.storage.clear();
|
||||
const toBeDeleted = [];
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
const key = this.storage.key(i)!;
|
||||
if (key.startsWith(this.prefix)) {
|
||||
toBeDeleted.push(key);
|
||||
}
|
||||
}
|
||||
toBeDeleted.forEach(key => this.storage.removeItem(key));
|
||||
}
|
||||
|
||||
public key(n: number) {
|
||||
return this.storage.key(n);
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.storage.length; i++) {
|
||||
const key = this.storage.key(i)!;
|
||||
if (key.startsWith(this.prefix)) {
|
||||
if (count === n) {
|
||||
return key;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getItem(key: string) {
|
||||
return this.storage.getItem(`${this.prefix}:${key}`);
|
||||
return this.storage.getItem(`${this.prefix}${key}`);
|
||||
}
|
||||
|
||||
public setItem(key: string, value: string) {
|
||||
return this.storage.setItem(`${this.prefix}:${key}`, value);
|
||||
return this.storage.setItem(`${this.prefix}${key}`, value);
|
||||
}
|
||||
|
||||
public removeItem(key: string) {
|
||||
return this.storage.removeItem(`${this.prefix}:${key}`);
|
||||
return this.storage.removeItem(`${this.prefix}${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { commitLocalUpdate, Environment, RecordSource } from "relay-runtime";
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createInMemoryStorage } from "talk-framework/lib/storage";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
@@ -16,7 +15,7 @@ beforeAll(() => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets auth token", async () => {
|
||||
it("Sets auth token to localStorage", () => {
|
||||
const context = {
|
||||
localStorage: createInMemoryStorage(),
|
||||
};
|
||||
@@ -26,26 +25,11 @@ it("Sets auth token", async () => {
|
||||
expect(context.localStorage.getItem("authToken")).toEqual(authToken);
|
||||
});
|
||||
|
||||
it("Removes auth token from localStorage", async () => {
|
||||
it("Removes auth token from localStorage", () => {
|
||||
const context = {
|
||||
localStorage: createInMemoryStorage(),
|
||||
};
|
||||
localStorage.setItem("authToken", "tmp");
|
||||
commit(environment, { authToken: null }, context as any);
|
||||
expect(context.localStorage.getItem("authToken")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Should call gc", async () => {
|
||||
const context = {
|
||||
localStorage: createInMemoryStorage(),
|
||||
};
|
||||
commitLocalUpdate(environment, store => {
|
||||
store.create("should-disappear", "tmp");
|
||||
});
|
||||
const authToken = null;
|
||||
expect(source.get("should-disappear")).not.toBeUndefined();
|
||||
commit(environment, { authToken }, context as any);
|
||||
await timeout();
|
||||
expect(source.get(LOCAL_ID)!.authToken).toEqual(authToken);
|
||||
expect(source.get("should-disappear")).toBeUndefined();
|
||||
expect(context.localStorage.getItem("authToken")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { PymStorage } from "talk-framework/lib/storage";
|
||||
|
||||
export class FakeStorage implements PymStorage {
|
||||
public store: Record<string, string> = {};
|
||||
|
||||
public setItem(key: string, value: string) {
|
||||
this.store[key] = value;
|
||||
return Promise.resolve();
|
||||
}
|
||||
public removeItem(key: string) {
|
||||
delete this.store[key];
|
||||
return Promise.resolve();
|
||||
}
|
||||
public getItem(key: string) {
|
||||
return Promise.resolve(this.store[key]);
|
||||
}
|
||||
}
|
||||
|
||||
export default function createFakePymStorage() {
|
||||
return new FakeStorage();
|
||||
}
|
||||
@@ -5,6 +5,31 @@ import path from "path";
|
||||
// These locale prefixes are always loaded.
|
||||
const commonPrefixes = ["common", "framework"];
|
||||
|
||||
function decorateErrorWhenMissing(bundle: FluentBundle) {
|
||||
const originalHasMessage = bundle.hasMessage;
|
||||
const originalGetMessage = bundle.getMessage;
|
||||
const missing: string[] = [];
|
||||
bundle.hasMessage = (id: string) => {
|
||||
const result = originalHasMessage.apply(bundle, [id]);
|
||||
if (!result) {
|
||||
const msg = `${bundle.locales} translation for key "${id}" not found`;
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(msg);
|
||||
missing.push(id);
|
||||
}
|
||||
// Even if it is missing, we say it is available and later return a descriptive error
|
||||
// string as the translation.
|
||||
return true;
|
||||
};
|
||||
bundle.getMessage = (id: string) => {
|
||||
if (missing.indexOf(id) !== -1) {
|
||||
return `Missing translation "${id}"`;
|
||||
}
|
||||
return originalGetMessage.apply(bundle, [id]);
|
||||
};
|
||||
return bundle;
|
||||
}
|
||||
|
||||
function createFluentBundle(
|
||||
target: string,
|
||||
pathToLocale: string
|
||||
@@ -15,13 +40,11 @@ function createFluentBundle(
|
||||
files.forEach(f => {
|
||||
prefixes.forEach(prefix => {
|
||||
if (f.startsWith(prefix)) {
|
||||
bundle.addMessages(
|
||||
fs.readFileSync(path.resolve(pathToLocale, f)).toString()
|
||||
);
|
||||
bundle.addMessages(require(path.resolve(pathToLocale, f)));
|
||||
}
|
||||
});
|
||||
});
|
||||
return bundle;
|
||||
return decorateErrorWhenMissing(bundle);
|
||||
}
|
||||
|
||||
export default createFluentBundle;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export default function createUUIDGenerator() {
|
||||
let counter = 0;
|
||||
return () => `uuid-${counter++}`;
|
||||
}
|
||||
@@ -4,8 +4,8 @@ export {
|
||||
} from "./createRelayEnvironment";
|
||||
export { default as createFluentBundle } from "./createFluentBundle";
|
||||
export { default as createSinonStub } from "./createSinonStub";
|
||||
export { default as createFakePymStorage } from "./createFakePymStorage";
|
||||
export {
|
||||
default as removeFragmentRefs,
|
||||
NoFragmentRefs,
|
||||
} from "./removeFragmentRefs";
|
||||
export { default as createUUIDGenerator } from "./createUUIDGenerator";
|
||||
|
||||
@@ -1,12 +1,42 @@
|
||||
import { ComponentType } from "react";
|
||||
|
||||
/** Remove all traces of `$fragmentRefs` and `$refType` from type recursively */
|
||||
/** Remove `$fragmentRefs` and `$refType` on a single object */
|
||||
export type OmitFragments<T> = Pick<
|
||||
T,
|
||||
{
|
||||
[P in keyof T]: P extends " $fragmentRefs" | " $refType" ? never : P
|
||||
}[keyof T]
|
||||
>;
|
||||
|
||||
export type NoFragmentRefs<T> = T extends object
|
||||
? {
|
||||
[P in Exclude<keyof T, " $fragmentRefs" | " $refType">]: NoFragmentRefs<
|
||||
T[P]
|
||||
>
|
||||
}
|
||||
? T extends ((...args: any[]) => any)
|
||||
? T
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<NoFragmentRefs2<U>> // TODO: (cvle) this should normally reference itself but it complains about a circular reference.
|
||||
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
|
||||
: T;
|
||||
|
||||
// TODO: (cvle) these NoFragmentRefX are a workaround for above issue
|
||||
export type NoFragmentRefs2<T> = T extends object
|
||||
? T extends ((...args: any[]) => any)
|
||||
? T
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<NoFragmentRefs3<U>>
|
||||
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
|
||||
: T;
|
||||
|
||||
export type NoFragmentRefs3<T> = T extends object
|
||||
? T extends ((...args: any[]) => any)
|
||||
? T
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<NoFragmentRefs4<U>>
|
||||
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
|
||||
: T;
|
||||
|
||||
export type NoFragmentRefs4<T> = T extends object
|
||||
? T extends ((...args: any[]) => any)
|
||||
? T
|
||||
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
|
||||
: T;
|
||||
|
||||
export default function removeFragmentRefs<T>(
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
.root {
|
||||
}
|
||||
.footer {
|
||||
.topBar {
|
||||
margin-bottom: calc(0.5 * var(--spacing-unit));
|
||||
}
|
||||
.footer:not(:empty) {
|
||||
margin-top: var(--spacing-unit);
|
||||
}
|
||||
.reply {
|
||||
margin-top: var(--spacing-unit);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import * as styles from "./Comment.css";
|
||||
import React, { ReactElement, StatelessComponent } from "react";
|
||||
|
||||
import PermalinkButtonContainer from "../../containers/PermalinkButtonContainer";
|
||||
import { Flex } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./Comment.css";
|
||||
import HTMLContent from "./HTMLContent";
|
||||
import Timestamp from "./Timestamp";
|
||||
import TopBar from "./TopBar";
|
||||
@@ -16,20 +16,21 @@ export interface CommentProps {
|
||||
} | null;
|
||||
body: string | null;
|
||||
createdAt: string;
|
||||
footer?: ReactElement<any> | Array<ReactElement<any>>;
|
||||
}
|
||||
|
||||
const Comment: StatelessComponent<CommentProps> = props => {
|
||||
return (
|
||||
<div role="article" className={styles.root}>
|
||||
<TopBar>
|
||||
<TopBar className={styles.topBar}>
|
||||
{props.author &&
|
||||
props.author.username && <Username>{props.author.username}</Username>}
|
||||
<Timestamp>{props.createdAt}</Timestamp>
|
||||
</TopBar>
|
||||
<HTMLContent>{props.body || ""}</HTMLContent>
|
||||
<div className={styles.footer}>
|
||||
<PermalinkButtonContainer commentID={props.id} />
|
||||
</div>
|
||||
<Flex className={styles.footer} direction="row" itemGutter="half">
|
||||
{props.footer}
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import IndentedComment from "./IndentedComment";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof IndentedComment> = {
|
||||
indentLevel: 1,
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: "Marvin",
|
||||
},
|
||||
body: "Woof",
|
||||
createdAt: "1995-12-17T03:24:00.000Z",
|
||||
};
|
||||
const wrapper = shallow(<IndentedComment {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import Indent from "../Indent";
|
||||
import Comment from "./Comment";
|
||||
|
||||
export interface IndentedCommentProps extends PropTypesOf<typeof Comment> {
|
||||
indentLevel?: number;
|
||||
}
|
||||
|
||||
const IndentedComment: StatelessComponent<IndentedCommentProps> = props => {
|
||||
const { indentLevel, ...rest } = props;
|
||||
const CommentElement = <Comment {...rest} />;
|
||||
const CommentwithIndent =
|
||||
(indentLevel && <Indent level={indentLevel}>{CommentElement}</Indent>) ||
|
||||
CommentElement;
|
||||
return CommentwithIndent;
|
||||
};
|
||||
|
||||
export default IndentedComment;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import ReplyButton from "./ReplyButton";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof ReplyButton> = {
|
||||
id: "id",
|
||||
onClick: noop,
|
||||
active: true,
|
||||
};
|
||||
const wrapper = shallow(<ReplyButton {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { EventHandler, MouseEvent, StatelessComponent } from "react";
|
||||
|
||||
import { Button, ButtonIcon, MatchMedia } from "talk-ui/components";
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
onClick?: EventHandler<MouseEvent<HTMLButtonElement>>;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const ReplyButton: StatelessComponent<Props> = props => (
|
||||
<Button
|
||||
id={props.id}
|
||||
onClick={props.onClick}
|
||||
variant="ghost"
|
||||
size="small"
|
||||
active={props.active}
|
||||
>
|
||||
<MatchMedia gtWidth="xs">
|
||||
<ButtonIcon>reply</ButtonIcon>
|
||||
</MatchMedia>
|
||||
<Localized id="comments-replyButton-reply">
|
||||
<span>Reply</span>
|
||||
</Localized>
|
||||
</Button>
|
||||
);
|
||||
|
||||
export default ReplyButton;
|
||||
@@ -1,3 +0,0 @@
|
||||
.root {
|
||||
margin-bottom: calc(0.5 * var(--spacing-unit));
|
||||
}
|
||||
@@ -4,15 +4,13 @@ import { StatelessComponent } from "react";
|
||||
|
||||
import { Flex, MatchMedia } from "talk-ui/components";
|
||||
|
||||
import * as styles from "./TopBar.css";
|
||||
|
||||
export interface TopBarProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const TopBar: StatelessComponent<TopBarProps> = props => {
|
||||
const rootClassName = cn(styles.root, props.className);
|
||||
const rootClassName = cn(props.className);
|
||||
return (
|
||||
<MatchMedia gtWidth="xs">
|
||||
{matches => (
|
||||
|
||||
@@ -5,7 +5,9 @@ exports[`renders username and body 1`] = `
|
||||
className="Comment-root"
|
||||
role="article"
|
||||
>
|
||||
<TopBar>
|
||||
<TopBar
|
||||
className="Comment-topBar"
|
||||
>
|
||||
<Username>
|
||||
Marvin
|
||||
</Username>
|
||||
@@ -16,12 +18,10 @@ exports[`renders username and body 1`] = `
|
||||
<HTMLContent>
|
||||
Woof
|
||||
</HTMLContent>
|
||||
<div
|
||||
<withPropsOnChange(Flex)
|
||||
className="Comment-footer"
|
||||
>
|
||||
<withContext(withLocalStateContainer(PermalinkContainer))
|
||||
commentID="comment-id"
|
||||
/>
|
||||
</div>
|
||||
direction="row"
|
||||
itemGutter="half"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<Indent
|
||||
level={1}
|
||||
>
|
||||
<Comment
|
||||
author={
|
||||
Object {
|
||||
"username": "Marvin",
|
||||
}
|
||||
}
|
||||
body="Woof"
|
||||
createdAt="1995-12-17T03:24:00.000Z"
|
||||
id="comment-id"
|
||||
/>
|
||||
</Indent>
|
||||
`;
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Button)
|
||||
active={true}
|
||||
id="id"
|
||||
onClick={[Function]}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
>
|
||||
<MatchMediaWithContext
|
||||
gtWidth="xs"
|
||||
>
|
||||
<withPropsOnChange(ButtonIcon)>
|
||||
reply
|
||||
</withPropsOnChange(ButtonIcon)>
|
||||
</MatchMediaWithContext>
|
||||
<Localized
|
||||
id="comments-replyButton-reply"
|
||||
>
|
||||
<span>
|
||||
Reply
|
||||
</span>
|
||||
</Localized>
|
||||
</withPropsOnChange(Button)>
|
||||
`;
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`renders correctly on big screens 1`] = `
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-flex Flex-itemGutter Flex-alignBaseline Flex-directionRow"
|
||||
className="Flex-root Flex-flex Flex-itemGutter Flex-alignBaseline Flex-directionRow"
|
||||
>
|
||||
<div>
|
||||
Hello World
|
||||
@@ -12,7 +12,7 @@ exports[`renders correctly on big screens 1`] = `
|
||||
|
||||
exports[`renders correctly on small screens 1`] = `
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<div>
|
||||
Hello World
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default, default as Comment, CommentProps } from "./Comment";
|
||||
export { default, default as IndentedComment } from "./IndentedComment";
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
.root {
|
||||
border-left: 3px solid;
|
||||
padding-left: var(--spacing-unit);
|
||||
}
|
||||
|
||||
.level0 {
|
||||
border-color: var(--palette-grey-darkest);
|
||||
.level1 {
|
||||
padding-left: var(--spacing-unit);
|
||||
border-left: 3px solid var(--palette-grey-darkest);
|
||||
}
|
||||
|
||||
.noBorder {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,29 @@ import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import Indent from "./Indent";
|
||||
|
||||
it("renders correctly", () => {
|
||||
it("renders level0", () => {
|
||||
const props: PropTypesOf<typeof Indent> = {
|
||||
children: <div>Hello World</div>,
|
||||
};
|
||||
const wrapper = shallow(<Indent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders level1", () => {
|
||||
const props: PropTypesOf<typeof Indent> = {
|
||||
level: 1,
|
||||
children: <div>Hello World</div>,
|
||||
};
|
||||
const wrapper = shallow(<Indent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders without border", () => {
|
||||
const props: PropTypesOf<typeof Indent> = {
|
||||
level: 1,
|
||||
noBorder: true,
|
||||
children: <div>Hello World</div>,
|
||||
};
|
||||
const wrapper = shallow(<Indent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -5,11 +5,21 @@ import * as styles from "./Indent.css";
|
||||
|
||||
export interface IndentProps {
|
||||
level?: number;
|
||||
noBorder?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Indent: StatelessComponent<IndentProps> = props => {
|
||||
return <div className={cn(styles.root, styles.level0)}>{props.children}</div>;
|
||||
return (
|
||||
<div
|
||||
className={cn(styles.root, {
|
||||
[styles.level1]: props.level === 1,
|
||||
[styles.noBorder]: props.noBorder,
|
||||
})}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Indent;
|
||||
|
||||
@@ -8,7 +8,9 @@ import CommentContainer from "../containers/CommentContainer";
|
||||
import * as styles from "./PermalinkView.css";
|
||||
|
||||
export interface PermalinkViewProps {
|
||||
comment: PropTypesOf<typeof CommentContainer>["data"] | null;
|
||||
me: PropTypesOf<typeof CommentContainer>["me"];
|
||||
asset: PropTypesOf<typeof CommentContainer>["asset"];
|
||||
comment: PropTypesOf<typeof CommentContainer>["comment"] | null;
|
||||
showAllCommentsHref: string | null;
|
||||
onShowAllComments: (e: MouseEvent<any>) => void;
|
||||
}
|
||||
@@ -16,7 +18,9 @@ export interface PermalinkViewProps {
|
||||
const PermalinkView: StatelessComponent<PermalinkViewProps> = ({
|
||||
showAllCommentsHref,
|
||||
comment,
|
||||
asset,
|
||||
onShowAllComments,
|
||||
me,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
@@ -42,7 +46,7 @@ const PermalinkView: StatelessComponent<PermalinkViewProps> = ({
|
||||
<Typography>Comment not found</Typography>
|
||||
</Localized>
|
||||
)}
|
||||
{comment && <CommentContainer data={comment} />}
|
||||
{comment && <CommentContainer me={me} comment={comment} asset={asset} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
.root {
|
||||
}
|
||||
|
||||
.textarea {
|
||||
composes: bodyCopy from "talk-ui/shared/typography.css";
|
||||
display: block;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: var(--spacing-unit);
|
||||
border-radius: var(--round-corners);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.poweredBy {
|
||||
margin-top: calc(-0.5 * var(--spacing-unit));
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface PostCommentFormProps {
|
||||
|
||||
const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
|
||||
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
|
||||
{({ handleSubmit, submitting }) => (
|
||||
{({ handleSubmit, submitting, hasValidationErrors }) => (
|
||||
<form
|
||||
autoComplete="off"
|
||||
onSubmit={handleSubmit}
|
||||
@@ -58,6 +58,7 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
|
||||
onChange={({ html }) => input.onChange(html)}
|
||||
value={input.value}
|
||||
placeholder="Post a comment"
|
||||
disabled={submitting}
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
@@ -79,7 +80,7 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
disabled={submitting}
|
||||
disabled={submitting || hasValidationErrors}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Blockquote, Bold, CoralRTE, Italic } from "@coralproject/rte";
|
||||
import { Localized as LocalizedOriginal } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { Ref, StatelessComponent } from "react";
|
||||
|
||||
import { Icon } from "talk-ui/components";
|
||||
|
||||
@@ -47,18 +47,20 @@ export interface RTEProps {
|
||||
onChange?: (data: { html: string; text: string }) => void;
|
||||
|
||||
disabled?: boolean;
|
||||
|
||||
forwardRef?: Ref<CoralRTE>;
|
||||
}
|
||||
|
||||
// tslint:disable:jsx-wrap-multiline
|
||||
const features = [
|
||||
<Localized key="bold" id="comments-rte-bold" attrs={{ title: true }}>
|
||||
<Bold>
|
||||
<Icon>format_bold</Icon>
|
||||
<Icon size="md">format_bold</Icon>
|
||||
</Bold>
|
||||
</Localized>,
|
||||
<Localized key="italic" id="comments-rte-italic" attrs={{ title: true }}>
|
||||
<Italic>
|
||||
<Icon>format_italic</Icon>
|
||||
<Icon size="md">format_italic</Icon>
|
||||
</Italic>
|
||||
</Localized>,
|
||||
<Localized
|
||||
@@ -67,7 +69,7 @@ const features = [
|
||||
attrs={{ title: true }}
|
||||
>
|
||||
<Blockquote key="blockquote">
|
||||
<Icon>format_quote</Icon>
|
||||
<Icon size="md">format_quote</Icon>
|
||||
</Blockquote>
|
||||
</Localized>,
|
||||
];
|
||||
@@ -83,6 +85,7 @@ const RTE: StatelessComponent<RTEProps> = props => {
|
||||
onChange,
|
||||
disabled,
|
||||
defaultValue,
|
||||
forwardRef,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
@@ -97,6 +100,7 @@ const RTE: StatelessComponent<RTEProps> = props => {
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
features={features}
|
||||
ref={forwardRef}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { CoralRTE } from "@coralproject/rte";
|
||||
import { FormState } from "final-form";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, {
|
||||
EventHandler,
|
||||
MouseEvent,
|
||||
Ref,
|
||||
StatelessComponent,
|
||||
} from "react";
|
||||
import { Field, Form, FormSpy } from "react-final-form";
|
||||
|
||||
import { OnSubmit } from "talk-framework/lib/form";
|
||||
import { required } from "talk-framework/lib/validation";
|
||||
import {
|
||||
AriaInfo,
|
||||
Button,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Typography,
|
||||
} from "talk-ui/components";
|
||||
|
||||
import RTE from "./RTE";
|
||||
|
||||
interface FormProps {
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface ReplyCommentFormProps {
|
||||
id: string;
|
||||
className?: string;
|
||||
onSubmit: OnSubmit<FormProps>;
|
||||
onCancel?: EventHandler<MouseEvent<any>>;
|
||||
onChange?: (state: FormState) => void;
|
||||
initialValues?: FormProps;
|
||||
rteRef?: Ref<CoralRTE>;
|
||||
}
|
||||
|
||||
const ReplyCommentForm: StatelessComponent<ReplyCommentFormProps> = props => {
|
||||
const inputID = `comments-replyCommentForm-rte-${props.id}`;
|
||||
return (
|
||||
<Form onSubmit={props.onSubmit} initialValues={props.initialValues}>
|
||||
{({ handleSubmit, submitting, hasValidationErrors }) => (
|
||||
<form
|
||||
className={props.className}
|
||||
autoComplete="off"
|
||||
onSubmit={handleSubmit}
|
||||
id={`comments-replyCommentForm-form-${props.id}`}
|
||||
>
|
||||
<FormSpy onChange={props.onChange} />
|
||||
<HorizontalGutter>
|
||||
<Field name="body" validate={required}>
|
||||
{({ input, meta }) => (
|
||||
<div>
|
||||
<Localized id="comments-replyCommentForm-rteLabel">
|
||||
<AriaInfo component="label" htmlFor={inputID}>
|
||||
Write a reply
|
||||
</AriaInfo>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="comments-replyCommentForm-rte"
|
||||
attrs={{ placeholder: true }}
|
||||
>
|
||||
<RTE
|
||||
inputId={inputID}
|
||||
onChange={({ html }) => input.onChange(html)}
|
||||
value={input.value}
|
||||
placeholder="Write a reply"
|
||||
forwardRef={props.rteRef}
|
||||
disabled={submitting}
|
||||
/>
|
||||
</Localized>
|
||||
{meta.touched &&
|
||||
(meta.error || meta.submitError) && (
|
||||
<Typography align="right" color="error" gutterBottom>
|
||||
{meta.error || meta.submitError}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<Flex direction="row" justifyContent="flex-end" itemGutter="half">
|
||||
<Localized id="comments-replyCommentForm-cancel">
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={submitting}
|
||||
onClick={props.onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="comments-replyCommentForm-submit">
|
||||
<Button
|
||||
color="primary"
|
||||
variant="filled"
|
||||
disabled={submitting || hasValidationErrors}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReplyCommentForm;
|
||||
@@ -12,11 +12,14 @@ const ReplyListN = removeFragmentRefs(ReplyList);
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof ReplyListN> = {
|
||||
commentID: "comment-id",
|
||||
asset: { id: "asset-id" },
|
||||
comment: { id: "comment-id" },
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
onShowAll: noop,
|
||||
hasMore: false,
|
||||
disableShowAll: false,
|
||||
indentLevel: 1,
|
||||
me: null,
|
||||
};
|
||||
const wrapper = shallow(<ReplyListN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -24,11 +27,14 @@ it("renders correctly", () => {
|
||||
|
||||
describe("when there is more", () => {
|
||||
const props: PropTypesOf<typeof ReplyListN> = {
|
||||
commentID: "comment-id",
|
||||
asset: { id: "asset-id" },
|
||||
comment: { id: "comment-id" },
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
onShowAll: sinon.spy(),
|
||||
hasMore: true,
|
||||
disableShowAll: false,
|
||||
indentLevel: 1,
|
||||
me: null,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<ReplyListN {...props} />);
|
||||
|
||||
@@ -9,30 +9,41 @@ import CommentContainer from "../containers/CommentContainer";
|
||||
import Indent from "./Indent";
|
||||
|
||||
export interface ReplyListProps {
|
||||
commentID: string;
|
||||
asset: PropTypesOf<typeof CommentContainer>["asset"];
|
||||
me: PropTypesOf<typeof CommentContainer>["me"];
|
||||
comment: {
|
||||
id: string;
|
||||
};
|
||||
comments: ReadonlyArray<
|
||||
{ id: string } & PropTypesOf<typeof CommentContainer>["data"]
|
||||
{ id: string } & PropTypesOf<typeof CommentContainer>["comment"]
|
||||
>;
|
||||
onShowAll: () => void;
|
||||
hasMore: boolean;
|
||||
disableShowAll: boolean;
|
||||
indentLevel?: number;
|
||||
}
|
||||
|
||||
const ReplyList: StatelessComponent<ReplyListProps> = props => {
|
||||
return (
|
||||
<Indent>
|
||||
<HorizontalGutter
|
||||
id={`talk-comments-replyList-log--${props.commentID}`}
|
||||
role="log"
|
||||
>
|
||||
{props.comments.map(comment => (
|
||||
<CommentContainer key={comment.id} data={comment} />
|
||||
))}
|
||||
{props.hasMore && (
|
||||
<HorizontalGutter
|
||||
id={`talk-comments-replyList-log--${props.comment.id}`}
|
||||
role="log"
|
||||
>
|
||||
{props.comments.map(comment => (
|
||||
<CommentContainer
|
||||
key={comment.id}
|
||||
me={props.me}
|
||||
comment={comment}
|
||||
asset={props.asset}
|
||||
indentLevel={props.indentLevel}
|
||||
/>
|
||||
))}
|
||||
{props.hasMore && (
|
||||
<Indent level={props.indentLevel} noBorder>
|
||||
<Localized id="comments-replyList-showAll">
|
||||
<Button
|
||||
id={`talk-comments-replyList-showAll--${props.commentID}`}
|
||||
aria-controls={`talk-comments-replyList-log--${props.commentID}`}
|
||||
id={`talk-comments-replyList-showAll--${props.comment.id}`}
|
||||
aria-controls={`talk-comments-replyList-log--${props.comment.id}`}
|
||||
onClick={props.onShowAll}
|
||||
disabled={props.disableShowAll}
|
||||
variant="outlined"
|
||||
@@ -41,9 +52,9 @@ const ReplyList: StatelessComponent<ReplyListProps> = props => {
|
||||
Show All Replies
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
</Indent>
|
||||
</Indent>
|
||||
)}
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,13 +12,15 @@ const StreamN = removeFragmentRefs(Stream);
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof StreamN> = {
|
||||
assetID: "asset-id",
|
||||
isClosed: false,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
isClosed: false,
|
||||
},
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
onLoadMore: noop,
|
||||
disableLoadMore: false,
|
||||
hasMore: false,
|
||||
user: null,
|
||||
me: null,
|
||||
};
|
||||
const wrapper = shallow(<StreamN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -27,13 +29,15 @@ it("renders correctly", () => {
|
||||
describe("when use is logged in", () => {
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof StreamN> = {
|
||||
assetID: "asset-id",
|
||||
isClosed: false,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
isClosed: false,
|
||||
},
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
onLoadMore: noop,
|
||||
disableLoadMore: false,
|
||||
hasMore: false,
|
||||
user: {},
|
||||
me: {},
|
||||
};
|
||||
const wrapper = shallow(<StreamN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -42,13 +46,15 @@ describe("when use is logged in", () => {
|
||||
|
||||
describe("when there is more", () => {
|
||||
const props: PropTypesOf<typeof StreamN> = {
|
||||
assetID: "asset-id",
|
||||
isClosed: false,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
isClosed: false,
|
||||
},
|
||||
comments: [{ id: "comment-1" }, { id: "comment-2" }],
|
||||
onLoadMore: sinon.spy(),
|
||||
disableLoadMore: false,
|
||||
hasMore: true,
|
||||
user: null,
|
||||
me: null,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<StreamN {...props} />);
|
||||
|
||||
@@ -13,25 +13,32 @@ import PostCommentFormFake from "./PostCommentFormFake";
|
||||
import * as styles from "./Stream.css";
|
||||
|
||||
export interface StreamProps {
|
||||
assetID: string;
|
||||
isClosed?: boolean;
|
||||
asset: {
|
||||
id: string;
|
||||
isClosed?: boolean;
|
||||
} & PropTypesOf<typeof CommentContainer>["asset"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["asset"];
|
||||
comments: ReadonlyArray<
|
||||
{ id: string } & PropTypesOf<typeof CommentContainer>["data"] &
|
||||
{ id: string } & PropTypesOf<typeof CommentContainer>["comment"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["comment"]
|
||||
>;
|
||||
onLoadMore?: () => void;
|
||||
hasMore?: boolean;
|
||||
disableLoadMore?: boolean;
|
||||
user: PropTypesOf<typeof UserBoxContainer>["user"] | null;
|
||||
me:
|
||||
| PropTypesOf<typeof UserBoxContainer>["me"] &
|
||||
PropTypesOf<typeof CommentContainer>["me"] &
|
||||
PropTypesOf<typeof ReplyListContainer>["me"]
|
||||
| null;
|
||||
}
|
||||
|
||||
const Stream: StatelessComponent<StreamProps> = props => {
|
||||
return (
|
||||
<HorizontalGutter className={styles.root} size="double">
|
||||
<HorizontalGutter size="half">
|
||||
<UserBoxContainer user={props.user} />
|
||||
{props.user ? (
|
||||
<PostCommentFormContainer assetID={props.assetID} />
|
||||
<UserBoxContainer me={props.me} />
|
||||
{props.me ? (
|
||||
<PostCommentFormContainer assetID={props.asset.id} />
|
||||
) : (
|
||||
<PostCommentFormFake />
|
||||
)}
|
||||
@@ -43,8 +50,16 @@ const Stream: StatelessComponent<StreamProps> = props => {
|
||||
>
|
||||
{props.comments.map(comment => (
|
||||
<HorizontalGutter key={comment.id}>
|
||||
<CommentContainer data={comment} />
|
||||
<ReplyListContainer comment={comment} />
|
||||
<CommentContainer
|
||||
me={props.me}
|
||||
comment={comment}
|
||||
asset={props.asset}
|
||||
/>
|
||||
<ReplyListContainer
|
||||
me={props.me}
|
||||
comment={comment}
|
||||
asset={props.asset}
|
||||
/>
|
||||
</HorizontalGutter>
|
||||
))}
|
||||
{props.hasMore && (
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
exports[`renders level0 1`] = `
|
||||
<div
|
||||
className="Indent-root Indent-level0"
|
||||
className="Indent-root"
|
||||
>
|
||||
<div>
|
||||
Hello World
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders level1 1`] = `
|
||||
<div
|
||||
className="Indent-root Indent-level1"
|
||||
>
|
||||
<div>
|
||||
Hello World
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders without border 1`] = `
|
||||
<div
|
||||
className="Indent-root Indent-level1 Indent-noBorder"
|
||||
>
|
||||
<div>
|
||||
Hello World
|
||||
|
||||
@@ -18,7 +18,9 @@ exports[`renders correctly 1`] = `
|
||||
id="comments-rte-bold"
|
||||
>
|
||||
<Toggle>
|
||||
<withPropsOnChange(Icon)>
|
||||
<withPropsOnChange(Icon)
|
||||
size="md"
|
||||
>
|
||||
format_bold
|
||||
</withPropsOnChange(Icon)>
|
||||
</Toggle>
|
||||
@@ -32,7 +34,9 @@ exports[`renders correctly 1`] = `
|
||||
id="comments-rte-italic"
|
||||
>
|
||||
<Toggle>
|
||||
<withPropsOnChange(Icon)>
|
||||
<withPropsOnChange(Icon)
|
||||
size="md"
|
||||
>
|
||||
format_italic
|
||||
</withPropsOnChange(Icon)>
|
||||
</Toggle>
|
||||
@@ -46,7 +50,9 @@ exports[`renders correctly 1`] = `
|
||||
id="comments-rte-blockquote"
|
||||
>
|
||||
<Toggle>
|
||||
<withPropsOnChange(Icon)>
|
||||
<withPropsOnChange(Icon)
|
||||
size="md"
|
||||
>
|
||||
format_quote
|
||||
</withPropsOnChange(Icon)>
|
||||
</Toggle>
|
||||
|
||||
@@ -1,53 +1,82 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<Indent>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
id="talk-comments-replyList-log--comment-id"
|
||||
role="log"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
id="talk-comments-replyList-log--comment-id"
|
||||
role="log"
|
||||
>
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
key="comment-1"
|
||||
/>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
key="comment-2"
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</Indent>
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
me={null}
|
||||
/>
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-2"
|
||||
me={null}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
`;
|
||||
|
||||
exports[`when there is more disables load more button 1`] = `
|
||||
<Indent>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
id="talk-comments-replyList-log--comment-id"
|
||||
role="log"
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
id="talk-comments-replyList-log--comment-id"
|
||||
role="log"
|
||||
>
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
me={null}
|
||||
/>
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-2"
|
||||
me={null}
|
||||
/>
|
||||
<Indent
|
||||
level={1}
|
||||
noBorder={true}
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
key="comment-1"
|
||||
/>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
key="comment-2"
|
||||
/>
|
||||
<Localized
|
||||
id="comments-replyList-showAll"
|
||||
>
|
||||
@@ -62,32 +91,49 @@ exports[`when there is more disables load more button 1`] = `
|
||||
Show All Replies
|
||||
</withPropsOnChange(Button)>
|
||||
</Localized>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</Indent>
|
||||
</Indent>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
`;
|
||||
|
||||
exports[`when there is more renders a load more button 1`] = `
|
||||
<Indent>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
id="talk-comments-replyList-log--comment-id"
|
||||
role="log"
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
id="talk-comments-replyList-log--comment-id"
|
||||
role="log"
|
||||
>
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-1"
|
||||
me={null}
|
||||
/>
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
indentLevel={1}
|
||||
key="comment-2"
|
||||
me={null}
|
||||
/>
|
||||
<Indent
|
||||
level={1}
|
||||
noBorder={true}
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
key="comment-1"
|
||||
/>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
key="comment-2"
|
||||
/>
|
||||
<Localized
|
||||
id="comments-replyList-showAll"
|
||||
>
|
||||
@@ -102,6 +148,6 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
Show All Replies
|
||||
</withPropsOnChange(Button)>
|
||||
</Localized>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</Indent>
|
||||
</Indent>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
`;
|
||||
|
||||
@@ -9,7 +9,7 @@ exports[`renders correctly 1`] = `
|
||||
size="half"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
|
||||
user={null}
|
||||
me={null}
|
||||
/>
|
||||
<PostCommentFormFake />
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -21,37 +21,65 @@ exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
key="comment-1"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -67,7 +95,7 @@ exports[`when there is more disables load more button 1`] = `
|
||||
size="half"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
|
||||
user={null}
|
||||
me={null}
|
||||
/>
|
||||
<PostCommentFormFake />
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -79,37 +107,65 @@ exports[`when there is more disables load more button 1`] = `
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
key="comment-1"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<Localized
|
||||
@@ -139,7 +195,7 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
size="half"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
|
||||
user={null}
|
||||
me={null}
|
||||
/>
|
||||
<PostCommentFormFake />
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
@@ -151,37 +207,65 @@ exports[`when there is more renders a load more button 1`] = `
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
key="comment-1"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
me={null}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<Localized
|
||||
@@ -211,7 +295,7 @@ exports[`when use is logged in renders correctly 1`] = `
|
||||
size="half"
|
||||
>
|
||||
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
|
||||
user={Object {}}
|
||||
me={Object {}}
|
||||
/>
|
||||
<withContext(withContext(createMutationContainer(PostCommentFormContainer)))
|
||||
assetID="asset-id"
|
||||
@@ -225,37 +309,65 @@ exports[`when use is logged in renders correctly 1`] = `
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
key="comment-1"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
me={Object {}}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-1",
|
||||
}
|
||||
}
|
||||
me={Object {}}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
<withPropsOnChange(HorizontalGutter)
|
||||
key="comment-2"
|
||||
>
|
||||
<Relay(CommentContainer)
|
||||
data={
|
||||
<withContext(createMutationContainer(Relay(CommentContainer)))
|
||||
asset={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
me={Object {}}
|
||||
/>
|
||||
<Relay(ReplyListContainer)
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-2",
|
||||
}
|
||||
}
|
||||
me={Object {}}
|
||||
/>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
</withPropsOnChange(HorizontalGutter)>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import { removeFragmentRefs } from "talk-framework/testHelpers";
|
||||
@@ -11,7 +12,11 @@ const CommentContainerN = removeFragmentRefs(CommentContainer);
|
||||
|
||||
it("renders username and body", () => {
|
||||
const props: PropTypesOf<typeof CommentContainerN> = {
|
||||
data: {
|
||||
me: null,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: "Marvin",
|
||||
@@ -19,6 +24,8 @@ it("renders username and body", () => {
|
||||
body: "Woof",
|
||||
createdAt: "1995-12-17T03:24:00.000Z",
|
||||
},
|
||||
indentLevel: 1,
|
||||
showAuthPopup: noop as any,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<CommentContainerN {...props} />);
|
||||
@@ -27,7 +34,11 @@ it("renders username and body", () => {
|
||||
|
||||
it("renders body only", () => {
|
||||
const props: PropTypesOf<typeof CommentContainerN> = {
|
||||
data: {
|
||||
me: null,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: null,
|
||||
@@ -35,6 +46,8 @@ it("renders body only", () => {
|
||||
body: "Woof",
|
||||
createdAt: "1995-12-17T03:24:00.000Z",
|
||||
},
|
||||
indentLevel: 1,
|
||||
showAuthPopup: noop as any,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<CommentContainerN {...props} />);
|
||||
|
||||
@@ -1,40 +1,110 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import React, { Component } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import withFragmentContainer from "talk-framework/lib/relay/withFragmentContainer";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { CommentContainer as Data } from "talk-stream/__generated__/CommentContainer.graphql";
|
||||
import { CommentContainer_asset as AssetData } from "talk-stream/__generated__/CommentContainer_asset.graphql";
|
||||
import { CommentContainer_comment as CommentData } from "talk-stream/__generated__/CommentContainer_comment.graphql";
|
||||
import { CommentContainer_me as MeData } from "talk-stream/__generated__/CommentContainer_me.graphql";
|
||||
import {
|
||||
ShowAuthPopupMutation,
|
||||
withShowAuthPopupMutation,
|
||||
} from "talk-stream/mutations";
|
||||
|
||||
import Comment from "../components/Comment";
|
||||
import ReplyButton from "../components/Comment/ReplyButton";
|
||||
import ReplyCommentFormContainer from ".//ReplyCommentFormContainer";
|
||||
import PermalinkButtonContainer from "./PermalinkButtonContainer";
|
||||
|
||||
interface InnerProps {
|
||||
data: Data;
|
||||
me: MeData | null;
|
||||
comment: CommentData;
|
||||
asset: AssetData;
|
||||
indentLevel?: number;
|
||||
showAuthPopup: ShowAuthPopupMutation;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
graphql`
|
||||
fragment CommentContainer_comment on Comment {
|
||||
id
|
||||
author {
|
||||
username
|
||||
interface State {
|
||||
showReplyDialog: boolean;
|
||||
}
|
||||
|
||||
export class CommentContainer extends Component<InnerProps, State> {
|
||||
public state = {
|
||||
showReplyDialog: false,
|
||||
};
|
||||
|
||||
private openReplyDialog = () => {
|
||||
if (this.props.me) {
|
||||
this.setState(state => ({
|
||||
showReplyDialog: true,
|
||||
}));
|
||||
} else {
|
||||
this.props.showAuthPopup({ view: "SIGN_IN" });
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
};
|
||||
|
||||
private closeReplyDialog = () => {
|
||||
this.setState(state => ({
|
||||
showReplyDialog: false,
|
||||
}));
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { comment, asset, ...rest } = this.props;
|
||||
const { showReplyDialog } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Comment
|
||||
{...rest}
|
||||
{...comment}
|
||||
footer={
|
||||
<>
|
||||
<ReplyButton
|
||||
id={`comments-commentContainer-replyButton-${comment.id}`}
|
||||
onClick={this.openReplyDialog}
|
||||
active={showReplyDialog}
|
||||
/>
|
||||
<PermalinkButtonContainer commentID={comment.id} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{showReplyDialog && (
|
||||
<ReplyCommentFormContainer
|
||||
comment={comment}
|
||||
asset={asset}
|
||||
onClose={this.closeReplyDialog}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export const CommentContainer: StatelessComponent<InnerProps> = props => {
|
||||
const { data, ...rest } = props;
|
||||
return <Comment {...rest} {...props.data} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<InnerProps>({
|
||||
data: graphql`
|
||||
fragment CommentContainer on Comment {
|
||||
...CommentContainer_comment @relay(mask: false)
|
||||
}
|
||||
`,
|
||||
})(CommentContainer);
|
||||
const enhanced = withShowAuthPopupMutation(
|
||||
withFragmentContainer<InnerProps>({
|
||||
me: graphql`
|
||||
fragment CommentContainer_me on User {
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
asset: graphql`
|
||||
fragment CommentContainer_asset on Asset {
|
||||
...ReplyCommentFormContainer_asset
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment CommentContainer_comment on Comment {
|
||||
id
|
||||
author {
|
||||
username
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
...ReplyCommentFormContainer_comment
|
||||
}
|
||||
`,
|
||||
})(CommentContainer)
|
||||
);
|
||||
|
||||
export type CommentContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
|
||||
@@ -5,7 +5,9 @@ import { graphql } from "react-relay";
|
||||
import { withContext } from "talk-framework/lib/bootstrap";
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { buildURL, parseURL } from "talk-framework/utils";
|
||||
import { PermalinkViewContainer_asset as AssetData } from "talk-stream/__generated__/PermalinkViewContainer_asset.graphql";
|
||||
import { PermalinkViewContainer_comment as CommentData } from "talk-stream/__generated__/PermalinkViewContainer_comment.graphql";
|
||||
import { PermalinkViewContainer_me as MeData } from "talk-stream/__generated__/PermalinkViewContainer_me.graphql";
|
||||
import {
|
||||
SetCommentIDMutation,
|
||||
withSetCommentIDMutation,
|
||||
@@ -15,6 +17,8 @@ import PermalinkView from "../components/PermalinkView";
|
||||
|
||||
interface PermalinkViewContainerProps {
|
||||
comment: CommentData | null;
|
||||
asset: AssetData;
|
||||
me: MeData | null;
|
||||
setCommentID: SetCommentIDMutation;
|
||||
pym: PymChild | undefined;
|
||||
}
|
||||
@@ -37,9 +41,11 @@ class PermalinkViewContainer extends React.Component<
|
||||
return buildURL({ ...urlParts, search });
|
||||
}
|
||||
public render() {
|
||||
const { comment } = this.props;
|
||||
const { comment, asset, me } = this.props;
|
||||
return (
|
||||
<PermalinkView
|
||||
me={me}
|
||||
asset={asset}
|
||||
comment={comment}
|
||||
showAllCommentsHref={this.getShowAllCommentsHref()}
|
||||
onShowAllComments={this.showAllComments}
|
||||
@@ -53,9 +59,19 @@ const enhanced = withContext(ctx => ({
|
||||
}))(
|
||||
withSetCommentIDMutation(
|
||||
withFragmentContainer<PermalinkViewContainerProps>({
|
||||
asset: graphql`
|
||||
fragment PermalinkViewContainer_asset on Asset {
|
||||
...CommentContainer_asset
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment PermalinkViewContainer_comment on Comment {
|
||||
...CommentContainer
|
||||
...CommentContainer_comment
|
||||
}
|
||||
`,
|
||||
me: graphql`
|
||||
fragment PermalinkViewContainer_me on User {
|
||||
...CommentContainer_me
|
||||
}
|
||||
`,
|
||||
})(PermalinkViewContainer)
|
||||
|
||||
@@ -3,10 +3,9 @@ import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createFakePymStorage } from "talk-framework/testHelpers";
|
||||
import { createPromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { PostCommentFormContainer } from "./PostCommentFormContainer";
|
||||
|
||||
const contextKey = "postCommentFormBody";
|
||||
@@ -16,7 +15,7 @@ it("renders correctly", async () => {
|
||||
// tslint:disable-next-line:no-empty
|
||||
createComment: (() => {}) as any,
|
||||
assetID: "asset-id",
|
||||
pymSessionStorage: createFakePymStorage(),
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
};
|
||||
|
||||
const wrapper = shallow(<PostCommentFormContainer {...props} />);
|
||||
@@ -30,10 +29,10 @@ it("renders with initialValues", async () => {
|
||||
// tslint:disable-next-line:no-empty
|
||||
createComment: (() => {}) as any,
|
||||
assetID: "asset-id",
|
||||
pymSessionStorage: createFakePymStorage(),
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
};
|
||||
|
||||
await props.pymSessionStorage.setItem(contextKey, "Hello World!");
|
||||
await props.sessionStorage.setItem(contextKey, "Hello World!");
|
||||
|
||||
const wrapper = shallow(<PostCommentFormContainer {...props} />);
|
||||
await timeout();
|
||||
@@ -46,10 +45,10 @@ it("save values", async () => {
|
||||
// tslint:disable-next-line:no-empty
|
||||
createComment: (() => {}) as any,
|
||||
assetID: "asset-id",
|
||||
pymSessionStorage: createFakePymStorage(),
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
};
|
||||
|
||||
await props.pymSessionStorage.setItem(contextKey, "Hello World!");
|
||||
await props.sessionStorage.setItem(contextKey, "Hello World!");
|
||||
|
||||
const wrapper = shallow(<PostCommentFormContainer {...props} />);
|
||||
await timeout();
|
||||
@@ -58,7 +57,7 @@ it("save values", async () => {
|
||||
.first()
|
||||
.props()
|
||||
.onChange({ values: { body: "changed" } });
|
||||
expect(await props.pymSessionStorage.getItem(contextKey)).toBe("changed");
|
||||
expect(await props.sessionStorage.getItem(contextKey)).toBe("changed");
|
||||
});
|
||||
|
||||
it("creates a comment", async () => {
|
||||
@@ -76,10 +75,10 @@ it("creates a comment", async () => {
|
||||
// tslint:disable-next-line:no-empty
|
||||
createComment: createCommentStub,
|
||||
assetID,
|
||||
pymSessionStorage: createFakePymStorage(),
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
};
|
||||
|
||||
await props.pymSessionStorage.setItem(contextKey, "Hello World!");
|
||||
await props.sessionStorage.setItem(contextKey, "Hello World!");
|
||||
|
||||
const wrapper = shallow(<PostCommentFormContainer {...props} />);
|
||||
await timeout();
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from "react";
|
||||
|
||||
import { withContext } from "talk-framework/lib/bootstrap";
|
||||
import { BadUserInputError } from "talk-framework/lib/errors";
|
||||
import { PymStorage } from "talk-framework/lib/storage";
|
||||
import { PromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
|
||||
import PostCommentForm, {
|
||||
@@ -13,7 +13,7 @@ import { CreateCommentMutation, withCreateCommentMutation } from "../mutations";
|
||||
interface InnerProps {
|
||||
createComment: CreateCommentMutation;
|
||||
assetID: string;
|
||||
pymSessionStorage: PymStorage;
|
||||
sessionStorage: PromisifiedStorage;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -32,7 +32,7 @@ export class PostCommentFormContainer extends Component<InnerProps, State> {
|
||||
}
|
||||
|
||||
private async init() {
|
||||
const body = await this.props.pymSessionStorage.getItem(contextKey);
|
||||
const body = await this.props.sessionStorage.getItem(contextKey);
|
||||
if (body) {
|
||||
this.setState({
|
||||
initialValues: {
|
||||
@@ -67,9 +67,9 @@ export class PostCommentFormContainer extends Component<InnerProps, State> {
|
||||
|
||||
private handleOnChange: PostCommentFormProps["onChange"] = state => {
|
||||
if (state.values.body) {
|
||||
this.props.pymSessionStorage.setItem(contextKey, state.values.body);
|
||||
this.props.sessionStorage.setItem(contextKey, state.values.body);
|
||||
} else {
|
||||
this.props.pymSessionStorage.removeItem(contextKey);
|
||||
this.props.sessionStorage.removeItem(contextKey);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,8 +87,8 @@ export class PostCommentFormContainer extends Component<InnerProps, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withContext(({ pymSessionStorage }) => ({
|
||||
pymSessionStorage,
|
||||
const enhanced = withContext(({ sessionStorage }) => ({
|
||||
sessionStorage,
|
||||
}))(withCreateCommentMutation(PostCommentFormContainer));
|
||||
export type PostCommentFormContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { shallow } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
import React from "react";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createPromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { removeFragmentRefs } from "talk-framework/testHelpers";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { ReplyCommentFormContainer } from "./ReplyCommentFormContainer";
|
||||
|
||||
const ReplyCommentFormContainerN = removeFragmentRefs(
|
||||
ReplyCommentFormContainer
|
||||
);
|
||||
|
||||
function getContextKey(commentID: string) {
|
||||
return `replyCommentFormBody-${commentID}`;
|
||||
}
|
||||
|
||||
it("renders correctly", async () => {
|
||||
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
|
||||
createComment: noop as any,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
},
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
autofocus: false,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<ReplyCommentFormContainerN {...props} />);
|
||||
await timeout();
|
||||
wrapper.update();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with initialValues", async () => {
|
||||
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
|
||||
createComment: noop as any,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
},
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
autofocus: false,
|
||||
};
|
||||
|
||||
await props.sessionStorage.setItem(
|
||||
getContextKey(props.comment.id),
|
||||
"Hello World!"
|
||||
);
|
||||
|
||||
const wrapper = shallow(<ReplyCommentFormContainerN {...props} />);
|
||||
await timeout();
|
||||
wrapper.update();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("save values", async () => {
|
||||
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
|
||||
createComment: noop as any,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
},
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
autofocus: false,
|
||||
};
|
||||
|
||||
await props.sessionStorage.setItem(
|
||||
getContextKey(props.comment.id),
|
||||
"Hello World!"
|
||||
);
|
||||
|
||||
const wrapper = shallow(<ReplyCommentFormContainerN {...props} />);
|
||||
await timeout();
|
||||
wrapper.update();
|
||||
wrapper
|
||||
.first()
|
||||
.props()
|
||||
.onChange({ values: { body: "changed" } });
|
||||
expect(
|
||||
await props.sessionStorage.getItem(getContextKey(props.comment.id))
|
||||
).toBe("changed");
|
||||
});
|
||||
|
||||
it("creates a comment", async () => {
|
||||
const assetID = "asset-id";
|
||||
const input = { body: "Hello World!" };
|
||||
const createCommentStub = sinon.stub();
|
||||
const form = { reset: noop };
|
||||
const onCloseStub = sinon.stub();
|
||||
|
||||
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
|
||||
createComment: createCommentStub,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
},
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
onClose: onCloseStub,
|
||||
autofocus: false,
|
||||
};
|
||||
|
||||
await props.sessionStorage.setItem(
|
||||
getContextKey(props.comment.id),
|
||||
"Hello World!"
|
||||
);
|
||||
|
||||
const wrapper = shallow(<ReplyCommentFormContainerN {...props} />);
|
||||
await timeout();
|
||||
wrapper.update();
|
||||
wrapper
|
||||
.first()
|
||||
.props()
|
||||
.onSubmit(input, form);
|
||||
expect(
|
||||
createCommentStub.calledWith({
|
||||
assetID,
|
||||
parentID: props.comment.id,
|
||||
...input,
|
||||
})
|
||||
).toBeTruthy();
|
||||
await timeout();
|
||||
expect(onCloseStub.calledOnce).toBe(true);
|
||||
});
|
||||
|
||||
it("closes on cancel", async () => {
|
||||
const onCloseStub = sinon.stub();
|
||||
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
|
||||
createComment: noop as any,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
},
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
onClose: onCloseStub,
|
||||
autofocus: false,
|
||||
};
|
||||
|
||||
await props.sessionStorage.setItem(
|
||||
getContextKey(props.comment.id),
|
||||
"Hello World!"
|
||||
);
|
||||
|
||||
const wrapper = shallow(<ReplyCommentFormContainerN {...props} />);
|
||||
await timeout();
|
||||
wrapper.update();
|
||||
wrapper.findWhere(w => !!w.prop("onCancel")).prop("onCancel")();
|
||||
|
||||
// Calls close.
|
||||
expect(onCloseStub.calledOnce).toBe(true);
|
||||
|
||||
// Removes saved value.
|
||||
expect(
|
||||
await props.sessionStorage.getItem(getContextKey(props.comment.id))
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("autofocuses", async () => {
|
||||
const focusStub = sinon.stub();
|
||||
const rte = { focus: focusStub };
|
||||
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
|
||||
createComment: noop as any,
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
},
|
||||
sessionStorage: createPromisifiedStorage(),
|
||||
autofocus: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<ReplyCommentFormContainerN {...props} />);
|
||||
await timeout();
|
||||
wrapper.update();
|
||||
wrapper
|
||||
.findWhere(n => n.prop("rteRef"))
|
||||
.props()
|
||||
.rteRef(rte);
|
||||
expect(focusStub.calledOnce).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import { CoralRTE } from "@coralproject/rte";
|
||||
import React, { Component } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withContext } from "talk-framework/lib/bootstrap";
|
||||
import { BadUserInputError } from "talk-framework/lib/errors";
|
||||
import { withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { PromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { ReplyCommentFormContainer_asset as AssetData } from "talk-stream/__generated__/ReplyCommentFormContainer_asset.graphql";
|
||||
import { ReplyCommentFormContainer_comment as CommentData } from "talk-stream/__generated__/ReplyCommentFormContainer_comment.graphql";
|
||||
|
||||
import ReplyCommentForm, {
|
||||
ReplyCommentFormProps,
|
||||
} from "../components/ReplyCommentForm";
|
||||
import { CreateCommentMutation, withCreateCommentMutation } from "../mutations";
|
||||
|
||||
interface InnerProps {
|
||||
createComment: CreateCommentMutation;
|
||||
sessionStorage: PromisifiedStorage;
|
||||
comment: CommentData;
|
||||
asset: AssetData;
|
||||
onClose?: () => void;
|
||||
autofocus: boolean;
|
||||
}
|
||||
|
||||
interface State {
|
||||
initialValues?: ReplyCommentFormProps["initialValues"];
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
export class ReplyCommentFormContainer extends Component<InnerProps, State> {
|
||||
public state: State = { initialized: false };
|
||||
private contextKey = `replyCommentFormBody-${this.props.comment.id}`;
|
||||
|
||||
constructor(props: InnerProps) {
|
||||
super(props);
|
||||
this.init();
|
||||
}
|
||||
|
||||
private handleRTERef = (rte: CoralRTE | null) => {
|
||||
if (rte && this.props.autofocus) {
|
||||
rte.focus();
|
||||
}
|
||||
};
|
||||
|
||||
private async init() {
|
||||
const body = await this.props.sessionStorage.getItem(this.contextKey);
|
||||
if (body) {
|
||||
this.setState({
|
||||
initialValues: {
|
||||
body,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
initialized: true,
|
||||
});
|
||||
}
|
||||
|
||||
private handleOnCancel = () => {
|
||||
this.props.sessionStorage.removeItem(this.contextKey);
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
private handleOnSubmit: ReplyCommentFormProps["onSubmit"] = async (
|
||||
input,
|
||||
form
|
||||
) => {
|
||||
try {
|
||||
await this.props.createComment({
|
||||
assetID: this.props.asset.id,
|
||||
parentID: this.props.comment.id,
|
||||
...input,
|
||||
});
|
||||
|
||||
this.props.sessionStorage.removeItem(this.contextKey);
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof BadUserInputError) {
|
||||
return error.invalidArgsLocalized;
|
||||
}
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(error);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
private handleOnChange: ReplyCommentFormProps["onChange"] = state => {
|
||||
if (state.values.body) {
|
||||
this.props.sessionStorage.setItem(this.contextKey, state.values.body);
|
||||
} else {
|
||||
this.props.sessionStorage.removeItem(this.contextKey);
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
if (!this.state.initialized) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ReplyCommentForm
|
||||
id={this.props.comment.id}
|
||||
onSubmit={this.handleOnSubmit}
|
||||
onChange={this.handleOnChange}
|
||||
initialValues={this.state.initialValues}
|
||||
onCancel={this.handleOnCancel}
|
||||
rteRef={this.handleRTERef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
|
||||
sessionStorage,
|
||||
// Disable autofocus on ios and enable for the rest.
|
||||
autofocus: !browserInfo.ios,
|
||||
}))(
|
||||
withCreateCommentMutation(
|
||||
withFragmentContainer<InnerProps>({
|
||||
asset: graphql`
|
||||
fragment ReplyCommentFormContainer_asset on Asset {
|
||||
id
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment ReplyCommentFormContainer_comment on Comment {
|
||||
id
|
||||
}
|
||||
`,
|
||||
})(ReplyCommentFormContainer)
|
||||
)
|
||||
);
|
||||
export type PostCommentFormContainerProps = PropTypesOf<typeof enhanced>;
|
||||
export default enhanced;
|
||||
@@ -13,6 +13,9 @@ const ReplyListContainerN = removeFragmentRefs(ReplyListContainer);
|
||||
|
||||
it("renders correctly", () => {
|
||||
const props: PropTypesOf<typeof ReplyListContainerN> = {
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
replies: {
|
||||
@@ -23,6 +26,7 @@ it("renders correctly", () => {
|
||||
hasMore: noop,
|
||||
isLoading: noop,
|
||||
} as any,
|
||||
me: null,
|
||||
};
|
||||
const wrapper = shallow(<ReplyListContainerN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -30,6 +34,9 @@ it("renders correctly", () => {
|
||||
|
||||
it("renders correctly when replies are null", () => {
|
||||
const props: PropTypesOf<typeof ReplyListContainerN> = {
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
replies: null,
|
||||
@@ -38,6 +45,7 @@ it("renders correctly when replies are null", () => {
|
||||
hasMore: noop,
|
||||
isLoading: noop,
|
||||
} as any,
|
||||
me: null,
|
||||
};
|
||||
const wrapper = shallow(<ReplyListContainerN {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -46,6 +54,9 @@ it("renders correctly when replies are null", () => {
|
||||
describe("when has more replies", () => {
|
||||
let finishLoading: ((error?: Error) => void) | null = null;
|
||||
const props: PropTypesOf<typeof ReplyListContainerN> = {
|
||||
asset: {
|
||||
id: "asset-id",
|
||||
},
|
||||
comment: {
|
||||
id: "comment-id",
|
||||
replies: {
|
||||
@@ -57,6 +68,7 @@ describe("when has more replies", () => {
|
||||
isLoading: () => false,
|
||||
loadMore: (_: any, callback: () => void) => (finishLoading = callback),
|
||||
} as any,
|
||||
me: null,
|
||||
};
|
||||
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
@@ -3,7 +3,9 @@ import { graphql, RelayPaginationProp } from "react-relay";
|
||||
|
||||
import { withPaginationContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { ReplyListContainer_comment as Data } from "talk-stream/__generated__/ReplyListContainer_comment.graphql";
|
||||
import { ReplyListContainer_asset as AssetData } from "talk-stream/__generated__/ReplyListContainer_asset.graphql";
|
||||
import { ReplyListContainer_comment as CommentData } from "talk-stream/__generated__/ReplyListContainer_comment.graphql";
|
||||
import { ReplyListContainer_me as MeData } from "talk-stream/__generated__/ReplyListContainer_me.graphql";
|
||||
import {
|
||||
COMMENT_SORT,
|
||||
ReplyListContainerPaginationQueryVariables,
|
||||
@@ -12,7 +14,9 @@ import {
|
||||
import ReplyList from "../components/ReplyList";
|
||||
|
||||
export interface InnerProps {
|
||||
comment: Data;
|
||||
me: MeData | null;
|
||||
asset: AssetData;
|
||||
comment: CommentData;
|
||||
relay: RelayPaginationProp;
|
||||
}
|
||||
|
||||
@@ -31,11 +35,14 @@ export class ReplyListContainer extends React.Component<InnerProps> {
|
||||
const comments = this.props.comment.replies.edges.map(edge => edge.node);
|
||||
return (
|
||||
<ReplyList
|
||||
commentID={this.props.comment.id}
|
||||
me={this.props.me}
|
||||
comment={this.props.comment}
|
||||
comments={comments}
|
||||
asset={this.props.asset}
|
||||
onShowAll={this.showAll}
|
||||
hasMore={this.props.relay.hasMore()}
|
||||
disableShowAll={this.state.disableShowAll}
|
||||
indentLevel={1}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -72,6 +79,16 @@ const enhanced = withPaginationContainer<
|
||||
FragmentVariables
|
||||
>(
|
||||
{
|
||||
me: graphql`
|
||||
fragment ReplyListContainer_me on User {
|
||||
...CommentContainer_me
|
||||
}
|
||||
`,
|
||||
asset: graphql`
|
||||
fragment ReplyListContainer_asset on Asset {
|
||||
...CommentContainer_asset
|
||||
}
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment ReplyListContainer_comment on Comment
|
||||
@argumentDefinitions(
|
||||
@@ -85,7 +102,7 @@ const enhanced = withPaginationContainer<
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...CommentContainer
|
||||
...CommentContainer_comment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ it("renders correctly", () => {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
},
|
||||
},
|
||||
user: null,
|
||||
me: null,
|
||||
relay: {
|
||||
hasMore: noop,
|
||||
isLoading: noop,
|
||||
@@ -40,7 +40,7 @@ describe("when has more comments", () => {
|
||||
edges: [{ node: { id: "comment-1" } }, { node: { id: "comment-2" } }],
|
||||
},
|
||||
},
|
||||
user: null,
|
||||
me: null,
|
||||
relay: {
|
||||
hasMore: () => true,
|
||||
isLoading: () => false,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { graphql, RelayPaginationProp } from "react-relay";
|
||||
import { withPaginationContainer } from "talk-framework/lib/relay";
|
||||
import { PropTypesOf } from "talk-framework/types";
|
||||
import { StreamContainer_asset as AssetData } from "talk-stream/__generated__/StreamContainer_asset.graphql";
|
||||
import { StreamContainer_user as UserData } from "talk-stream/__generated__/StreamContainer_user.graphql";
|
||||
import { StreamContainer_me as MeData } from "talk-stream/__generated__/StreamContainer_me.graphql";
|
||||
import {
|
||||
COMMENT_SORT,
|
||||
StreamContainerPaginationQueryVariables,
|
||||
@@ -14,10 +14,19 @@ import Stream from "../components/Stream";
|
||||
|
||||
interface InnerProps {
|
||||
asset: AssetData;
|
||||
user: UserData | null;
|
||||
me: MeData | null;
|
||||
relay: RelayPaginationProp;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
graphql`
|
||||
fragment StreamContainer_comment on Comment {
|
||||
id
|
||||
...CommentContainer_comment
|
||||
...ReplyListContainer_comment
|
||||
}
|
||||
`;
|
||||
|
||||
export class StreamContainer extends React.Component<InnerProps> {
|
||||
public state = {
|
||||
disableLoadMore: false,
|
||||
@@ -27,13 +36,12 @@ export class StreamContainer extends React.Component<InnerProps> {
|
||||
const comments = this.props.asset.comments.edges.map(edge => edge.node);
|
||||
return (
|
||||
<Stream
|
||||
assetID={this.props.asset.id}
|
||||
isClosed={this.props.asset.isClosed}
|
||||
asset={this.props.asset}
|
||||
comments={comments}
|
||||
onLoadMore={this.loadMore}
|
||||
hasMore={this.props.relay.hasMore()}
|
||||
disableLoadMore={this.state.disableLoadMore}
|
||||
user={this.props.user}
|
||||
me={this.props.me}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -82,17 +90,19 @@ const enhanced = withPaginationContainer<
|
||||
@connection(key: "Stream_comments") {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
...CommentContainer
|
||||
...ReplyListContainer_comment
|
||||
...StreamContainer_comment @relay(mask: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
...CommentContainer_asset
|
||||
...ReplyListContainer_asset
|
||||
}
|
||||
`,
|
||||
user: graphql`
|
||||
fragment StreamContainer_user on User {
|
||||
...UserBoxContainer_user
|
||||
me: graphql`
|
||||
fragment StreamContainer_me on User {
|
||||
...ReplyListContainer_me
|
||||
...CommentContainer_me
|
||||
...UserBoxContainer_me
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ it("renders correctly", () => {
|
||||
view: "SIGN_IN",
|
||||
},
|
||||
},
|
||||
user: null,
|
||||
me: null,
|
||||
// tslint:disable-next-line:no-empty
|
||||
showAuthPopup: async () => {},
|
||||
// tslint:disable-next-line:no-empty
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
withLocalStateContainer,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { SignOutMutation, withSignOutMutation } from "talk-framework/mutations";
|
||||
import { UserBoxContainer_user as UserData } from "talk-stream/__generated__/UserBoxContainer_user.graphql";
|
||||
import { UserBoxContainer_me as MeData } from "talk-stream/__generated__/UserBoxContainer_me.graphql";
|
||||
import { UserBoxContainerLocal as Local } from "talk-stream/__generated__/UserBoxContainerLocal.graphql";
|
||||
import UserBoxUnauthenticated from "talk-stream/components/UserBoxUnauthenticated";
|
||||
import {
|
||||
@@ -22,7 +22,7 @@ import UserBoxAuthenticated from "../components/UserBoxAuthenticated";
|
||||
|
||||
interface InnerProps {
|
||||
local: Local;
|
||||
user: UserData | null;
|
||||
me: MeData | null;
|
||||
showAuthPopup: ShowAuthPopupMutation;
|
||||
setAuthPopupState: SetAuthPopupStateMutation;
|
||||
signOut: SignOutMutation;
|
||||
@@ -40,16 +40,16 @@ export class UserBoxContainer extends Component<InnerProps> {
|
||||
local: {
|
||||
authPopup: { open, focus, view },
|
||||
},
|
||||
user,
|
||||
me,
|
||||
signOut,
|
||||
} = this.props;
|
||||
|
||||
if (user) {
|
||||
if (me) {
|
||||
return (
|
||||
<UserBoxAuthenticated
|
||||
onSignOut={signOut}
|
||||
// TODO: why nullable?
|
||||
username={user.username!}
|
||||
username={me.username!}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -90,8 +90,8 @@ const enhanced = withSignOutMutation(
|
||||
`
|
||||
)(
|
||||
withFragmentContainer<InnerProps>({
|
||||
user: graphql`
|
||||
fragment UserBoxContainer_user on User {
|
||||
me: graphql`
|
||||
fragment UserBoxContainer_me on User {
|
||||
username
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,27 +1,61 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders body only 1`] = `
|
||||
<Comment
|
||||
author={
|
||||
Object {
|
||||
"username": null,
|
||||
<React.Fragment>
|
||||
<IndentedComment
|
||||
author={
|
||||
Object {
|
||||
"username": null,
|
||||
}
|
||||
}
|
||||
}
|
||||
body="Woof"
|
||||
createdAt="1995-12-17T03:24:00.000Z"
|
||||
id="comment-id"
|
||||
/>
|
||||
body="Woof"
|
||||
createdAt="1995-12-17T03:24:00.000Z"
|
||||
footer={
|
||||
<React.Fragment>
|
||||
<ReplyButton
|
||||
active={false}
|
||||
id="comments-commentContainer-replyButton-comment-id"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<withContext(withLocalStateContainer(PermalinkContainer))
|
||||
commentID="comment-id"
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
id="comment-id"
|
||||
indentLevel={1}
|
||||
me={null}
|
||||
showAuthPopup={[Function]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
exports[`renders username and body 1`] = `
|
||||
<Comment
|
||||
author={
|
||||
Object {
|
||||
"username": "Marvin",
|
||||
<React.Fragment>
|
||||
<IndentedComment
|
||||
author={
|
||||
Object {
|
||||
"username": "Marvin",
|
||||
}
|
||||
}
|
||||
}
|
||||
body="Woof"
|
||||
createdAt="1995-12-17T03:24:00.000Z"
|
||||
id="comment-id"
|
||||
/>
|
||||
body="Woof"
|
||||
createdAt="1995-12-17T03:24:00.000Z"
|
||||
footer={
|
||||
<React.Fragment>
|
||||
<ReplyButton
|
||||
active={false}
|
||||
id="comments-commentContainer-replyButton-comment-id"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<withContext(withLocalStateContainer(PermalinkContainer))
|
||||
commentID="comment-id"
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
id="comment-id"
|
||||
indentLevel={1}
|
||||
me={null}
|
||||
showAuthPopup={[Function]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
`;
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<ReplyCommentForm
|
||||
id="comment-id"
|
||||
onCancel={[Function]}
|
||||
onChange={[Function]}
|
||||
onSubmit={[Function]}
|
||||
rteRef={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`renders with initialValues 1`] = `
|
||||
<ReplyCommentForm
|
||||
id="comment-id"
|
||||
initialValues={
|
||||
Object {
|
||||
"body": "Hello World!",
|
||||
}
|
||||
}
|
||||
onCancel={[Function]}
|
||||
onChange={[Function]}
|
||||
onSubmit={[Function]}
|
||||
rteRef={[Function]}
|
||||
/>
|
||||
`;
|
||||
@@ -2,7 +2,30 @@
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<ReplyList
|
||||
commentID="comment-id"
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-id",
|
||||
"replies": Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
@@ -14,6 +37,8 @@ exports[`renders correctly 1`] = `
|
||||
]
|
||||
}
|
||||
disableShowAll={false}
|
||||
indentLevel={1}
|
||||
me={null}
|
||||
onShowAll={[Function]}
|
||||
/>
|
||||
`;
|
||||
@@ -22,7 +47,30 @@ exports[`renders correctly when replies are null 1`] = `""`;
|
||||
|
||||
exports[`when has more replies renders hasMore 1`] = `
|
||||
<ReplyList
|
||||
commentID="comment-id"
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-id",
|
||||
"replies": Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
@@ -35,13 +83,38 @@ exports[`when has more replies renders hasMore 1`] = `
|
||||
}
|
||||
disableShowAll={false}
|
||||
hasMore={true}
|
||||
indentLevel={1}
|
||||
me={null}
|
||||
onShowAll={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more replies when showing all disables show all button 1`] = `
|
||||
<ReplyList
|
||||
commentID="comment-id"
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-id",
|
||||
"replies": Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
@@ -54,13 +127,38 @@ exports[`when has more replies when showing all disables show all button 1`] = `
|
||||
}
|
||||
disableShowAll={true}
|
||||
hasMore={true}
|
||||
indentLevel={1}
|
||||
me={null}
|
||||
onShowAll={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more replies when showing all enable show all button after loading is done 1`] = `
|
||||
<ReplyList
|
||||
commentID="comment-id"
|
||||
asset={
|
||||
Object {
|
||||
"id": "asset-id",
|
||||
}
|
||||
}
|
||||
comment={
|
||||
Object {
|
||||
"id": "comment-id",
|
||||
"replies": Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
@@ -73,6 +171,8 @@ exports[`when has more replies when showing all enable show all button after loa
|
||||
}
|
||||
disableShowAll={false}
|
||||
hasMore={true}
|
||||
indentLevel={1}
|
||||
me={null}
|
||||
onShowAll={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,26 @@
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<Stream
|
||||
assetID="asset-id"
|
||||
asset={
|
||||
Object {
|
||||
"comments": Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
@@ -14,15 +33,33 @@ exports[`renders correctly 1`] = `
|
||||
]
|
||||
}
|
||||
disableLoadMore={false}
|
||||
isClosed={false}
|
||||
me={null}
|
||||
onLoadMore={[Function]}
|
||||
user={null}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more comments renders hasMore 1`] = `
|
||||
<Stream
|
||||
assetID="asset-id"
|
||||
asset={
|
||||
Object {
|
||||
"comments": Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
@@ -35,15 +72,33 @@ exports[`when has more comments renders hasMore 1`] = `
|
||||
}
|
||||
disableLoadMore={false}
|
||||
hasMore={true}
|
||||
isClosed={false}
|
||||
me={null}
|
||||
onLoadMore={[Function]}
|
||||
user={null}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more comments when loading more disables load more button 1`] = `
|
||||
<Stream
|
||||
assetID="asset-id"
|
||||
asset={
|
||||
Object {
|
||||
"comments": Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
@@ -56,15 +111,33 @@ exports[`when has more comments when loading more disables load more button 1`]
|
||||
}
|
||||
disableLoadMore={true}
|
||||
hasMore={true}
|
||||
isClosed={false}
|
||||
me={null}
|
||||
onLoadMore={[Function]}
|
||||
user={null}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`when has more comments when loading more enable load more button after loading is done 1`] = `
|
||||
<Stream
|
||||
assetID="asset-id"
|
||||
asset={
|
||||
Object {
|
||||
"comments": Object {
|
||||
"edges": Array [
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-1",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"node": Object {
|
||||
"id": "comment-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"id": "asset-id",
|
||||
"isClosed": false,
|
||||
}
|
||||
}
|
||||
comments={
|
||||
Array [
|
||||
Object {
|
||||
@@ -77,8 +150,7 @@ exports[`when has more comments when loading more enable load more button after
|
||||
}
|
||||
disableLoadMore={false}
|
||||
hasMore={true}
|
||||
isClosed={false}
|
||||
me={null}
|
||||
onLoadMore={[Function]}
|
||||
user={null}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Environment, RecordSource } from "relay-runtime";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
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 initLocalState from "./initLocalState";
|
||||
@@ -19,14 +20,18 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
it("init local state", async () => {
|
||||
await initLocalState(environment, {
|
||||
localStorage: createInMemoryStorage(),
|
||||
} as any);
|
||||
const context: Partial<TalkContext> = {
|
||||
localStorage: createPromisifiedStorage(),
|
||||
};
|
||||
await initLocalState(environment, context as any);
|
||||
await timeout();
|
||||
expect(JSON.stringify(source.toJSON(), null, 2)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("set assetID from query", async () => {
|
||||
const context: Partial<TalkContext> = {
|
||||
localStorage: createPromisifiedStorage(),
|
||||
};
|
||||
const assetID = "asset-id";
|
||||
const previousLocation = location.toString();
|
||||
const previousState = window.history.state;
|
||||
@@ -35,14 +40,15 @@ it("set assetID from query", async () => {
|
||||
document.title,
|
||||
`http://localhost/?assetID=${assetID}`
|
||||
);
|
||||
await initLocalState(environment, {
|
||||
localStorage: createInMemoryStorage(),
|
||||
} as any);
|
||||
await initLocalState(environment, context as any);
|
||||
expect(source.get(LOCAL_ID)!.assetID).toBe(assetID);
|
||||
window.history.replaceState(previousState, document.title, previousLocation);
|
||||
});
|
||||
|
||||
it("set commentID from query", async () => {
|
||||
const context: Partial<TalkContext> = {
|
||||
localStorage: createPromisifiedStorage(),
|
||||
};
|
||||
const commentID = "comment-id";
|
||||
const previousLocation = location.toString();
|
||||
const previousState = window.history.state;
|
||||
@@ -51,18 +57,17 @@ it("set commentID from query", async () => {
|
||||
document.title,
|
||||
`http://localhost/?commentID=${commentID}`
|
||||
);
|
||||
await initLocalState(environment, {
|
||||
localStorage: createInMemoryStorage(),
|
||||
} as any);
|
||||
await initLocalState(environment, context as any);
|
||||
expect(source.get(LOCAL_ID)!.commentID).toBe(commentID);
|
||||
window.history.replaceState(previousState, document.title, previousLocation);
|
||||
});
|
||||
|
||||
it("set authToken from localStorage", async () => {
|
||||
const context: Partial<TalkContext> = {
|
||||
localStorage: createPromisifiedStorage(),
|
||||
};
|
||||
const authToken = "auth-token";
|
||||
const localStorage = createInMemoryStorage();
|
||||
localStorage.setItem("authToken", authToken);
|
||||
await initLocalState(environment, { localStorage } as any);
|
||||
context.localStorage!.setItem("authToken", authToken);
|
||||
await initLocalState(environment, context as any);
|
||||
expect(source.get(LOCAL_ID)!.authToken).toBe(authToken);
|
||||
localStorage.removeItem("authToken");
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function initLocalState(
|
||||
environment: Environment,
|
||||
{ localStorage }: TalkContext
|
||||
) {
|
||||
const authToken = await localStorage.getItem("authToken");
|
||||
const authToken = await localStorage!.getItem("authToken");
|
||||
|
||||
commitLocalUpdate(environment, s => {
|
||||
// TODO: (cvle) move local, auth token and network initialization to framework.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
import uuid from "uuid/v4";
|
||||
import { Environment, RelayMutationConfig } from "relay-runtime";
|
||||
|
||||
import { getMe } from "talk-framework/helpers";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutationContainer,
|
||||
@@ -22,13 +22,7 @@ const mutation = graphql`
|
||||
edge {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
author {
|
||||
id
|
||||
username
|
||||
}
|
||||
body
|
||||
createdAt
|
||||
...StreamContainer_comment @relay(mask: false)
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
@@ -38,7 +32,44 @@ const mutation = graphql`
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
function commit(environment: Environment, input: CreateCommentInput) {
|
||||
function getConfig(input: CreateCommentInput): RelayMutationConfig[] {
|
||||
if (!input.parentID) {
|
||||
return [
|
||||
{
|
||||
type: "RANGE_ADD",
|
||||
connectionInfo: [
|
||||
{
|
||||
key: "Stream_comments",
|
||||
rangeBehavior: "prepend",
|
||||
filters: { orderBy: "CREATED_AT_DESC" },
|
||||
},
|
||||
],
|
||||
parentID: input.assetID,
|
||||
edgeName: "edge",
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: "RANGE_ADD",
|
||||
connectionInfo: [
|
||||
{
|
||||
key: "ReplyList_replies",
|
||||
rangeBehavior: "append",
|
||||
filters: { orderBy: "CREATED_AT_ASC" },
|
||||
},
|
||||
],
|
||||
parentID: input.parentID,
|
||||
edgeName: "edge",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function commit(
|
||||
environment: Environment,
|
||||
input: CreateCommentInput,
|
||||
{ uuidGenerator }: TalkContext
|
||||
) {
|
||||
const me = getMe(environment)!;
|
||||
const currentDate = new Date().toISOString();
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
@@ -54,7 +85,7 @@ function commit(environment: Environment, input: CreateCommentInput) {
|
||||
edge: {
|
||||
cursor: currentDate,
|
||||
node: {
|
||||
id: uuid(),
|
||||
id: uuidGenerator(),
|
||||
createdAt: currentDate,
|
||||
author: {
|
||||
id: me.id,
|
||||
@@ -65,21 +96,8 @@ function commit(environment: Environment, input: CreateCommentInput) {
|
||||
},
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
configs: [
|
||||
{
|
||||
type: "RANGE_ADD",
|
||||
connectionInfo: [
|
||||
{
|
||||
key: "Stream_comments",
|
||||
rangeBehavior: "prepend",
|
||||
filters: { orderBy: "CREATED_AT_DESC" },
|
||||
},
|
||||
],
|
||||
parentID: input.assetID,
|
||||
edgeName: "edge",
|
||||
},
|
||||
],
|
||||
} as any, // TODO: (cvle) generated types should contain one for the optimistic response.
|
||||
configs: getConfig(input),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { render } from "./PermalinkViewQuery";
|
||||
it("renders permalink view container", () => {
|
||||
const data = {
|
||||
props: {
|
||||
asset: {},
|
||||
comment: {},
|
||||
} as any,
|
||||
error: null,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import * as React from "react";
|
||||
import { StatelessComponent } from "react";
|
||||
import { ReadyState } from "react-relay";
|
||||
@@ -25,24 +26,52 @@ export const render = ({
|
||||
return <div>{error.message}</div>;
|
||||
}
|
||||
if (props) {
|
||||
return <PermalinkViewContainer comment={props.comment} />;
|
||||
if (!props.asset) {
|
||||
return (
|
||||
<Localized id="comments-permalinkViewQuery-assetNotFound">
|
||||
<div>Asset not found</div>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PermalinkViewContainer
|
||||
me={props.me}
|
||||
comment={props.comment}
|
||||
asset={props.asset}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Spinner />;
|
||||
};
|
||||
|
||||
const PermalinkViewQuery: StatelessComponent<InnerProps> = ({
|
||||
local: { commentID },
|
||||
local: { commentID, assetID, authRevision },
|
||||
}) => (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query PermalinkViewQuery($commentID: ID!) {
|
||||
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) {
|
||||
...PermalinkViewContainer_me
|
||||
}
|
||||
asset(id: $assetID) {
|
||||
...PermalinkViewContainer_asset
|
||||
}
|
||||
comment(id: $commentID) {
|
||||
...PermalinkViewContainer_comment
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
assetID: assetID!,
|
||||
commentID: commentID!,
|
||||
authRevision,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
@@ -51,6 +80,8 @@ const PermalinkViewQuery: StatelessComponent<InnerProps> = ({
|
||||
const enhanced = withLocalStateContainer(
|
||||
graphql`
|
||||
fragment PermalinkViewQueryLocal on Local {
|
||||
assetID
|
||||
authRevision
|
||||
commentID
|
||||
}
|
||||
`
|
||||
|
||||
@@ -31,7 +31,7 @@ export const render = ({
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
return <StreamContainer asset={props.asset} user={props.me} />;
|
||||
return <StreamContainer me={props.me} asset={props.asset} />;
|
||||
}
|
||||
|
||||
return <Spinner />;
|
||||
@@ -43,14 +43,14 @@ const StreamQuery: StatelessComponent<InnerProps> = ({
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query StreamQuery($assetID: ID!, $authRevision: Int!) {
|
||||
asset(id: $assetID) {
|
||||
...StreamContainer_asset
|
||||
}
|
||||
# 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) {
|
||||
...StreamContainer_user
|
||||
...StreamContainer_me
|
||||
}
|
||||
asset(id: $assetID) {
|
||||
...StreamContainer_asset
|
||||
}
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -10,6 +10,7 @@ exports[`renders loading 1`] = `<withPropsOnChange(Spinner) />`;
|
||||
|
||||
exports[`renders permalink view container 1`] = `
|
||||
<withContext(withContext(createMutationContainer(Relay(PermalinkViewContainer))))
|
||||
asset={Object {}}
|
||||
comment={Object {}}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.root {
|
||||
composes: bodyCopy from "talk-ui/shared/typography.css";
|
||||
overflow-wrap: break-word;
|
||||
|
||||
& * bold,
|
||||
& * strong {
|
||||
|
||||
@@ -62,7 +62,7 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_bold
|
||||
</span>
|
||||
@@ -76,7 +76,7 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_italic
|
||||
</span>
|
||||
@@ -90,7 +90,7 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_quote
|
||||
</span>
|
||||
@@ -153,7 +153,7 @@ exports[`loads more comments 1`] = `
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
@@ -177,8 +177,25 @@ exports[`loads more comments 1`] = `
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
className="Flex-root Comment-footer Flex-flex Flex-halfItemGutter Flex-directionRow"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
id="comments-commentContainer-replyButton-comment-0"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -189,7 +206,7 @@ exports[`loads more comments 1`] = `
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
@@ -213,8 +230,25 @@ exports[`loads more comments 1`] = `
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
className="Flex-root Comment-footer Flex-flex Flex-halfItemGutter Flex-directionRow"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
id="comments-commentContainer-replyButton-comment-1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -225,7 +259,7 @@ exports[`loads more comments 1`] = `
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
@@ -249,8 +283,25 @@ exports[`loads more comments 1`] = `
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
className="Flex-root Comment-footer Flex-flex Flex-halfItemGutter Flex-directionRow"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
id="comments-commentContainer-replyButton-comment-2"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,7 +371,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_bold
|
||||
</span>
|
||||
@@ -334,7 +385,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_italic
|
||||
</span>
|
||||
@@ -348,7 +399,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_quote
|
||||
</span>
|
||||
@@ -411,7 +462,7 @@ exports[`renders comment stream 1`] = `
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
@@ -435,8 +486,25 @@ exports[`renders comment stream 1`] = `
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
className="Flex-root Comment-footer Flex-flex Flex-halfItemGutter Flex-directionRow"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
id="comments-commentContainer-replyButton-comment-0"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -447,7 +515,7 @@ exports[`renders comment stream 1`] = `
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
@@ -471,8 +539,25 @@ exports[`renders comment stream 1`] = `
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
className="Flex-root Comment-footer Flex-flex Flex-halfItemGutter Flex-directionRow"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
id="comments-commentContainer-replyButton-comment-1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -28,7 +28,7 @@ exports[`renders permalink view 1`] = `
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
@@ -52,8 +52,25 @@ exports[`renders permalink view 1`] = `
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
className="Flex-root Comment-footer Flex-flex Flex-halfItemGutter Flex-directionRow"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
id="comments-commentContainer-replyButton-comment-0"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,7 +138,7 @@ exports[`show all comments 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_bold
|
||||
</span>
|
||||
@@ -135,7 +152,7 @@ exports[`show all comments 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_italic
|
||||
</span>
|
||||
@@ -149,7 +166,7 @@ exports[`show all comments 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
format_quote
|
||||
</span>
|
||||
@@ -212,7 +229,7 @@ exports[`show all comments 1`] = `
|
||||
role="article"
|
||||
>
|
||||
<div
|
||||
className="Flex-root TopBar-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
>
|
||||
<span
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
|
||||
@@ -236,8 +253,25 @@ exports[`show all comments 1`] = `
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="Comment-footer"
|
||||
/>
|
||||
className="Flex-root Comment-footer Flex-flex Flex-halfItemGutter Flex-directionRow"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
|
||||
id="comments-commentContainer-replyButton-comment-0"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Reply
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user