mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 21:40:11 +08:00
Merge branch 'next' into update-types-recompose
This commit is contained in:
@@ -10,6 +10,8 @@ module.exports = {
|
||||
"<rootDir>/src/core/build/polyfills.js",
|
||||
"<rootDir>/src/core/client/test/setup.ts",
|
||||
],
|
||||
setupTestFrameworkScriptFile:
|
||||
"<rootDir>/src/core/client/test/setupTestFramework.ts",
|
||||
testMatch: ["**/*.spec.{js,jsx,mjs,ts,tsx}"],
|
||||
testEnvironment: "node",
|
||||
testURL: "http://localhost",
|
||||
|
||||
Generated
+11
@@ -14229,6 +14229,12 @@
|
||||
"integrity": "sha1-rRxg8p6HGdR8JuETgJi20YsmETQ=",
|
||||
"dev": true
|
||||
},
|
||||
"jest-mock-console": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-mock-console/-/jest-mock-console-0.4.0.tgz",
|
||||
"integrity": "sha512-WElCbNvfqQlD7cpfHfTn1ytZ+RjKg1Ftrvr5wEjdWP7a9esXmaiZuEAPeYUSK5fd0Cra+dR1oF8HAjjKKxDQdg==",
|
||||
"dev": true
|
||||
},
|
||||
"jest-regex-util": {
|
||||
"version": "23.3.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-23.3.0.tgz",
|
||||
@@ -18045,6 +18051,11 @@
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
},
|
||||
"permit": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/permit/-/permit-0.2.4.tgz",
|
||||
"integrity": "sha512-Mp2XTEMD3mPsZIWq3bp0claE4IxXKa4C6nhSDPZgGri8Q4CLjEjAQrP/xGKq2548a2KFENmA1V7W0Lob8kTuzw=="
|
||||
},
|
||||
"pify": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"passport-oauth2": "^1.4.0",
|
||||
"passport-strategy": "^1.0.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"permit": "^0.2.4",
|
||||
"subscriptions-transport-ws": "^0.9.12",
|
||||
"tlds": "^1.203.1",
|
||||
"uuid": "^3.3.2"
|
||||
@@ -185,6 +186,7 @@
|
||||
"jest": "^23.4.1",
|
||||
"jest-junit": "^5.1.0",
|
||||
"jest-localstorage-mock": "^2.2.0",
|
||||
"jest-mock-console": "^0.4.0",
|
||||
"jsdom": "^11.11.0",
|
||||
"loader-utils": "^1.1.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
|
||||
@@ -448,25 +448,41 @@ export default function createWebpackConfig({
|
||||
].filter(s => s),
|
||||
output: {
|
||||
...baseConfig.output,
|
||||
library: "Talk",
|
||||
library: "Coral",
|
||||
// don't hash the embed, cache-busting must be completed by the requester
|
||||
// as this lives in a static template on the embed site.
|
||||
filename: "assets/js/embed.js",
|
||||
},
|
||||
plugins: [
|
||||
...baseConfig.plugins!,
|
||||
// Generates an `stream.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "embed.html",
|
||||
template: paths.appEmbedHTML,
|
||||
inject: "head",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In development, this will be an empty string.
|
||||
new InterpolateHtmlPlugin(env),
|
||||
...(isProduction
|
||||
? []
|
||||
: [
|
||||
// Generates an `embed.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "embed.html",
|
||||
template: paths.appEmbedHTML,
|
||||
inject: "head",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "article.html",
|
||||
template: paths.appEmbedArticleHTML,
|
||||
inject: "head",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "articleButton.html",
|
||||
template: paths.appEmbedArticleButtonHTML,
|
||||
inject: "head",
|
||||
...htmlWebpackConfig,
|
||||
}),
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In development, this will be an empty string.
|
||||
new InterpolateHtmlPlugin(env),
|
||||
]),
|
||||
// Generate a manifest file which contains a mapping of all asset filenames
|
||||
// to their corresponding output file so that tools can pick it up without
|
||||
// having to parse `index.html`.
|
||||
|
||||
@@ -31,6 +31,8 @@ export default {
|
||||
|
||||
appEmbedIndex: resolveSrc("core/client/embed/index.ts"),
|
||||
appEmbedHTML: resolveSrc("core/client/embed/index.html"),
|
||||
appEmbedArticleHTML: resolveSrc("core/client/embed/article.html"),
|
||||
appEmbedArticleButtonHTML: resolveSrc("core/client/embed/articleButton.html"),
|
||||
|
||||
appDistStatic: resolveApp("dist/static"),
|
||||
appPublic: resolveApp("public"),
|
||||
|
||||
@@ -2,13 +2,18 @@ import pym from "pym.js";
|
||||
|
||||
import { CleanupCallback, Decorator } from "./decorators";
|
||||
|
||||
interface PymControlConfig {
|
||||
export interface PymControlConfig {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
decorators?: ReadonlyArray<Decorator>;
|
||||
}
|
||||
|
||||
export type PymControlFactory = (config: PymControlConfig) => PymControl;
|
||||
|
||||
export const defaultPymControlFactory: PymControlFactory = config =>
|
||||
new PymControl(config);
|
||||
|
||||
export default class PymControl {
|
||||
private pym: pym.Parent;
|
||||
private cleanups: CleanupCallback[];
|
||||
@@ -20,6 +25,7 @@ export default class PymControl {
|
||||
title: config.title,
|
||||
id: `${config.id}_iframe`,
|
||||
name: `${config.id}_iframe`,
|
||||
optionalparams: "",
|
||||
});
|
||||
|
||||
this.cleanups = decorators
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import sinon from "sinon";
|
||||
|
||||
import { createStreamInterface } from "./Stream";
|
||||
|
||||
it("should call eventEmitter.on", () => {
|
||||
const control = {};
|
||||
const cb = () => "";
|
||||
const eventEmitter = {
|
||||
on: sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withArgs("eventName", cb),
|
||||
};
|
||||
const stream = createStreamInterface(control as any, eventEmitter as any);
|
||||
stream.on("eventName", cb);
|
||||
eventEmitter.on.verify();
|
||||
});
|
||||
|
||||
it("should call eventEmitter.off", () => {
|
||||
const control = {};
|
||||
const cb = () => "";
|
||||
const eventEmitter = {
|
||||
off: sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withArgs("eventName", cb),
|
||||
};
|
||||
const stream = createStreamInterface(control as any, eventEmitter as any);
|
||||
stream.off("eventName", cb);
|
||||
eventEmitter.off.verify();
|
||||
});
|
||||
|
||||
it("should call control.login", () => {
|
||||
const control = {
|
||||
sendMessage: sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withArgs("login", "token"),
|
||||
};
|
||||
const eventEmitter = {};
|
||||
const stream = createStreamInterface(control as any, eventEmitter as any);
|
||||
stream.login("token");
|
||||
control.sendMessage.verify();
|
||||
});
|
||||
|
||||
it("should call control.logout", () => {
|
||||
const control = {
|
||||
sendMessage: sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withArgs("logout"),
|
||||
};
|
||||
const eventEmitter = {};
|
||||
const stream = createStreamInterface(control as any, eventEmitter as any);
|
||||
stream.logout();
|
||||
control.sendMessage.verify();
|
||||
});
|
||||
|
||||
it("should call control.remove", () => {
|
||||
const control = {
|
||||
remove: sinon
|
||||
.mock()
|
||||
.once()
|
||||
.withArgs(),
|
||||
};
|
||||
const eventEmitter = {};
|
||||
const stream = createStreamInterface(control as any, eventEmitter as any);
|
||||
stream.remove();
|
||||
control.remove.verify();
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import qs from "query-string";
|
||||
|
||||
import {
|
||||
Decorator,
|
||||
withAutoHeight,
|
||||
withClickEvent,
|
||||
withEventEmitter,
|
||||
withIOSSafariWidthWorkaround,
|
||||
withPymStorage,
|
||||
withSetCommentID,
|
||||
} from "./decorators";
|
||||
import PymControl from "./PymControl";
|
||||
import { ensureEndSlash } from "./utils";
|
||||
|
||||
interface CreatePymControlConfig {
|
||||
assetID?: string;
|
||||
assetURL?: string;
|
||||
commentID?: string;
|
||||
title?: string;
|
||||
eventEmitter: EventEmitter2;
|
||||
id: string;
|
||||
rootURL: string;
|
||||
}
|
||||
|
||||
export function createPymControl(config: CreatePymControlConfig) {
|
||||
const streamDecorators: ReadonlyArray<Decorator> = [
|
||||
withIOSSafariWidthWorkaround,
|
||||
withAutoHeight,
|
||||
withClickEvent,
|
||||
withSetCommentID,
|
||||
withEventEmitter(config.eventEmitter),
|
||||
withPymStorage(localStorage, "localStorage"),
|
||||
withPymStorage(sessionStorage, "sessionStorage"),
|
||||
];
|
||||
|
||||
const query = qs.stringify({
|
||||
assetID: config.assetID,
|
||||
assetURL: config.assetURL,
|
||||
commentID: config.commentID,
|
||||
});
|
||||
const url = `${ensureEndSlash(config.rootURL)}stream.html?${query}`;
|
||||
return new PymControl({
|
||||
id: config.id,
|
||||
title: config.title || "Talk Embed Stream",
|
||||
decorators: streamDecorators,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
type EventCallback = (data: any) => void;
|
||||
|
||||
export function createStreamInterface(
|
||||
control: PymControl,
|
||||
eventEmitter: EventEmitter2
|
||||
) {
|
||||
return {
|
||||
on(eventName: string, callback: EventCallback) {
|
||||
return eventEmitter.on(eventName, callback);
|
||||
},
|
||||
off(eventName: string, callback: EventCallback) {
|
||||
return eventEmitter.off(eventName, callback);
|
||||
},
|
||||
login(token: string) {
|
||||
control.sendMessage("login", token);
|
||||
},
|
||||
logout() {
|
||||
control.sendMessage("logout");
|
||||
},
|
||||
remove() {
|
||||
return control.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type StreamInterface = ReturnType<typeof createStreamInterface>;
|
||||
|
||||
export interface CreateConfig {
|
||||
assetID?: string;
|
||||
assetURL?: string;
|
||||
commentID?: string;
|
||||
title?: string;
|
||||
eventEmitter: EventEmitter2;
|
||||
id: string;
|
||||
rootURL: string;
|
||||
}
|
||||
export default function create(config: CreateConfig) {
|
||||
return createStreamInterface(createPymControl(config), config.eventEmitter);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import { omit } from "lodash";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { PymControlConfig } from "./PymControl";
|
||||
import { StreamEmbed, StreamEmbedConfig } from "./StreamEmbed";
|
||||
|
||||
it("should throw when calling pym dependent methods but was not rendered", () => {
|
||||
const config: StreamEmbedConfig = {
|
||||
title: "StreamEmbed",
|
||||
eventEmitter: new EventEmitter2(),
|
||||
id: "container-id",
|
||||
rootURL: "http://localhost/",
|
||||
};
|
||||
const streamEmbed = new StreamEmbed(config);
|
||||
[
|
||||
() => streamEmbed.login("token"),
|
||||
() => streamEmbed.logout(),
|
||||
() => streamEmbed.remove(),
|
||||
].forEach(cb => {
|
||||
expect(cb).toThrow();
|
||||
});
|
||||
});
|
||||
it("should return rendered", () => {
|
||||
const config: StreamEmbedConfig = {
|
||||
title: "StreamEmbed",
|
||||
eventEmitter: new EventEmitter2(),
|
||||
id: "container-id",
|
||||
rootURL: "http://localhost/",
|
||||
};
|
||||
const pymControl = {
|
||||
remove: sinon.stub(),
|
||||
};
|
||||
const fakeFactory: any = () => pymControl;
|
||||
const streamEmbed = new StreamEmbed(config, fakeFactory);
|
||||
expect(streamEmbed.rendered).toBe(false);
|
||||
streamEmbed.render();
|
||||
expect(streamEmbed.rendered).toBe(true);
|
||||
streamEmbed.remove();
|
||||
expect(streamEmbed.rendered).toBe(false);
|
||||
expect(pymControl.remove.called).toBe(true);
|
||||
});
|
||||
|
||||
it("should relay events methods to event emitter", () => {
|
||||
const config: StreamEmbedConfig = {
|
||||
title: "StreamEmbed",
|
||||
eventEmitter: new EventEmitter2(),
|
||||
id: "container-id",
|
||||
rootURL: "http://localhost/",
|
||||
};
|
||||
// tslint:disable-next-line:no-empty
|
||||
const callback = () => {};
|
||||
const fakeFactory: any = () => ({});
|
||||
const emitterMock = sinon.mock(config.eventEmitter);
|
||||
emitterMock
|
||||
.expects("on")
|
||||
.withArgs("event", callback)
|
||||
.once();
|
||||
emitterMock
|
||||
.expects("off")
|
||||
.withArgs("event", callback)
|
||||
.once();
|
||||
const streamEmbed = new StreamEmbed(config, fakeFactory);
|
||||
streamEmbed.on("event", callback);
|
||||
streamEmbed.off("event", callback);
|
||||
});
|
||||
|
||||
it("should send login message to PymControl", () => {
|
||||
const config: StreamEmbedConfig = {
|
||||
title: "StreamEmbed",
|
||||
eventEmitter: new EventEmitter2(),
|
||||
id: "container-id",
|
||||
rootURL: "http://localhost/",
|
||||
};
|
||||
const pymControl = {
|
||||
// tslint:disable-next-line:no-empty
|
||||
sendMessage: () => {},
|
||||
};
|
||||
const pymControlMock = sinon.mock(pymControl);
|
||||
pymControlMock.expects("sendMessage").withArgs("login", "token");
|
||||
const fakeFactory: any = () => pymControl;
|
||||
const streamEmbed = new StreamEmbed(config, fakeFactory);
|
||||
streamEmbed.render();
|
||||
streamEmbed.login("token");
|
||||
pymControlMock.verify();
|
||||
});
|
||||
|
||||
it("should send logout message to PymControl", () => {
|
||||
const config: StreamEmbedConfig = {
|
||||
title: "StreamEmbed",
|
||||
eventEmitter: new EventEmitter2(),
|
||||
id: "container-id",
|
||||
rootURL: "http://localhost/",
|
||||
};
|
||||
const pymControl = {
|
||||
// tslint:disable-next-line:no-empty
|
||||
sendMessage: () => {},
|
||||
};
|
||||
const pymControlMock = sinon.mock(pymControl);
|
||||
pymControlMock.expects("sendMessage").withArgs("logout");
|
||||
const fakeFactory: any = () => pymControl;
|
||||
const streamEmbed = new StreamEmbed(config, fakeFactory);
|
||||
streamEmbed.render();
|
||||
streamEmbed.logout();
|
||||
pymControlMock.verify();
|
||||
});
|
||||
|
||||
it("should pass default values to pymControl", () => {
|
||||
const config: StreamEmbedConfig = {
|
||||
title: "StreamEmbed",
|
||||
eventEmitter: new EventEmitter2(),
|
||||
id: "container-id",
|
||||
rootURL: "http://localhost/",
|
||||
};
|
||||
let pymControlConfig: PymControlConfig | null = null;
|
||||
const fakeFactory: any = (cfg: PymControlConfig) => {
|
||||
pymControlConfig = cfg;
|
||||
return {};
|
||||
};
|
||||
const streamEmbed = new StreamEmbed(config, fakeFactory);
|
||||
streamEmbed.render();
|
||||
expect(omit(pymControlConfig, "decorators")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should pass correct values to pymControl", () => {
|
||||
const config: StreamEmbedConfig = {
|
||||
title: "StreamEmbed",
|
||||
eventEmitter: new EventEmitter2(),
|
||||
id: "container-id",
|
||||
rootURL: "http://localhost/",
|
||||
commentID: "comment-id",
|
||||
assetID: "asset-id",
|
||||
assetURL: "asset-url",
|
||||
};
|
||||
let pymControlConfig: PymControlConfig | null = null;
|
||||
const fakeFactory: any = (cfg: PymControlConfig) => {
|
||||
pymControlConfig = cfg;
|
||||
return {};
|
||||
};
|
||||
const streamEmbed = new StreamEmbed(config, fakeFactory);
|
||||
streamEmbed.render();
|
||||
expect(omit(pymControlConfig, "decorators")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should emit showPermalink", () => {
|
||||
jest.useFakeTimers();
|
||||
const config: StreamEmbedConfig = {
|
||||
title: "StreamEmbed",
|
||||
eventEmitter: new EventEmitter2(),
|
||||
id: "container-id",
|
||||
rootURL: "http://localhost/",
|
||||
commentID: "comment-id",
|
||||
};
|
||||
// tslint:disable-next-line:no-empty
|
||||
const fakeFactory: any = () => ({});
|
||||
const emitterMock = sinon.mock(config.eventEmitter);
|
||||
emitterMock
|
||||
.expects("emit")
|
||||
.withArgs("showPermalink")
|
||||
.once();
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
new StreamEmbed(config, fakeFactory);
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
emitterMock.verify();
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import qs from "query-string";
|
||||
|
||||
import {
|
||||
Decorator,
|
||||
withAutoHeight,
|
||||
withClickEvent,
|
||||
withEventEmitter,
|
||||
withIOSSafariWidthWorkaround,
|
||||
withPymStorage,
|
||||
withSetCommentID,
|
||||
} from "./decorators";
|
||||
import onIntersect from "./onIntersect";
|
||||
import PymControl, {
|
||||
defaultPymControlFactory,
|
||||
PymControlFactory,
|
||||
} from "./PymControl";
|
||||
import { ensureEndSlash } from "./utils";
|
||||
|
||||
export interface StreamEmbedConfig {
|
||||
assetID?: string;
|
||||
assetURL?: string;
|
||||
commentID?: string;
|
||||
autoRender?: boolean;
|
||||
title: string;
|
||||
eventEmitter: EventEmitter2;
|
||||
id: string;
|
||||
rootURL: string;
|
||||
}
|
||||
|
||||
export class StreamEmbed {
|
||||
private config: StreamEmbedConfig;
|
||||
private pymControl?: PymControl;
|
||||
private pymControlFactory: PymControlFactory;
|
||||
|
||||
constructor(
|
||||
config: StreamEmbedConfig,
|
||||
pymControlFactory = defaultPymControlFactory
|
||||
) {
|
||||
this.config = config;
|
||||
this.pymControlFactory = pymControlFactory;
|
||||
if (config.commentID) {
|
||||
// Delay emit of `showPermalink` event to allow
|
||||
// user enough time to setup event listeners.
|
||||
setTimeout(() => config.eventEmitter.emit("showPermalink"), 0);
|
||||
}
|
||||
if (config.autoRender) {
|
||||
if (config.commentID) {
|
||||
this.render();
|
||||
} else {
|
||||
onIntersect(document.getElementById(config.id)!, () => {
|
||||
if (!this.rendered) {
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private assertRendered() {
|
||||
if (!this.pymControl) {
|
||||
throw new Error("Stream Embed must be rendered first");
|
||||
}
|
||||
}
|
||||
|
||||
public on(eventName: string, callback: (data: any) => void) {
|
||||
return this.config.eventEmitter.on(eventName, callback);
|
||||
}
|
||||
|
||||
public off(eventName: string, callback: (data: any) => void) {
|
||||
return this.config.eventEmitter.off(eventName, callback);
|
||||
}
|
||||
|
||||
public login(token: string) {
|
||||
this.assertRendered();
|
||||
this.pymControl!.sendMessage("login", token);
|
||||
}
|
||||
|
||||
public logout() {
|
||||
this.assertRendered();
|
||||
this.pymControl!.sendMessage("logout");
|
||||
}
|
||||
|
||||
public remove() {
|
||||
this.assertRendered();
|
||||
this.pymControl!.remove();
|
||||
this.pymControl = undefined;
|
||||
}
|
||||
|
||||
get rendered() {
|
||||
return !!this.pymControl;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.pymControl) {
|
||||
throw new Error("Stream Embed already rendered");
|
||||
}
|
||||
const streamDecorators: ReadonlyArray<Decorator> = [
|
||||
withIOSSafariWidthWorkaround,
|
||||
withAutoHeight,
|
||||
withClickEvent,
|
||||
withSetCommentID,
|
||||
withEventEmitter(this.config.eventEmitter),
|
||||
withPymStorage(localStorage, "localStorage"),
|
||||
withPymStorage(sessionStorage, "sessionStorage"),
|
||||
];
|
||||
|
||||
const query = qs.stringify({
|
||||
assetID: this.config.assetID,
|
||||
assetURL: this.config.assetURL,
|
||||
commentID: this.config.commentID,
|
||||
});
|
||||
const url = `${ensureEndSlash(this.config.rootURL)}stream.html${
|
||||
query ? `?${query}` : ""
|
||||
}`;
|
||||
this.pymControl = this.pymControlFactory({
|
||||
id: this.config.id,
|
||||
title: this.config.title,
|
||||
decorators: streamDecorators,
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function createStreamEmbed(config: StreamEmbedConfig) {
|
||||
return new StreamEmbed(config);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import qs from "query-string";
|
||||
|
||||
import { default as create, StreamEmbed } from "./StreamEmbed";
|
||||
|
||||
export interface Config {
|
||||
assetID?: string;
|
||||
assetURL?: string;
|
||||
commentID?: string;
|
||||
rootURL?: string;
|
||||
id?: string;
|
||||
autoRender?: boolean;
|
||||
events?: (eventEmitter: EventEmitter2) => void;
|
||||
}
|
||||
|
||||
function getLocationOrigin() {
|
||||
return (
|
||||
location.origin ||
|
||||
`${window.location.protocol}//${window.location.hostname}${
|
||||
window.location.port ? `:${window.location.port}` : ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAssetURL() {
|
||||
const canonical = document.querySelector(
|
||||
'link[rel="canonical"]'
|
||||
) as HTMLLinkElement;
|
||||
if (canonical) {
|
||||
return canonical.href;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(
|
||||
"This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages."
|
||||
);
|
||||
|
||||
return getLocationOrigin() + window.location.pathname;
|
||||
}
|
||||
|
||||
export function createStreamEmbed(config: Config): StreamEmbed {
|
||||
// Parse query params
|
||||
const query = qs.parse(location.search);
|
||||
const eventEmitter = new EventEmitter2({ wildcard: true });
|
||||
|
||||
if (config.events) {
|
||||
config.events(eventEmitter);
|
||||
}
|
||||
|
||||
return create({
|
||||
title: "Talk Embed Stream",
|
||||
assetID: config.assetID || query.assetID,
|
||||
assetURL: config.assetURL || resolveAssetURL(),
|
||||
commentID: config.commentID || query.commentID,
|
||||
id: config.id || "talk-embed-stream",
|
||||
rootURL: config.rootURL || getLocationOrigin(),
|
||||
autoRender: config.autoRender,
|
||||
eventEmitter,
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PymControl should create iframe 1`] = `"<iframe src=\\"http://coralproject.net/?initialWidth=0&childId=pymcontrol-test-id&parentTitle=&parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"iFrame title\\" id=\\"pymcontrol-test-id_iframe\\" name=\\"pymcontrol-test-id_iframe\\"></iframe>"`;
|
||||
exports[`PymControl should create iframe 1`] = `"<iframe src=\\"http://coralproject.net/?initialWidth=0&childId=pymcontrol-test-id\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"iFrame title\\" id=\\"pymcontrol-test-id_iframe\\" name=\\"pymcontrol-test-id_iframe\\"></iframe>"`;
|
||||
|
||||
exports[`PymControl should send message 1`] = `"pymxPYMxpymcontrol-test-idxPYMxtestxPYMxhello world"`;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should pass correct values to pymControl 1`] = `
|
||||
Object {
|
||||
"id": "container-id",
|
||||
"title": "StreamEmbed",
|
||||
"url": "http://localhost/stream.html?assetID=asset-id&assetURL=asset-url&commentID=comment-id",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`should pass default values to pymControl 1`] = `
|
||||
Object {
|
||||
"id": "container-id",
|
||||
"title": "StreamEmbed",
|
||||
"url": "http://localhost/stream.html",
|
||||
}
|
||||
`;
|
||||
@@ -1,3 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Basic integration test should render iframe 1`] = `"<iframe src=\\"http://localhost/stream.html?&initialWidth=0&childId=basic-integration-test-id&parentTitle=&parentUrl=http%3A%2F%2Flocalhost%2F\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" id=\\"basic-integration-test-id_iframe\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
|
||||
exports[`Basic integration test should render iframe 1`] = `"<iframe src=\\"http://localhost/stream.html?assetURL=http%3A%2F%2Flocalhost%2F&initialWidth=0&childId=basic-integration-test-id\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" id=\\"basic-integration-test-id_iframe\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
|
||||
|
||||
exports[`Basic integration test should use canonical link 1`] = `"<iframe src=\\"http://localhost/stream.html?assetURL=http%3A%2F%2Flocalhost%2Fcanonical&initialWidth=0&childId=basic-integration-test-id\\" width=\\"100%\\" scrolling=\\"no\\" marginheight=\\"0\\" frameborder=\\"0\\" title=\\"Talk Embed Stream\\" name=\\"basic-integration-test-id_iframe\\" style=\\"width: 1px; min-width: 100%;\\"></iframe>"`;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Talk 5.0 – Embed Stream</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0 100px 50px 100px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p style="text-align: center">
|
||||
<a href="/">Default</a> | <a href="/articleButton.html">Article With Button</a>
|
||||
</p>
|
||||
<h1 style="text-align: center">Talk 5.0 – Article</h1>
|
||||
<p>Dismember a mouse and then regurgitate parts of it on the family room floor. Dont wait for the storm to pass,
|
||||
dance in the rain stand in front of the computer screen, so stares at human while pushing stuff off a table chew
|
||||
the plant meow hiss at vacuum cleaner. Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not
|
||||
sorry chew the plant. Litter kitter kitty litty little kitten big roar roar feed me rub whiskers on bare skin act
|
||||
innocent sleep on keyboard, so give me attention or face the wrath of my claws for demand to be let outside at
|
||||
once, and expect owner to wait for me as i think about it spread kitty litter all over house so nya nya nyan. Catty
|
||||
ipsum massacre a bird in the living room and then look like the cutest and most innocent animal on the planet you
|
||||
have cat to be kitten me right meow. Hiss and stare at nothing then run suddenly away refuse to come home when
|
||||
humans are going to bed; stay out all night then yowl like i am dying at 4am and lick plastic bags. Chase dog then
|
||||
run away purrr purr littel cat, little cat purr purr and step on your keyboard while you're gaming and then turn in
|
||||
a circle . Twitch tail in permanent irritation put butt in owner's face and the dog smells bad yet attempt to leap
|
||||
between furniture but woefully miscalibrate and bellyflop onto the floor; what's your problem? i meant to do that
|
||||
now i shall wash myself intently. Sniff all the things groom forever, stretch tongue and leave it slightly out,
|
||||
blep, but bring your owner a dead bird decide to want nothing to do with my owner today for lay on arms while
|
||||
you're using the keyboard meow meow, i tell my human or scratch. Sleep on my human's head then cats take over the
|
||||
world bleghbleghvomit my furball really tie the room together sleep more napping, more napping all the napping is
|
||||
exhausting. When in doubt, wash drink water out of the faucet, cats are fats i like to pets them they like to meow
|
||||
back and cat dog hate mouse eat string barf pillow no baths hate everything yet swat at dog kitty kitty but you
|
||||
call this cat food. Cough furball into food bowl then scratch owner for a new one flex claws on the human's belly
|
||||
and purr like a lawnmower for has closed eyes but still sees you groom yourself 4 hours - checked, have your beauty
|
||||
sleep 18 hours - checked, be fabulous for the rest of the day - checked. Freak human out make funny noise mow mow
|
||||
mow mow mow mow success now attack human flex claws on the human's belly and purr like a lawnmower or meowwww.
|
||||
Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not sorry paw at your fat belly so yowling
|
||||
nonstop the whole night small kitty warm kitty little balls of fur or eat owner's food reward the chosen human with
|
||||
a slow blink. Gate keepers of hell plan steps for world domination for more napping, more napping all the napping
|
||||
is exhausting give me some of your food give me some of your food give me some of your food meh, i don't want it so
|
||||
flop over. Make meme, make cute face ears back wide eyed so sit and stare. Dead stare with ears cocked furrier and
|
||||
even more furrier hairball. Stand in front of the computer screen demand to have some of whatever the human is
|
||||
cooking, then sniff the offering and walk away for catasstrophe, kitty scratches couch bad kitty. Wack the mini
|
||||
furry mouse intrigued by the shower, and pooping rainbow while flying in a toasted bread costume in space.
|
||||
Mesmerizing birds love me! shake treat bag, yet lies down where is my slave? I'm getting hungry so lick face hiss
|
||||
at owner, pee a lot, and meow repeatedly scratch at fence purrrrrr eat muffins and poutine until owner comes back.
|
||||
You have cat to be kitten me right meow sniff other cat's butt and hang jaw half open thereafter but run outside as
|
||||
soon as door open so munch on tasty moths or munch on tasty moths, for paw at beetle and eat it before it gets
|
||||
away. Sit on human. Gnaw the corn cob massacre a bird in the living room and then look like the cutest and most
|
||||
innocent animal on the planet for sit on the laptop. Meow scratch leg; meow for can opener to feed me cat fur is
|
||||
the new black but hide when guests come over, and Gate keepers of hell. Refuse to come home when humans are going
|
||||
to bed; stay out all night then yowl like i am dying at 4am cat slap dog in face or eat a rug and furry furry hairs
|
||||
everywhere oh no human coming lie on counter don't get off counter for i like fish sit on human they not getting up
|
||||
ever but meow meow but cuddle no cuddle cuddle love scratch scratch.</p>
|
||||
<p>I show my fluffy belly but it's a trap! if you pet it i will tear up your hand refuse to drink water except out of
|
||||
someone's glass mice, so cough hairball, eat toilet paper or curl into a furry donut lick sellotape but wack the
|
||||
mini furry mouse. When owners are asleep, cry for no apparent reason. Chase imaginary bugs. Stinky cat reward the
|
||||
chosen human with a slow blink, or chase dog then run away. Chew on cable scratch the furniture for you are a
|
||||
captive audience while sitting on the toilet, pet me for i like cats because they are fat and fluffy and spend all
|
||||
night ensuring people don't sleep sleep all day. Scoot butt on the rug need to check on human, have not seen in an
|
||||
hour might be dead oh look, human is alive, hiss at human, feed me, leave fur on owners clothes, so instantly break
|
||||
out into full speed gallop across the house for no reason play riveting piece on synthesizer keyboard and scoot
|
||||
butt on the rug yet meow meow. Attack dog, run away and pretend to be victim annoy the old grumpy cat, start a
|
||||
fight and then retreat to wash when i lose or meow go back to sleep owner brings food and water tries to pet on
|
||||
head, so scratch get sprayed by water because bad cat. Meowwww pelt around the house and up and down stairs chasing
|
||||
phantoms drink water out of the faucet meow meow, i tell my human. Destroy couch.</p>
|
||||
<p>Ask to go outside and ask to come inside and ask to go outside and ask to come inside the dog smells bad. Lick
|
||||
butt and make a weird face. Toilet paper attack claws fluff everywhere meow miao french ciao litterbox. Shake treat
|
||||
bag immediately regret falling into bathtub or white cat sleeps on a black shirt so what a cat-ass-trophy! eat
|
||||
owner's food spit up on light gray carpet instead of adjacent linoleum. Warm up laptop with butt lick butt fart
|
||||
rainbows until owner yells pee in litter box hiss at cats scratch the box so loved it, hated it, loved it, hated it
|
||||
but need to check on human, have not seen in an hour might be dead oh look, human is alive, hiss at human, feed me.
|
||||
</p>
|
||||
<div id="coralStreamEmbed" style="max-width: 640px; margin: 0 auto"></div>
|
||||
<script>
|
||||
const TalkStreamEmbed = Coral.Talk.createStreamEmbed({ id: 'coralStreamEmbed', autoRender: true });
|
||||
window.TalkStreamEmbed = TalkStreamEmbed;
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Talk 5.0 – Embed Stream</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0 100px 50px 100px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p style="text-align: center">
|
||||
<a href="/">Default</a> | <a href="/article.html">Article</a>
|
||||
</p>
|
||||
<h1 style="text-align: center">Talk 5.0 – Article with Button</h1>
|
||||
<p>Dismember a mouse and then regurgitate parts of it on the family room floor. Dont wait for the storm to pass,
|
||||
dance in the rain stand in front of the computer screen, so stares at human while pushing stuff off a table chew
|
||||
the plant meow hiss at vacuum cleaner. Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not
|
||||
sorry chew the plant. Litter kitter kitty litty little kitten big roar roar feed me rub whiskers on bare skin act
|
||||
innocent sleep on keyboard, so give me attention or face the wrath of my claws for demand to be let outside at
|
||||
once, and expect owner to wait for me as i think about it spread kitty litter all over house so nya nya nyan. Catty
|
||||
ipsum massacre a bird in the living room and then look like the cutest and most innocent animal on the planet you
|
||||
have cat to be kitten me right meow. Hiss and stare at nothing then run suddenly away refuse to come home when
|
||||
humans are going to bed; stay out all night then yowl like i am dying at 4am and lick plastic bags. Chase dog then
|
||||
run away purrr purr littel cat, little cat purr purr and step on your keyboard while you're gaming and then turn in
|
||||
a circle . Twitch tail in permanent irritation put butt in owner's face and the dog smells bad yet attempt to leap
|
||||
between furniture but woefully miscalibrate and bellyflop onto the floor; what's your problem? i meant to do that
|
||||
now i shall wash myself intently. Sniff all the things groom forever, stretch tongue and leave it slightly out,
|
||||
blep, but bring your owner a dead bird decide to want nothing to do with my owner today for lay on arms while
|
||||
you're using the keyboard meow meow, i tell my human or scratch. Sleep on my human's head then cats take over the
|
||||
world bleghbleghvomit my furball really tie the room together sleep more napping, more napping all the napping is
|
||||
exhausting. When in doubt, wash drink water out of the faucet, cats are fats i like to pets them they like to meow
|
||||
back and cat dog hate mouse eat string barf pillow no baths hate everything yet swat at dog kitty kitty but you
|
||||
call this cat food. Cough furball into food bowl then scratch owner for a new one flex claws on the human's belly
|
||||
and purr like a lawnmower for has closed eyes but still sees you groom yourself 4 hours - checked, have your beauty
|
||||
sleep 18 hours - checked, be fabulous for the rest of the day - checked. Freak human out make funny noise mow mow
|
||||
mow mow mow mow success now attack human flex claws on the human's belly and purr like a lawnmower or meowwww.
|
||||
Terrorize the hundred-and-twenty-pound rottweiler and steal his bed, not sorry paw at your fat belly so yowling
|
||||
nonstop the whole night small kitty warm kitty little balls of fur or eat owner's food reward the chosen human with
|
||||
a slow blink. Gate keepers of hell plan steps for world domination for more napping, more napping all the napping
|
||||
is exhausting give me some of your food give me some of your food give me some of your food meh, i don't want it so
|
||||
flop over. Make meme, make cute face ears back wide eyed so sit and stare. Dead stare with ears cocked furrier and
|
||||
even more furrier hairball. Stand in front of the computer screen demand to have some of whatever the human is
|
||||
cooking, then sniff the offering and walk away for catasstrophe, kitty scratches couch bad kitty. Wack the mini
|
||||
furry mouse intrigued by the shower, and pooping rainbow while flying in a toasted bread costume in space.
|
||||
Mesmerizing birds love me! shake treat bag, yet lies down where is my slave? I'm getting hungry so lick face hiss
|
||||
at owner, pee a lot, and meow repeatedly scratch at fence purrrrrr eat muffins and poutine until owner comes back.
|
||||
You have cat to be kitten me right meow sniff other cat's butt and hang jaw half open thereafter but run outside as
|
||||
soon as door open so munch on tasty moths or munch on tasty moths, for paw at beetle and eat it before it gets
|
||||
away. Sit on human. Gnaw the corn cob massacre a bird in the living room and then look like the cutest and most
|
||||
innocent animal on the planet for sit on the laptop. Meow scratch leg; meow for can opener to feed me cat fur is
|
||||
the new black but hide when guests come over, and Gate keepers of hell. Refuse to come home when humans are going
|
||||
to bed; stay out all night then yowl like i am dying at 4am cat slap dog in face or eat a rug and furry furry hairs
|
||||
everywhere oh no human coming lie on counter don't get off counter for i like fish sit on human they not getting up
|
||||
ever but meow meow but cuddle no cuddle cuddle love scratch scratch.</p>
|
||||
<p>I show my fluffy belly but it's a trap! if you pet it i will tear up your hand refuse to drink water except out of
|
||||
someone's glass mice, so cough hairball, eat toilet paper or curl into a furry donut lick sellotape but wack the
|
||||
mini furry mouse. When owners are asleep, cry for no apparent reason. Chase imaginary bugs. Stinky cat reward the
|
||||
chosen human with a slow blink, or chase dog then run away. Chew on cable scratch the furniture for you are a
|
||||
captive audience while sitting on the toilet, pet me for i like cats because they are fat and fluffy and spend all
|
||||
night ensuring people don't sleep sleep all day. Scoot butt on the rug need to check on human, have not seen in an
|
||||
hour might be dead oh look, human is alive, hiss at human, feed me, leave fur on owners clothes, so instantly break
|
||||
out into full speed gallop across the house for no reason play riveting piece on synthesizer keyboard and scoot
|
||||
butt on the rug yet meow meow. Attack dog, run away and pretend to be victim annoy the old grumpy cat, start a
|
||||
fight and then retreat to wash when i lose or meow go back to sleep owner brings food and water tries to pet on
|
||||
head, so scratch get sprayed by water because bad cat. Meowwww pelt around the house and up and down stairs chasing
|
||||
phantoms drink water out of the faucet meow meow, i tell my human. Destroy couch.</p>
|
||||
<p>Ask to go outside and ask to come inside and ask to go outside and ask to come inside the dog smells bad. Lick
|
||||
butt and make a weird face. Toilet paper attack claws fluff everywhere meow miao french ciao litterbox. Shake treat
|
||||
bag immediately regret falling into bathtub or white cat sleeps on a black shirt so what a cat-ass-trophy! eat
|
||||
owner's food spit up on light gray carpet instead of adjacent linoleum. Warm up laptop with butt lick butt fart
|
||||
rainbows until owner yells pee in litter box hiss at cats scratch the box so loved it, hated it, loved it, hated it
|
||||
but need to check on human, have not seen in an hour might be dead oh look, human is alive, hiss at human, feed me.
|
||||
</p>
|
||||
<div id="coralStreamEmbed" style="max-width: 640px; margin: 0 auto"></div>
|
||||
<div style="text-align: center">
|
||||
<button id="showComments">Show Comments</button>
|
||||
</div>
|
||||
<script>
|
||||
const TalkStreamEmbed = Coral.Talk.createStreamEmbed({
|
||||
id: 'coralStreamEmbed',
|
||||
});
|
||||
window.TalkStreamEmbed = TalkStreamEmbed;
|
||||
|
||||
const button = document.getElementById("showComments");
|
||||
|
||||
const showStreamEmbed = () => {
|
||||
TalkStreamEmbed.render();
|
||||
button.parentElement.removeChild(button);
|
||||
};
|
||||
|
||||
button.onclick = showStreamEmbed;
|
||||
TalkStreamEmbed.on("showPermalink", showStreamEmbed);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -9,16 +9,21 @@
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 0 0 50px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1 style="text-align: center" }>Talk 5.0 – Embed Stream</h1>
|
||||
<p style="text-align: center">
|
||||
<a href="/article.html">Article</a> | <a href="/articleButton.html">Article With Button</a>
|
||||
</p>
|
||||
<h1 style="text-align: center">Talk 5.0 – Embed Stream</h1>
|
||||
<div id="coralStreamEmbed" style="max-width: 640px; margin: 0 auto"></div>
|
||||
<script>
|
||||
window.TalkEmbed = Talk.render(document.getElementById('coralStreamEmbed'));
|
||||
const TalkStreamEmbed = Coral.Talk.createStreamEmbed({ id: 'coralStreamEmbed' });
|
||||
window.TalkStreamEmbed = TalkStreamEmbed;
|
||||
TalkStreamEmbed.render();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as Talk from "./";
|
||||
import mockConsole from "jest-mock-console";
|
||||
import * as Coral from "./";
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
describe("Basic integration test", () => {
|
||||
const container: HTMLElement = document.createElement("div");
|
||||
let streamInterface: ReturnType<typeof Talk.render>;
|
||||
beforeAll(() => {
|
||||
container.id = "basic-integration-test-id";
|
||||
document.body.appendChild(container);
|
||||
@@ -11,13 +13,40 @@ describe("Basic integration test", () => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
it("should render iframe", () => {
|
||||
streamInterface = Talk.render({
|
||||
mockConsole();
|
||||
const TalkEmbedStream = Coral.Talk.createStreamEmbed({
|
||||
id: "basic-integration-test-id",
|
||||
});
|
||||
TalkEmbedStream.render();
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(console.error).not.toHaveBeenCalled();
|
||||
});
|
||||
it("should use canonical link", () => {
|
||||
mockConsole();
|
||||
const link = document.createElement("link");
|
||||
link.rel = "canonical";
|
||||
link.href = "http://localhost/canonical";
|
||||
document.head.appendChild(link);
|
||||
const TalkEmbedStream = Coral.Talk.createStreamEmbed({
|
||||
id: "basic-integration-test-id",
|
||||
});
|
||||
TalkEmbedStream.render();
|
||||
expect(container.innerHTML).toMatchSnapshot();
|
||||
document.head.removeChild(link);
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
expect(console.error).not.toHaveBeenCalled();
|
||||
});
|
||||
it("should remove iframe", () => {
|
||||
streamInterface.remove();
|
||||
mockConsole();
|
||||
const TalkEmbedStream = Coral.Talk.createStreamEmbed({
|
||||
id: "basic-integration-test-id",
|
||||
});
|
||||
TalkEmbedStream.render();
|
||||
TalkEmbedStream.remove();
|
||||
expect(container.innerHTML).toBe("");
|
||||
// tslint:disable-next-line:no-console
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(console.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,2 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
import qs from "query-string";
|
||||
|
||||
import createStreamInterface from "./Stream";
|
||||
|
||||
export interface Config {
|
||||
assetID?: string;
|
||||
assetURL?: string;
|
||||
commentID?: string;
|
||||
rootURL?: string;
|
||||
id?: string;
|
||||
events?: (eventEmitter: EventEmitter2) => void;
|
||||
}
|
||||
|
||||
export function render(config: Config = {}) {
|
||||
// Parse query params
|
||||
const query = qs.parse(location.search);
|
||||
const eventEmitter = new EventEmitter2({ wildcard: true });
|
||||
|
||||
if (config.events) {
|
||||
config.events(eventEmitter);
|
||||
}
|
||||
|
||||
return createStreamInterface({
|
||||
assetID: config.assetID || query.assetID,
|
||||
assetURL: config.assetURL || query.assetURL,
|
||||
commentID: config.commentID || query.commentID,
|
||||
id: config.id || "talk-embed-stream",
|
||||
rootURL: config.rootURL || location.origin,
|
||||
eventEmitter,
|
||||
});
|
||||
}
|
||||
import * as TalkImport from "./Talk";
|
||||
export const Talk = TalkImport;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export default function onIntersect(el: HTMLElement, callback: () => void) {
|
||||
if (!IntersectionObserver) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn("IntersectionObserver not available");
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
const options = {
|
||||
rootMargin: "100px",
|
||||
threshold: 1.0,
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
observer.disconnect();
|
||||
callback();
|
||||
}
|
||||
}, options);
|
||||
observer.observe(el);
|
||||
}
|
||||
@@ -2,3 +2,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";
|
||||
export { default as parseHashQuery } from "./parseHashQuery";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import parseHashQuery from "./parseHashQuery";
|
||||
|
||||
it("should parse hash", () => {
|
||||
const testCases: Array<[string, ReturnType<typeof parseHashQuery>]> = [
|
||||
[
|
||||
"#commentID=comment-id",
|
||||
{
|
||||
commentID: "comment-id",
|
||||
},
|
||||
],
|
||||
[
|
||||
"#commentID=comment-id&assetURL=asset-url",
|
||||
{
|
||||
commentID: "comment-id",
|
||||
assetURL: "asset-url",
|
||||
},
|
||||
],
|
||||
["#", {}],
|
||||
["", {}],
|
||||
];
|
||||
testCases.forEach(tc => {
|
||||
expect(parseHashQuery(tc[0])).toEqual(tc[1]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import qs from "query-string";
|
||||
|
||||
export default function parseQueryHash(hash: string): Record<string, string> {
|
||||
return qs.parse(hash);
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as buildURL } from "./buildURL";
|
||||
export { default as parseURL } from "./parseURL";
|
||||
export { default as modifyQuery } from "./modifyQuery";
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import modifyQuery from "./modifyQuery";
|
||||
|
||||
it("should modify query", () => {
|
||||
const testCases: Array<[string, Record<string, any>, string]> = [
|
||||
[
|
||||
"http://localhost:8080/?a=b#hash",
|
||||
{
|
||||
c: "d",
|
||||
},
|
||||
"http://localhost:8080/?a=b&c=d#hash",
|
||||
],
|
||||
[
|
||||
"http://localhost:8080/#hash",
|
||||
{
|
||||
a: "b",
|
||||
},
|
||||
"http://localhost:8080/?a=b#hash",
|
||||
],
|
||||
[
|
||||
"http://localhost:8080/?a=b#hash",
|
||||
{
|
||||
a: undefined,
|
||||
},
|
||||
"http://localhost:8080/#hash",
|
||||
],
|
||||
];
|
||||
testCases.forEach(([url, params, expected]) => {
|
||||
expect(modifyQuery(url, params)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import qs from "query-string";
|
||||
|
||||
import buildURL from "./buildURL";
|
||||
import parseURL from "./parseURL";
|
||||
|
||||
export default function modifyQuery(url: string, params: {}) {
|
||||
const parsed = parseURL(url);
|
||||
const query = qs.parse(parsed.search);
|
||||
parsed.search = qs.stringify({ ...query, ...params });
|
||||
return buildURL(parsed);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import parseHashQuery from "./parseHashQuery";
|
||||
|
||||
it("should parse hash", () => {
|
||||
const testCases: Array<[string, ReturnType<typeof parseHashQuery>]> = [
|
||||
[
|
||||
"#commentID=comment-id",
|
||||
{
|
||||
commentID: "comment-id",
|
||||
},
|
||||
],
|
||||
[
|
||||
"#commentID=comment-id&assetURL=asset-url",
|
||||
{
|
||||
commentID: "comment-id",
|
||||
assetURL: "asset-url",
|
||||
},
|
||||
],
|
||||
["#", {}],
|
||||
["", {}],
|
||||
];
|
||||
testCases.forEach(([url, expected]) => {
|
||||
expect(parseHashQuery(url)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import qs from "query-string";
|
||||
|
||||
export default function parseQueryHash(hash: string): Record<string, string> {
|
||||
return qs.parse(hash);
|
||||
}
|
||||
@@ -42,12 +42,8 @@ export default async function initLocalState(
|
||||
localRecord.setValue(query.assetID, "assetID");
|
||||
}
|
||||
|
||||
// Saving location host for permalink until we get the asset url - the url now points to the tenant
|
||||
if (location && query.assetID) {
|
||||
localRecord.setValue(
|
||||
`${location.origin}/?assetID=${query.assetID}`,
|
||||
"assetURL"
|
||||
);
|
||||
if (query.assetURL) {
|
||||
localRecord.setValue(query.assetURL, "assetURL");
|
||||
}
|
||||
|
||||
if (query.commentID) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import Comment from "./Comment";
|
||||
|
||||
it("renders username and body", () => {
|
||||
const props: PropTypesOf<typeof Comment> = {
|
||||
id: "comment-id",
|
||||
author: {
|
||||
username: "Marvin",
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import TopBarLeft from "./TopBarLeft";
|
||||
import Username from "./Username";
|
||||
|
||||
export interface CommentProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
author: {
|
||||
username: string | null;
|
||||
@@ -28,6 +29,7 @@ const Comment: StatelessComponent<CommentProps> = props => {
|
||||
className={styles.topBar}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
id={props.id}
|
||||
>
|
||||
<TopBarLeft>
|
||||
{props.author &&
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ exports[`renders username and body 1`] = `
|
||||
<withPropsOnChange(Flex)
|
||||
className="Comment-topBar"
|
||||
direction="row"
|
||||
id="comment-id"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<TopBarLeft>
|
||||
|
||||
@@ -15,7 +15,7 @@ import PermalinkPopover from "./PermalinkPopover";
|
||||
|
||||
interface PermalinkProps {
|
||||
commentID: string;
|
||||
assetURL: string | null;
|
||||
url: string;
|
||||
}
|
||||
|
||||
class Permalink extends React.Component<PermalinkProps> {
|
||||
@@ -28,7 +28,7 @@ class Permalink extends React.Component<PermalinkProps> {
|
||||
);
|
||||
|
||||
public render() {
|
||||
const { commentID, assetURL } = this.props;
|
||||
const { commentID, url } = this.props;
|
||||
const popoverID = `permalink-popover-${commentID}`;
|
||||
return (
|
||||
<Popover
|
||||
@@ -43,7 +43,7 @@ class Permalink extends React.Component<PermalinkProps> {
|
||||
}
|
||||
>
|
||||
<PermalinkPopover
|
||||
permalinkURL={`${assetURL}&commentID=${commentID}`}
|
||||
permalinkURL={url}
|
||||
toggleVisibility={toggleVisibility}
|
||||
/>
|
||||
</ClickOutside>
|
||||
|
||||
@@ -120,6 +120,7 @@ export class CommentContainer extends Component<InnerProps, State> {
|
||||
return (
|
||||
<>
|
||||
<Comment
|
||||
id={`comment-${comment.id}`}
|
||||
indentLevel={indentLevel}
|
||||
author={comment.author}
|
||||
body={comment.body}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { StatelessComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
import { withLocalStateContainer } from "talk-framework/lib/relay";
|
||||
import { modifyQuery } from "talk-framework/utils";
|
||||
import { PermalinkButtonContainerLocal as Local } from "talk-stream/__generated__/PermalinkButtonContainerLocal.graphql";
|
||||
|
||||
import PermalinkButton from "../components/PermalinkButton";
|
||||
@@ -15,7 +16,10 @@ export const PermalinkContainer: StatelessComponent<InnerProps> = ({
|
||||
commentID,
|
||||
}) => {
|
||||
return local.assetURL ? (
|
||||
<PermalinkButton assetURL={local.assetURL} commentID={commentID} />
|
||||
<PermalinkButton
|
||||
commentID={commentID}
|
||||
url={modifyQuery(local.assetURL, { commentID })}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,18 @@ class PermalinkViewContainer extends React.Component<
|
||||
// Remove the commentId url param.
|
||||
return buildURL({ ...urlParts, search });
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.props.pym) {
|
||||
const scrollTo = this.props.comment
|
||||
? document
|
||||
.getElementById(`comment-${this.props.comment.id}`)!
|
||||
.getBoundingClientRect().top + window.pageYOffset
|
||||
: 50;
|
||||
setTimeout(() => this.props.pym!.scrollParentToChildPos(scrollTo), 100);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { comment, asset, me } = this.props;
|
||||
return (
|
||||
@@ -66,6 +78,7 @@ const enhanced = withContext(ctx => ({
|
||||
`,
|
||||
comment: graphql`
|
||||
fragment PermalinkViewContainer_comment on Comment {
|
||||
id
|
||||
...CommentContainer_comment
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -135,7 +135,7 @@ const enhanced = withPaginationContainer<
|
||||
$count: Int!
|
||||
$cursor: Cursor
|
||||
$orderBy: COMMENT_SORT!
|
||||
$assetID: ID!
|
||||
$assetID: ID
|
||||
) {
|
||||
asset(id: $assetID) {
|
||||
...StreamContainer_asset
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ exports[`renders body only 1`] = `
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
id="comment-comment-id"
|
||||
indentLevel={1}
|
||||
showEditedMarker={false}
|
||||
/>
|
||||
@@ -54,6 +55,7 @@ exports[`renders username and body 1`] = `
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
id="comment-comment-id"
|
||||
indentLevel={1}
|
||||
showEditedMarker={false}
|
||||
/>
|
||||
|
||||
@@ -45,15 +45,19 @@ export const render = ({
|
||||
};
|
||||
|
||||
const PermalinkViewQuery: StatelessComponent<InnerProps> = ({
|
||||
local: { commentID, assetID },
|
||||
local: { commentID, assetID, assetURL },
|
||||
}) => (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query PermalinkViewQuery($commentID: ID!, $assetID: ID!) {
|
||||
query PermalinkViewQuery(
|
||||
$commentID: ID!
|
||||
$assetID: ID
|
||||
$assetURL: String
|
||||
) {
|
||||
me {
|
||||
...PermalinkViewContainer_me
|
||||
}
|
||||
asset(id: $assetID) {
|
||||
asset(id: $assetID, url: $assetURL) {
|
||||
...PermalinkViewContainer_asset
|
||||
}
|
||||
comment(id: $commentID) {
|
||||
@@ -62,8 +66,9 @@ const PermalinkViewQuery: StatelessComponent<InnerProps> = ({
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
assetID: assetID!,
|
||||
commentID: commentID!,
|
||||
assetID,
|
||||
assetURL,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
@@ -74,6 +79,7 @@ const enhanced = withLocalStateContainer(
|
||||
fragment PermalinkViewQueryLocal on Local {
|
||||
assetID
|
||||
commentID
|
||||
assetURL
|
||||
}
|
||||
`
|
||||
)(PermalinkViewQuery);
|
||||
|
||||
@@ -38,21 +38,22 @@ export const render = ({
|
||||
};
|
||||
|
||||
const StreamQuery: StatelessComponent<InnerProps> = ({
|
||||
local: { assetID },
|
||||
local: { assetID, assetURL },
|
||||
}) => (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query StreamQuery($assetID: ID!) {
|
||||
query StreamQuery($assetID: ID, $assetURL: String) {
|
||||
me {
|
||||
...StreamContainer_me
|
||||
}
|
||||
asset(id: $assetID) {
|
||||
asset(id: $assetID, url: $assetURL) {
|
||||
...StreamContainer_asset
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{
|
||||
assetID: assetID!,
|
||||
assetID,
|
||||
assetURL,
|
||||
}}
|
||||
render={render}
|
||||
/>
|
||||
@@ -62,6 +63,7 @@ const enhanced = withLocalStateContainer(
|
||||
graphql`
|
||||
fragment StreamQueryLocal on Local {
|
||||
assetID
|
||||
assetURL
|
||||
}
|
||||
`
|
||||
)(StreamQuery);
|
||||
|
||||
@@ -199,6 +199,7 @@ exports[`cancel edit: edit canceled 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -284,6 +285,7 @@ exports[`cancel edit: edit canceled 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -707,6 +709,7 @@ exports[`edit a comment: edit form 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1130,6 +1133,7 @@ exports[`edit a comment: optimistic response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1388,6 +1392,7 @@ exports[`edit a comment: render stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1473,6 +1478,7 @@ exports[`edit a comment: render stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1731,6 +1737,7 @@ exports[`edit a comment: server response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1825,6 +1832,7 @@ exports[`edit a comment: server response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -2083,6 +2091,7 @@ exports[`shows expiry message: edit form closed 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -2168,6 +2177,7 @@ exports[`shows expiry message: edit form closed 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -2568,6 +2578,7 @@ exports[`shows expiry message: edit time expired 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
|
||||
@@ -160,6 +160,7 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -229,6 +230,7 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -298,6 +300,7 @@ exports[`loads more comments 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-2"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -517,6 +520,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -586,6 +590,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
|
||||
@@ -35,6 +35,7 @@ exports[`renders permalink view 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -252,6 +253,7 @@ exports[`show all comments 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
|
||||
+1
@@ -192,6 +192,7 @@ exports[`show all comments 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
|
||||
@@ -193,6 +193,7 @@ exports[`post a comment: optimistic response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-uuid-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -278,6 +279,7 @@ exports[`post a comment: optimistic response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -347,6 +349,7 @@ exports[`post a comment: optimistic response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -605,6 +608,7 @@ exports[`post a comment: server response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-x"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -674,6 +678,7 @@ exports[`post a comment: server response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -743,6 +748,7 @@ exports[`post a comment: server response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1001,6 +1007,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1070,6 +1077,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
|
||||
@@ -199,6 +199,7 @@ exports[`post a reply: open reply form 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -395,6 +396,7 @@ exports[`post a reply: open reply form 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -653,6 +655,7 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -847,6 +850,7 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-uuid-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -934,6 +938,7 @@ exports[`post a reply: optimistic response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1192,6 +1197,7 @@ exports[`post a reply: server response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1265,6 +1271,7 @@ exports[`post a reply: server response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-x"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1336,6 +1343,7 @@ exports[`post a reply: server response 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1594,6 +1602,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -1663,6 +1672,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
|
||||
@@ -160,6 +160,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -229,6 +230,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-with-deep-replies"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -302,6 +304,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-with-replies"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -375,6 +378,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-3"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -444,6 +448,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-4"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -515,6 +520,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-5"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
|
||||
@@ -160,6 +160,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -229,6 +230,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
|
||||
@@ -160,6 +160,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -233,6 +234,7 @@ exports[`renders comment stream 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -478,6 +480,7 @@ exports[`show all replies 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-0"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -551,6 +554,7 @@ exports[`show all replies 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-1"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
@@ -620,6 +624,7 @@ exports[`show all replies 1`] = `
|
||||
>
|
||||
<div
|
||||
className="Flex-root Comment-topBar Flex-flex Flex-justifySpaceBetween Flex-directionRow"
|
||||
id="comment-comment-2"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
|
||||
|
||||
@@ -11,7 +11,10 @@ function createTestRenderer() {
|
||||
Query: {
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0])
|
||||
s =>
|
||||
s
|
||||
.withArgs(undefined, { id: assets[0].id, url: null })
|
||||
.returns(assets[0])
|
||||
),
|
||||
me: createSinonStub(
|
||||
s => s.throws(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { timeout } from "talk-common/utils";
|
||||
import { createSinonStub } from "talk-framework/testHelpers";
|
||||
@@ -55,7 +56,15 @@ beforeEach(() => {
|
||||
Query: {
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub)
|
||||
s =>
|
||||
s
|
||||
.withArgs(
|
||||
undefined,
|
||||
sinon
|
||||
.match({ id: assetStub.id, url: null })
|
||||
.or(sinon.match({ id: assetStub.id }))
|
||||
)
|
||||
.returns(assetStub)
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,7 +36,10 @@ beforeEach(() => {
|
||||
),
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub)
|
||||
s =>
|
||||
s
|
||||
.withArgs(undefined, { id: assetStub.id, url: null })
|
||||
.returns(assetStub)
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,7 +33,10 @@ beforeEach(() => {
|
||||
comment: () => null,
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub)
|
||||
s =>
|
||||
s
|
||||
.withArgs(undefined, { id: assetStub.id, url: null })
|
||||
.returns(assetStub)
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ beforeEach(() => {
|
||||
s => s.throws(),
|
||||
s =>
|
||||
s
|
||||
.withArgs(undefined, { id: assetWithDeepReplies.id })
|
||||
.withArgs(undefined, { id: assetWithDeepReplies.id, url: null })
|
||||
.returns(assetWithDeepReplies)
|
||||
),
|
||||
},
|
||||
|
||||
@@ -12,7 +12,10 @@ beforeEach(() => {
|
||||
Query: {
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { id: assets[0].id }).returns(assets[0])
|
||||
s =>
|
||||
s
|
||||
.withArgs(undefined, { id: assets[0].id, url: null })
|
||||
.returns(assets[0])
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -71,7 +71,10 @@ beforeEach(() => {
|
||||
),
|
||||
asset: createSinonStub(
|
||||
s => s.throws(),
|
||||
s => s.withArgs(undefined, { id: assetStub.id }).returns(assetStub)
|
||||
s =>
|
||||
s
|
||||
.withArgs(undefined, { id: assetStub.id, url: null })
|
||||
.returns(assetStub)
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
// Automatically unmock console.
|
||||
import "jest-mock-console/dist/setupTestFramework";
|
||||
@@ -4,65 +4,34 @@ import { Config } from "talk-common/config";
|
||||
import {
|
||||
createJWTSigningConfig,
|
||||
extractJWTFromRequest,
|
||||
parseAuthHeader,
|
||||
} from "talk-server/app/middleware/passport/jwt";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
describe("parseAuthHeader", () => {
|
||||
it("parses valid headers", () => {
|
||||
const parsed = {
|
||||
scheme: "bearer",
|
||||
value: "token",
|
||||
};
|
||||
|
||||
expect(parseAuthHeader("Bearer token")).toEqual(parsed);
|
||||
|
||||
expect(parseAuthHeader("bearer token")).toEqual(parsed);
|
||||
|
||||
expect(parseAuthHeader("bearer token")).toEqual(parsed);
|
||||
});
|
||||
|
||||
it("parses invalid headers", () => {
|
||||
expect(parseAuthHeader("this-is-a-wrong-header")).toEqual(null);
|
||||
expect(parseAuthHeader("bearerthis-is-a-wrong-header")).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractJWTFromRequest", () => {
|
||||
it("extracts the token from header", () => {
|
||||
const req = {
|
||||
get: sinon
|
||||
.stub()
|
||||
.withArgs("authorization")
|
||||
.returns("Bearer token"),
|
||||
headers: {
|
||||
authorization: "Bearer token",
|
||||
},
|
||||
url: "",
|
||||
};
|
||||
|
||||
expect(extractJWTFromRequest((req as any) as Request)).toEqual("token");
|
||||
expect(req.get.calledOnce).toBeTruthy();
|
||||
|
||||
req.get.reset();
|
||||
req.get.returns(null);
|
||||
delete req.headers.authorization;
|
||||
|
||||
expect(extractJWTFromRequest((req as any) as Request)).toEqual(null);
|
||||
expect(req.get.calledOnce).toBeTruthy();
|
||||
});
|
||||
|
||||
it("extracts the token from query string", () => {
|
||||
const req = {
|
||||
get: sinon
|
||||
.stub()
|
||||
.withArgs("authorization")
|
||||
.returns(null),
|
||||
query: { access_token: "token" },
|
||||
url: "",
|
||||
};
|
||||
expect(extractJWTFromRequest((req as any) as Request)).toEqual(null);
|
||||
|
||||
req.url = "https://talk.coralproject.net/api?access_token=token";
|
||||
|
||||
expect(extractJWTFromRequest((req as any) as Request)).toEqual("token");
|
||||
expect(req.get.calledOnce).toBeTruthy();
|
||||
|
||||
delete req.query.access_token;
|
||||
|
||||
req.get.reset();
|
||||
expect(extractJWTFromRequest((req as any) as Request)).toEqual(null);
|
||||
expect(req.get.calledOnce).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,41 +2,20 @@ import { Redis } from "ioredis";
|
||||
import jwt, { SignOptions } from "jsonwebtoken";
|
||||
import { Db } from "mongodb";
|
||||
import { Strategy } from "passport-strategy";
|
||||
import { Bearer } from "permit";
|
||||
import uuid from "uuid";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import { retrieveUser, User } from "talk-server/models/user";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
const authHeaderRegex = /(\S+)\s+(\S+)/;
|
||||
|
||||
export function parseAuthHeader(header: string) {
|
||||
const matches = header.match(authHeaderRegex);
|
||||
if (!matches || matches.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
scheme: matches[1].toLowerCase(),
|
||||
value: matches[2],
|
||||
};
|
||||
}
|
||||
|
||||
export function extractJWTFromRequest(req: Request) {
|
||||
const header = req.get("authorization");
|
||||
if (header) {
|
||||
const parts = parseAuthHeader(header);
|
||||
if (parts && parts.scheme === "bearer") {
|
||||
return parts.value;
|
||||
}
|
||||
}
|
||||
const permit = new Bearer({
|
||||
basic: "password",
|
||||
query: "access_token",
|
||||
});
|
||||
|
||||
const token: string | undefined | false = req.query && req.query.access_token;
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
|
||||
return null;
|
||||
return permit.check(req) || null;
|
||||
}
|
||||
|
||||
function generateJTIBlacklistKey(jti: string) {
|
||||
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
// TODO: (wyattjoh) following https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29061 to merge then replace this with @types/permit.
|
||||
declare module "permit" {
|
||||
import { IncomingMessage, ServerResponse } from "http";
|
||||
|
||||
export interface PermitOptions {
|
||||
scheme?: string;
|
||||
proxy?: string;
|
||||
realm?: string;
|
||||
}
|
||||
|
||||
export interface BearerOptions extends PermitOptions {
|
||||
basic?: string;
|
||||
header?: string;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
export class Permit {
|
||||
constructor(options: PermitOptions);
|
||||
check(req: IncomingMessage): void;
|
||||
fail(res: ServerResponse): void;
|
||||
}
|
||||
|
||||
export class Bearer extends Permit {
|
||||
constructor(options: BearerOptions);
|
||||
check(req: IncomingMessage): string;
|
||||
}
|
||||
|
||||
export class Basic extends Permit {
|
||||
check(req: IncomingMessage): [string, string];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user