mirror of
https://github.com/wassname/talk.git
synced 2026-08-17 11:27:52 +08:00
[CORL-181] Comment Count Injection (#2581)
* feat: inject comment counts * fix: tests * feat: integrate stream embed with coral counts * chore: refactor constants * test: test for count bundle * test: test live comment count integration with stream embed * feat: use defer * fix: snapshot * feat: auto add count.js in when calling Coral.createStreamEmbed * fix: tests * fix: rm duplicate test * chore: remove unuse file
This commit is contained in:
@@ -2,9 +2,9 @@ import { stripIndent } from "common-tags";
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
|
||||
import { getLocationOrigin } from "coral-common/utils";
|
||||
import { CopyButton } from "coral-framework/components";
|
||||
import { GetMessage, withGetMessage } from "coral-framework/lib/i18n";
|
||||
import { getLocationOrigin } from "coral-framework/utils";
|
||||
import { HorizontalGutter, Typography } from "coral-ui/components";
|
||||
|
||||
import Header from "../../Header";
|
||||
@@ -56,6 +56,8 @@ const EmbedCode: FunctionComponent<Props> = ({ staticURI, getMessage }) => {
|
||||
(function() {
|
||||
var d = document, s = d.createElement('script');
|
||||
s.src = '${script}/assets/js/embed.js';
|
||||
s.async = false;
|
||||
s.defer = true;
|
||||
s.onload = function() {
|
||||
Coral.createStreamEmbed({
|
||||
id: "coral_thread",
|
||||
|
||||
@@ -138,12 +138,14 @@ each of your site’s stories.
|
||||
<textarea
|
||||
className="EmbedCode-textArea"
|
||||
readOnly={true}
|
||||
rows={22}
|
||||
rows={24}
|
||||
value="<div id=\\"coral_thread\\"></div>
|
||||
<script type=\\"text/javascript\\">
|
||||
(function() {
|
||||
var d = document, s = d.createElement('script');
|
||||
s.src = 'http://localhost/assets/js/embed.js';
|
||||
s.async = false;
|
||||
s.defer = true;
|
||||
s.onload = function() {
|
||||
Coral.createStreamEmbed({
|
||||
id: \\"coral_thread\\",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* getCurrentScriptOrigin will try to find the script origin.
|
||||
* For legacy browsers a fallbackIdentifier is required.
|
||||
*
|
||||
* @argument fallbackID id attached to a script tag to get its origin from for legacy browsrs.
|
||||
*/
|
||||
function getCurrentScriptOrigin(fallbackID?: string) {
|
||||
// Find current script (modern browsers).
|
||||
let script = document.currentScript as HTMLScriptElement | null;
|
||||
|
||||
if (!script && fallbackID) {
|
||||
// Find script tag with `fallbackIdentifier` as its id.
|
||||
script = document.getElementById(fallbackID) as HTMLScriptElement | null;
|
||||
if (!script) {
|
||||
// Find script tag with `fallbackIdentifier` as its className.
|
||||
script = document.querySelector(
|
||||
`.${fallbackID}`
|
||||
) as HTMLScriptElement | null;
|
||||
}
|
||||
}
|
||||
if (!script) {
|
||||
throw new Error("Current script not found");
|
||||
}
|
||||
return new URL(script.src).origin;
|
||||
}
|
||||
|
||||
export default getCurrentScriptOrigin;
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Coral - Count</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" />
|
||||
<link rel="canonical" href="http://localhost:8080/" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Comment Counts</h1>
|
||||
<h3>Specified by canonical link</h3>
|
||||
<p><a href="http://localhost:8080/">Default: <span class="coral-count"></span></a></p>
|
||||
|
||||
<h3>Specified by data-coral-url</h3>
|
||||
<p><a href="http://localhost:8080/story.html">Story: <span class="coral-count" data-coral-url="http://localhost:8080/story.html"></span></a></p>
|
||||
<p><a href="http://localhost:8080/storyButton.html">Story With Button: <span class="coral-count" data-coral-url="http://localhost:8080/storyButton.html"></span></a></p>
|
||||
|
||||
<h3>No text</h3>
|
||||
<p><a href="http://localhost:8080/">Default notext: <span class="coral-count" data-coral-notext="true" data-coral-url="http://localhost:8080/"></span></a></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,61 @@
|
||||
import { COUNT_SELECTOR, ORIGIN_FALLBACK_ID } from "coral-framework/constants";
|
||||
import detectCountScript from "coral-framework/helpers/detectCountScript";
|
||||
import resolveStoryURL from "coral-framework/helpers/resolveStoryURL";
|
||||
import jsonp from "coral-framework/utils/jsonp";
|
||||
|
||||
import getCurrentScriptOrigin from "./getCurrentScriptOrigin";
|
||||
import injectJSONPCallback from "./injectJSONPCallback";
|
||||
|
||||
/** Arguments that will be send to the server. */
|
||||
interface CountQueryArgs {
|
||||
id?: string;
|
||||
url?: string;
|
||||
notext?: boolean;
|
||||
}
|
||||
|
||||
/** createCountQueryRef creates a unique reference from the query args */
|
||||
function createCountQueryRef(args: CountQueryArgs) {
|
||||
return btoa(`${JSON.stringify(!!args.notext)};${args.id || args.url}`);
|
||||
}
|
||||
|
||||
/** Detects count elements and use jsonp to inject the counts. */
|
||||
function detectAndInject() {
|
||||
const ORIGIN = getCurrentScriptOrigin(ORIGIN_FALLBACK_ID);
|
||||
const STORY_URL = resolveStoryURL();
|
||||
/** A map of references pointing to the count query arguments */
|
||||
const queryMap: Record<string, CountQueryArgs> = {};
|
||||
|
||||
// Find all the selected elements and fill the queryMap.
|
||||
const elements = document.querySelectorAll(COUNT_SELECTOR);
|
||||
Array.prototype.forEach.call(elements, (element: HTMLElement) => {
|
||||
let url = element.dataset.coralUrl;
|
||||
const id = element.dataset.coralId;
|
||||
const notext = element.dataset.coralNotext === "true";
|
||||
if (!url && !id) {
|
||||
url = STORY_URL;
|
||||
element.dataset.coralUrl = STORY_URL;
|
||||
}
|
||||
const args = { id, url, notext };
|
||||
const ref = createCountQueryRef(args);
|
||||
if (!(ref in queryMap)) {
|
||||
queryMap[ref] = args;
|
||||
}
|
||||
element.dataset.coralRef = ref;
|
||||
});
|
||||
|
||||
// Call server using JSONP.
|
||||
Object.keys(queryMap).forEach(ref => {
|
||||
const { url, id, notext } = queryMap[ref];
|
||||
const args = { url, id, notext: notext ? "true" : "false", ref };
|
||||
jsonp(`${ORIGIN}/api/story/count.js`, "CoralCount.setCount", args);
|
||||
});
|
||||
}
|
||||
|
||||
export function main() {
|
||||
injectJSONPCallback();
|
||||
detectAndInject();
|
||||
}
|
||||
|
||||
if (!detectCountScript() && process.env.NODE_ENV !== "test") {
|
||||
main();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { COUNT_SELECTOR } from "coral-framework/constants";
|
||||
|
||||
/** Injects a global CoralCount callback into the window object to be used in JSONP */
|
||||
function injectJSONPCallback() {
|
||||
(window as any).CoralCount = {
|
||||
setCount: (data: { ref: string; html: string }) => {
|
||||
// Find all the elements with ref.
|
||||
const elements = document.querySelectorAll(
|
||||
`${COUNT_SELECTOR}[data-coral-ref='${data.ref}']`
|
||||
);
|
||||
Array.prototype.forEach.call(elements, (element: HTMLElement) => {
|
||||
element.innerHTML = data.html;
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default injectJSONPCallback;
|
||||
@@ -0,0 +1,95 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Calls JSONP 1`] = `
|
||||
<body>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw="
|
||||
data-coral-url="http://localhost:8080/story.html"
|
||||
/>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-id="1234-5678-91021"
|
||||
data-coral-ref="ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
|
||||
/>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
|
||||
data-coral-url="http://localhost:8080/"
|
||||
data-notext="true"
|
||||
/>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
|
||||
data-coral-url="http://localhost:8080/"
|
||||
/>
|
||||
<script
|
||||
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html¬ext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D"
|
||||
/>
|
||||
<script
|
||||
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=1234-5678-91021¬ext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
|
||||
/>
|
||||
<script
|
||||
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&url=http%3A%2F%2Flocalhost%3A8080%2F¬ext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D"
|
||||
/>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Inject counts 1`] = `
|
||||
<body>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw="
|
||||
data-coral-url="http://localhost:8080/story.html"
|
||||
/>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-id="1234-5678-91021"
|
||||
data-coral-ref="ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
|
||||
/>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
|
||||
data-coral-url="http://localhost:8080/"
|
||||
data-notext="true"
|
||||
>
|
||||
<span
|
||||
class="coral-count-number"
|
||||
>
|
||||
5
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="coral-count-text"
|
||||
>
|
||||
Comments
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-ref="ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw=="
|
||||
data-coral-url="http://localhost:8080/"
|
||||
>
|
||||
<span
|
||||
class="coral-count-number"
|
||||
>
|
||||
5
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="coral-count-text"
|
||||
>
|
||||
Comments
|
||||
</span>
|
||||
</span>
|
||||
<script
|
||||
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&url=http%3A%2F%2Flocalhost%3A8080%2Fstory.html¬ext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwL3N0b3J5Lmh0bWw%3D"
|
||||
/>
|
||||
<script
|
||||
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&id=1234-5678-91021¬ext=false&ref=ZmFsc2U7MTIzNC01Njc4LTkxMDIx"
|
||||
/>
|
||||
<script
|
||||
src="http://localhost:8080/api/story/count.js?callback=CoralCount.setCount&url=http%3A%2F%2Flocalhost%3A8080%2F¬ext=false&ref=ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw%3D%3D"
|
||||
/>
|
||||
</body>
|
||||
`;
|
||||
@@ -0,0 +1,53 @@
|
||||
beforeAll(async () => {
|
||||
const script = document.createElement("script");
|
||||
script.src = "http://localhost:8080/assets/js/count.js";
|
||||
Object.defineProperty(window.document, "currentScript", {
|
||||
value: script,
|
||||
});
|
||||
|
||||
const link = document.createElement("link");
|
||||
link.rel = "canonical";
|
||||
link.href = "http://localhost:8080/";
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
document.body.innerHTML = "";
|
||||
const tags = [
|
||||
{
|
||||
coralUrl: "http://localhost:8080/story.html",
|
||||
},
|
||||
{
|
||||
coralId: "1234-5678-91021",
|
||||
},
|
||||
{
|
||||
notext: "true",
|
||||
},
|
||||
{},
|
||||
];
|
||||
|
||||
tags.forEach(attrs => {
|
||||
const element = document.createElement("span");
|
||||
element.className = "coral-count";
|
||||
Object.assign(element.dataset, attrs);
|
||||
document.body.appendChild(element);
|
||||
});
|
||||
(await import("../")).main();
|
||||
});
|
||||
|
||||
it("Sets the JSONP callback", async () => {
|
||||
expect((window as any).CoralCount).toBeDefined();
|
||||
});
|
||||
|
||||
it("Calls JSONP", async () => {
|
||||
expect(document.body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("Inject counts", async () => {
|
||||
(window as any).CoralCount.setCount({
|
||||
ref: "ZmFsc2U7aHR0cDovL2xvY2FsaG9zdDo4MDgwLw==",
|
||||
html:
|
||||
'<span class="coral-count-number">5</span> <span class="coral-count-text">Comments</span>',
|
||||
});
|
||||
expect(document.body).toMatchSnapshot();
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
|
||||
import { getLocationOrigin, parseQuery } from "coral-common/utils";
|
||||
import { parseQuery } from "coral-common/utils";
|
||||
import resolveStoryURL from "coral-framework/helpers/resolveStoryURL";
|
||||
import getLocationOrigin from "coral-framework/utils/getLocationOrigin";
|
||||
|
||||
import { default as create, StreamEmbed } from "./StreamEmbed";
|
||||
|
||||
@@ -15,22 +17,6 @@ export interface Config {
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
function resolveStoryURL() {
|
||||
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. Coral has inferred this story_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 = parseQuery(location.search);
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
withConfig,
|
||||
withEventEmitter,
|
||||
withIOSSafariWidthWorkaround,
|
||||
withLiveCommentCount,
|
||||
withPymStorage,
|
||||
withSetCommentID,
|
||||
} from "./decorators";
|
||||
import injectCountScriptIfNeeded from "./injectCountScriptIfNeeded";
|
||||
import onIntersect, { OnIntersectCancellation } from "./onIntersect";
|
||||
import PymControl, {
|
||||
defaultPymControlFactory,
|
||||
@@ -46,6 +48,10 @@ export class StreamEmbed {
|
||||
) {
|
||||
this.config = config;
|
||||
this.pymControlFactory = pymControlFactory;
|
||||
|
||||
// Detect if comment count injection is needed and add the count script.
|
||||
injectCountScriptIfNeeded(config.rootURL);
|
||||
|
||||
if (config.commentID) {
|
||||
// Delay emit of `showPermalink` event to allow
|
||||
// user enough time to setup event listeners.
|
||||
@@ -132,6 +138,7 @@ export class StreamEmbed {
|
||||
withClickEvent,
|
||||
withSetCommentID,
|
||||
withEventEmitter(this.config.eventEmitter),
|
||||
withLiveCommentCount(this.config.eventEmitter),
|
||||
withPymStorage(localStorage, "localStorage"),
|
||||
withPymStorage(sessionStorage, "sessionStorage"),
|
||||
withConfig(externalConfig),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should live update counts when receiving a commentCount event 1`] = `
|
||||
<body>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-id="story-1"
|
||||
>
|
||||
<span
|
||||
class="coral-count-number"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="coral-count-text"
|
||||
>
|
||||
Comments
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-url="http://localhost/stories/story-1"
|
||||
>
|
||||
<span
|
||||
class="coral-count-number"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="coral-count-text"
|
||||
>
|
||||
Comments
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="coral-count"
|
||||
data-coral-id="story-x"
|
||||
>
|
||||
<span
|
||||
class="coral-count-number"
|
||||
>
|
||||
4
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="coral-count-text"
|
||||
>
|
||||
Comments
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="x-count"
|
||||
data-coral-id="story-1"
|
||||
>
|
||||
<span
|
||||
class="coral-count-number"
|
||||
>
|
||||
1
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="coral-count-text"
|
||||
>
|
||||
Comment
|
||||
</span>
|
||||
</span>
|
||||
</body>
|
||||
`;
|
||||
@@ -5,6 +5,7 @@ export { default as withSetCommentID } from "./withSetCommentID";
|
||||
export { default as withEventEmitter } from "./withEventEmitter";
|
||||
export { default as withPymStorage } from "./withPymStorage";
|
||||
export { default as withConfig } from "./withConfig";
|
||||
export { default as withLiveCommentCount } from "./withLiveCommentCount";
|
||||
export {
|
||||
default as withIOSSafariWidthWorkaround,
|
||||
} from "./withIOSSafariWidthWorkaround";
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
|
||||
import withLiveCommentCount from "./withLiveCommentCount";
|
||||
|
||||
beforeAll(() => {
|
||||
const elementWithID = document.createElement("span");
|
||||
elementWithID.className = "coral-count";
|
||||
elementWithID.dataset.coralId = "story-1";
|
||||
elementWithID.innerHTML =
|
||||
'<span class="coral-count-number">1</span> <span class="coral-count-text">Comment</span>';
|
||||
document.body.appendChild(elementWithID);
|
||||
|
||||
const elementWithURL = document.createElement("span");
|
||||
elementWithURL.className = "coral-count";
|
||||
elementWithURL.dataset.coralUrl = "http://localhost/stories/story-1";
|
||||
elementWithURL.innerHTML =
|
||||
'<span class="coral-count-number">1</span> <span class="coral-count-text">Comment</span>';
|
||||
document.body.appendChild(elementWithURL);
|
||||
|
||||
const nonMatchingElement = document.createElement("span");
|
||||
nonMatchingElement.className = "coral-count";
|
||||
nonMatchingElement.dataset.coralId = "story-x";
|
||||
nonMatchingElement.innerHTML =
|
||||
'<span class="coral-count-number">4</span> <span class="coral-count-text">Comments</span>';
|
||||
document.body.appendChild(nonMatchingElement);
|
||||
|
||||
const wrongSelectorElement = document.createElement("span");
|
||||
wrongSelectorElement.className = "x-count";
|
||||
wrongSelectorElement.dataset.coralId = "story-1";
|
||||
wrongSelectorElement.innerHTML =
|
||||
'<span class="coral-count-number">1</span> <span class="coral-count-text">Comment</span>';
|
||||
document.body.appendChild(wrongSelectorElement);
|
||||
});
|
||||
|
||||
it("should live update counts when receiving a commentCount event", () => {
|
||||
const events = new EventEmitter2({ wildcard: true });
|
||||
const fakePym = null as any;
|
||||
withLiveCommentCount(events)(fakePym);
|
||||
events.emit("commentCount", {
|
||||
number: 2,
|
||||
storyID: "story-1",
|
||||
storyURL: "http://localhost/stories/story-1",
|
||||
text: "Comments",
|
||||
});
|
||||
expect(document.body).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { EventEmitter2 } from "eventemitter2";
|
||||
|
||||
import { COUNT_SELECTOR } from "coral-framework/constants";
|
||||
|
||||
import { Decorator } from "./types";
|
||||
|
||||
/**
|
||||
* withLiveCommentCount will listen to `commentCount` events
|
||||
* and update any comment counts managed by our `count.js` script.
|
||||
*/
|
||||
const withLiveCommentCount = (eventEmitter: EventEmitter2): Decorator => () => {
|
||||
eventEmitter.on("commentCount", args => {
|
||||
// Find all matching elements.
|
||||
const elements = document.querySelectorAll(
|
||||
`${COUNT_SELECTOR}[data-coral-url='${
|
||||
args.storyURL
|
||||
}'], ${COUNT_SELECTOR}[data-coral-id='${args.storyID}']`
|
||||
);
|
||||
elements.forEach(element => {
|
||||
// Replace number.
|
||||
element.querySelectorAll(".coral-count-number").forEach(no => {
|
||||
no.innerHTML = args.number;
|
||||
});
|
||||
// Replace text.
|
||||
element.querySelectorAll(".coral-count-text").forEach(no => {
|
||||
no.innerHTML = args.text;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default withLiveCommentCount;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { COUNT_SELECTOR } from "coral-framework/constants";
|
||||
import detectCountScript from "coral-framework/helpers/detectCountScript";
|
||||
|
||||
/**
|
||||
* injectCountScriptIfNeeded will detect if comment count injection is necessary and
|
||||
* automatically includes the `count.js` script.
|
||||
*/
|
||||
const injectCountScriptIfNeeded = (rootURL: string) => {
|
||||
if (detectCountScript()) {
|
||||
return;
|
||||
}
|
||||
// Detect if we need to inject counts.
|
||||
if (document.querySelector(COUNT_SELECTOR)) {
|
||||
const s = document.createElement("script");
|
||||
s.src = `${rootURL}/assets/js/count.js`;
|
||||
s.async = false;
|
||||
s.defer = true;
|
||||
(document.head || document.body).appendChild(s);
|
||||
}
|
||||
};
|
||||
|
||||
export default injectCountScriptIfNeeded;
|
||||
@@ -19,6 +19,9 @@
|
||||
<a href="/storyButton.html">Story With Button</a>
|
||||
</p>
|
||||
<h1 style="text-align: center">Coral 5.0 – Story</h1>
|
||||
<p>
|
||||
<a href="#coralStreamEmbed"><span class="coral-count"></span></a>
|
||||
</p>
|
||||
<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
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<a href="/story.html">Story</a>
|
||||
</p>
|
||||
<h1 style="text-align: center">Coral 5.0 – Story with Button</h1>
|
||||
<p>
|
||||
<a href="#coralStreamEmbed"><span class="coral-count"></span></a>
|
||||
</p>
|
||||
<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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import mockConsole from "jest-mock-console";
|
||||
import * as Coral from "./";
|
||||
import * as Coral from "../";
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import mockConsole from "jest-mock-console";
|
||||
import * as Coral from "../";
|
||||
|
||||
import { COUNT_SELECTOR } from "coral-framework/constants";
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
describe("Basic integration test", () => {
|
||||
const container: HTMLElement = document.createElement("div");
|
||||
beforeAll(() => {
|
||||
container.id = "basic-integration-test-id";
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
afterAll(() => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
it("should not inject count script", () => {
|
||||
mockConsole();
|
||||
const CoralEmbedStream = Coral.createStreamEmbed({
|
||||
id: "basic-integration-test-id",
|
||||
});
|
||||
CoralEmbedStream.render();
|
||||
expect(document.head.querySelector("script")).toBeNull();
|
||||
});
|
||||
it("should inject count script", () => {
|
||||
mockConsole();
|
||||
const commentCount: HTMLElement = document.createElement("div");
|
||||
commentCount.className = "coral-count";
|
||||
document.body.appendChild(commentCount);
|
||||
Coral.createStreamEmbed({
|
||||
id: "basic-integration-test-id",
|
||||
});
|
||||
const s = document.head.querySelector("script");
|
||||
expect(s).not.toBeNull();
|
||||
expect(s!.defer).toBe(true);
|
||||
document.head.removeChild(s!);
|
||||
document.body.removeChild(commentCount);
|
||||
});
|
||||
it("should not inject count script when it's already there", () => {
|
||||
mockConsole();
|
||||
|
||||
// Make detectCountScript return true.
|
||||
(window as any).CoralCount = {};
|
||||
|
||||
const commentCount: HTMLElement = document.createElement("div");
|
||||
commentCount.className = COUNT_SELECTOR;
|
||||
document.body.appendChild(commentCount);
|
||||
const CoralEmbedStream = Coral.createStreamEmbed({
|
||||
id: "basic-integration-test-id",
|
||||
});
|
||||
CoralEmbedStream.render();
|
||||
expect(document.head.querySelector("script")).toBeNull();
|
||||
document.body.removeChild(commentCount);
|
||||
|
||||
delete (window as any).CoralCount;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* COUNT_SELECTOR is a css selector used to identify elements that
|
||||
* will be replaced by the story count.
|
||||
*/
|
||||
export const COUNT_SELECTOR = ".coral-count";
|
||||
|
||||
/**
|
||||
* ORIGIN_FALLBACK_ID can be attached to any <script /> tag as an
|
||||
* id to allow the `count.js` script to find its origin when
|
||||
* `document.currentScript` is not available (for legacy browsers).
|
||||
*/
|
||||
export const ORIGIN_FALLBACK_ID = "coral-origin";
|
||||
@@ -0,0 +1,7 @@
|
||||
function detectCountScript() {
|
||||
// If CoralCount JSONP callback has been defined, then the
|
||||
// count script has already run.
|
||||
return (window as any).CoralCount !== undefined;
|
||||
}
|
||||
|
||||
export default detectCountScript;
|
||||
@@ -10,3 +10,5 @@ export {
|
||||
export { default as getParamsFromHash } from "./getParamsFromHash";
|
||||
export { default as clearHash } from "./clearHash";
|
||||
export { default as roleIsAtLeast } from "./roleIsAtLeast";
|
||||
export { default as resolveStoryURL } from "./resolveStoryURL";
|
||||
export { default as detectCountScript } from "./detectCountScript";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getLocationOrigin } from "coral-framework/utils";
|
||||
|
||||
function resolveStoryURL() {
|
||||
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. Coral has inferred this story_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 default resolveStoryURL;
|
||||
@@ -0,0 +1,5 @@
|
||||
export default function getLocationOrigin() {
|
||||
return (
|
||||
location.origin || `${window.location.protocol}//${window.location.host}`
|
||||
);
|
||||
}
|
||||
@@ -3,3 +3,5 @@ export { default as parseURL } from "./parseURL";
|
||||
export { default as modifyQuery } from "./modifyQuery";
|
||||
export { default as areWeInIframe } from "./areWeInIframe";
|
||||
export { default as parseHashQuery } from "./parseHashQuery";
|
||||
export { default as getLocationOrigin } from "./getLocationOrigin";
|
||||
export { default as jsonp } from "./jsonp";
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Initiate a jsonp request.
|
||||
* @argument endpoint jsonp endpoint
|
||||
* @argument callback name of global callback to receive response
|
||||
* @argument args args to send along the jsonp request.
|
||||
*/
|
||||
function jsonp(
|
||||
endpoint: string,
|
||||
callback: string,
|
||||
args: Record<string, string | number | null | undefined>
|
||||
) {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${endpoint}?callback=${callback}`;
|
||||
Object.keys(args).forEach(key => {
|
||||
let val = "";
|
||||
if (args[key] === undefined) {
|
||||
return;
|
||||
}
|
||||
if (typeof args[key] === "string") {
|
||||
val = args[key] as string;
|
||||
} else {
|
||||
val = JSON.stringify(args[key]);
|
||||
}
|
||||
script.src += `&${key}=${encodeURIComponent(val)}`;
|
||||
});
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
export default jsonp;
|
||||
@@ -36,6 +36,7 @@ import SortMenu from "./SortMenu";
|
||||
import StoryClosedTimeoutContainer from "./StoryClosedTimeout";
|
||||
import styles from "./StreamContainer.css";
|
||||
import { SuspendedInfoContainer } from "./SuspendedInfo/index";
|
||||
import useCommentCountEvent from "./useCommentCountEvent";
|
||||
|
||||
interface Props {
|
||||
story: StoryData;
|
||||
@@ -96,6 +97,9 @@ export const StreamContainer: FunctionComponent<Props> = props => {
|
||||
const allCommentsCount = props.story.commentCounts.totalPublished;
|
||||
const featuredCommentsCount = props.story.commentCounts.tags.FEATURED;
|
||||
|
||||
// Emit comment count event.
|
||||
useCommentCountEvent(props.story.id, props.story.url, allCommentsCount);
|
||||
|
||||
useEffect(() => {
|
||||
// If the comment tab is still in its uninitialized state, "NONE", then we
|
||||
// should evaluate that based on the featuredCommentsCount if we should show
|
||||
@@ -221,6 +225,8 @@ const enhanced = withFragmentContainer<Props>({
|
||||
...StoryClosedTimeoutContainer_story
|
||||
...CreateCommentReplyMutation_story
|
||||
...CreateCommentMutation_story
|
||||
id
|
||||
url
|
||||
commentCounts {
|
||||
totalPublished
|
||||
tags {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useEffectWhenChanged } from "coral-framework/hooks";
|
||||
import { useCoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { getMessage } from "coral-framework/lib/i18n";
|
||||
|
||||
/**
|
||||
* useCommentCountEvent is a React hook that will
|
||||
* emit `commentCount` events.
|
||||
* @param storyID story id of the comment count
|
||||
* @param storyURL story url of the comment count
|
||||
* @param commentCount number of total published comments
|
||||
*/
|
||||
function useCommentCountEvent(
|
||||
storyID: string,
|
||||
storyURL: string,
|
||||
commentCount: number
|
||||
) {
|
||||
const { eventEmitter, localeBundles } = useCoralContext();
|
||||
const callback = () => {
|
||||
eventEmitter.emit("commentCount", {
|
||||
number: commentCount,
|
||||
text: getMessage(localeBundles, "comment-count-text", "Comment", {
|
||||
count: commentCount,
|
||||
}),
|
||||
storyID,
|
||||
storyURL,
|
||||
});
|
||||
};
|
||||
useEffect(callback, []);
|
||||
useEffectWhenChanged(callback, [
|
||||
eventEmitter,
|
||||
commentCount,
|
||||
localeBundles,
|
||||
storyID,
|
||||
storyURL,
|
||||
]);
|
||||
}
|
||||
|
||||
export default useCommentCountEvent;
|
||||
+1
-1
@@ -320,7 +320,7 @@ const ChangeUsernameContainer: FunctionComponent<Props> = ({
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="profile-changeUsername-submit-button-save">
|
||||
<Localized id="profile-changeUsername-save">
|
||||
<Button
|
||||
className={CLASSES.myUsername.form.saveButton}
|
||||
variant={pristine || invalid ? "outlined" : "filled"}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { pureMerge } from "coral-common/utils";
|
||||
import { GQLResolver } from "coral-framework/schema";
|
||||
import {
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
} from "coral-framework/testHelpers";
|
||||
|
||||
import { commenters, settings, stories } from "../../fixtures";
|
||||
import create from "./create";
|
||||
|
||||
const story = stories[0];
|
||||
const viewer = commenters[0];
|
||||
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer, context } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
settings: () => settings,
|
||||
viewer: () => viewer,
|
||||
story: () => story,
|
||||
},
|
||||
}),
|
||||
params.resolvers
|
||||
),
|
||||
initLocalState: (localRecord, source, environment) => {
|
||||
localRecord.setValue(story.id, "storyID");
|
||||
if (params.initLocalState) {
|
||||
params.initLocalState(localRecord, source, environment);
|
||||
}
|
||||
},
|
||||
});
|
||||
return {
|
||||
testRenderer,
|
||||
context,
|
||||
};
|
||||
}
|
||||
|
||||
it("emit commentCount events", done => {
|
||||
createTestRenderer().then(({ context: { eventEmitter } }) => {
|
||||
eventEmitter.on("commentCount", args => {
|
||||
expect(args).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"number": 2,
|
||||
"storyID": "story-1",
|
||||
"storyURL": "http://localhost/stories/story-1",
|
||||
"text": "Comments",
|
||||
}
|
||||
`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -123,9 +123,9 @@ exports[`renders configure 1`] = `
|
||||
<p
|
||||
className="Box-root Typography-root Typography-detail Typography-colorTextSecondary WidthLimitedDescription-root"
|
||||
>
|
||||
When enabled, the comments will be updated instantly
|
||||
as new comments and replies are submitted, instead of
|
||||
requiring a page refresh. You can disable this in the
|
||||
When enabled, the comments will be updated instantly
|
||||
as new comments and replies are submitted, instead of
|
||||
requiring a page refresh. You can disable this in the
|
||||
unusual situation of an article getting so much traffic that the comments are loading slowly.
|
||||
</p>
|
||||
</div>
|
||||
@@ -258,8 +258,8 @@ unusual situation of an article getting so much traffic that the comments are lo
|
||||
<p
|
||||
className="Box-root Typography-root Typography-detail Typography-colorTextSecondary WidthLimitedDescription-root"
|
||||
>
|
||||
Add a message to the top of the comment box for your readers.
|
||||
Use this to suggest a discussion topic, ask a question or make
|
||||
Add a message to the top of the comment box for your readers.
|
||||
Use this to suggest a discussion topic, ask a question or make
|
||||
announcements relating to the comments on this story.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -300,7 +300,7 @@ exports[`renders the empty settings pane 1`] = `
|
||||
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
|
||||
>
|
||||
An email has been sent to $email to verify your account.
|
||||
You must verify your new email address before it can be used
|
||||
You must verify your new email address before it can be used
|
||||
to sign in to your account or to receive notifications.
|
||||
</p>
|
||||
<button
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"coral-account/*": ["./account/*"],
|
||||
"coral-admin/*": ["./admin/*"],
|
||||
"coral-auth/*": ["./auth/*"],
|
||||
"coral-count/*": ["./count/*"],
|
||||
"coral-embed/*": ["./embed/*"],
|
||||
"coral-stream/*": ["./stream/*"],
|
||||
"coral-framework/*": ["./framework/*"],
|
||||
|
||||
Reference in New Issue
Block a user