Merge branch 'next' into update-types-recompose

This commit is contained in:
Wyatt Johnson
2018-09-21 22:44:05 +00:00
committed by GitHub
60 changed files with 959 additions and 308 deletions
+2
View File
@@ -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",
+11
View File
@@ -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",
+2
View File
@@ -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",
+29 -13
View File
@@ -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`.
+2
View File
@@ -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"),
+7 -1
View File
@@ -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
-70
View File
@@ -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();
});
-89
View File
@@ -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);
}
+166
View File
@@ -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();
});
+127
View File
@@ -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);
}
+60
View File
@@ -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&amp;childId=pymcontrol-test-id&amp;parentTitle=&amp;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&amp;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?&amp;initialWidth=0&amp;childId=basic-integration-test-id&amp;parentTitle=&amp;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&amp;initialWidth=0&amp;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&amp;initialWidth=0&amp;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>"`;
+88
View File
@@ -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>
+103
View File
@@ -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>
+8 -3
View File
@@ -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>
+33 -4
View File
@@ -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();
});
});
+2 -32
View File
@@ -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;
+20
View File
@@ -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);
}
+1
View File
@@ -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
View File
@@ -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 &&
@@ -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
@@ -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"
@@ -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();
});
});
+6 -27
View File
@@ -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) {
+31
View File
@@ -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];
}
}