mirror of
https://github.com/wassname/talk.git
synced 2026-07-03 08:19:42 +08:00
Merge branch 'next' into ui-message
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