From ea02e47241eaa1f3d0af3def4f5fe559eae09707 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Fri, 10 Aug 2018 15:57:07 +0200 Subject: [PATCH] Add PostMessage Service --- .../framework/lib/bootstrap/TalkContext.tsx | 4 ++ .../framework/lib/bootstrap/createContext.tsx | 2 + .../client/framework/lib/postMessage.spec.ts | 69 +++++++++++++++++++ src/core/client/framework/lib/postMessage.ts | 66 ++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 src/core/client/framework/lib/postMessage.spec.ts create mode 100644 src/core/client/framework/lib/postMessage.ts diff --git a/src/core/client/framework/lib/bootstrap/TalkContext.tsx b/src/core/client/framework/lib/bootstrap/TalkContext.tsx index 75ddc974c..f1d62c81c 100644 --- a/src/core/client/framework/lib/bootstrap/TalkContext.tsx +++ b/src/core/client/framework/lib/bootstrap/TalkContext.tsx @@ -6,6 +6,7 @@ import { MediaQueryMatchers } from "react-responsive"; import { Formatter } from "react-timeago"; import { Environment } from "relay-runtime"; +import { PostMessageService } from "talk-framework/lib/postMessage"; import { UIContext } from "talk-ui/components"; import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside"; @@ -22,6 +23,9 @@ export interface TalkContext { /** media query values for testing purposes */ mediaQueryValues?: MediaQueryMatchers; + /** postMessage service */ + postMessage: PostMessageService; + /** * A way to listen for clicks that are e.g. outside of the * current frame for `ClickOutside` diff --git a/src/core/client/framework/lib/bootstrap/createContext.tsx b/src/core/client/framework/lib/bootstrap/createContext.tsx index 2e6bd2aff..91470277a 100644 --- a/src/core/client/framework/lib/bootstrap/createContext.tsx +++ b/src/core/client/framework/lib/bootstrap/createContext.tsx @@ -10,6 +10,7 @@ import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside"; import { generateMessages, LocalesData, negotiateLanguages } from "../i18n"; import { fetchQuery } from "../network"; +import { PostMessageService } from "../postMessage"; import { TalkContext } from "./TalkContext"; interface CreateContextArguments { @@ -99,6 +100,7 @@ export default async function createContext({ pym, eventEmitter, registerClickFarAway, + postMessage: new PostMessageService(), }; // Run custom initializations. diff --git a/src/core/client/framework/lib/postMessage.spec.ts b/src/core/client/framework/lib/postMessage.spec.ts new file mode 100644 index 000000000..1c8a61a1f --- /dev/null +++ b/src/core/client/framework/lib/postMessage.spec.ts @@ -0,0 +1,69 @@ +import { PostMessageService } from "./postMessage"; + +it("post and subscribe to a message", done => { + const postMessage = new PostMessageService(); + const cancel = postMessage.on("test", value => { + expect(value).toBe("value"); + done(); + cancel(); + }); + postMessage.send("test", "value", window); +}); + +it("send to a different origin", done => { + const postMessage = new PostMessageService(); + const cancelA = postMessage.on("testA", value => { + throw new Error("Should not reach this"); + }); + const cancelB = postMessage.on("testB", value => { + done(); + cancelA(); + cancelB(); + }); + postMessage.send("testA", "value", window, "http://i-do-not-exist.de"); + postMessage.send("testB", "value", window); +}); + +it("should cancel", done => { + const postMessage = new PostMessageService(); + const cancelA = postMessage.on("testA", value => { + throw new Error("Should not reach this"); + }); + const cancelB = postMessage.on("testB", value => { + done(); + cancelB(); + }); + cancelA(); + postMessage.send("testA", "value", window); + postMessage.send("testB", "value", window); +}); + +it("different scopes are isolated", done => { + const postMessageA = new PostMessageService("scopeA"); + const postMessageB = new PostMessageService("scopeB"); + const cancelA = postMessageA.on("testA", value => { + throw new Error("Should not reach this"); + }); + const cancelB = postMessageA.on("testB", value => { + done(); + cancelA(); + cancelB(); + }); + postMessageB.send("testA", "value", window); + postMessageA.send("testB", "value", window); +}); + +it("different message names are isolated", done => { + const postMessage = new PostMessageService(); + const cancelA = postMessage.on("testA", value => { + expect(value).toBe("valueA"); + }); + const cancelB = postMessage.on("testB", value => { + expect(value).toBe("valueB"); + done(); + cancelA(); + cancelB(); + }); + postMessage.send("testA", "valueA", window); + postMessage.send("testB", "valueB", window); +}); diff --git a/src/core/client/framework/lib/postMessage.ts b/src/core/client/framework/lib/postMessage.ts new file mode 100644 index 000000000..70523cd5a --- /dev/null +++ b/src/core/client/framework/lib/postMessage.ts @@ -0,0 +1,66 @@ +type PostMessageHandler = (value: any, name: string) => void; + +/** + * Wrapper around the HTML postMessage API. + */ +export class PostMessageService { + private origin: string; + private scope: string; + + constructor( + scope = "talk", + origin: string = `${location.protocol}//${location.host}` + ) { + this.origin = origin; + this.scope = scope; + } + + /** + * Send a message over the postMessage API + * @param name string name of the message + * @param value string value of the message + * @param target Window target window, e.g. window.opener + * @param targetOrigin string origin of target + */ + public send(name: string, value: any, target: Window, targetOrigin?: string) { + if (!target) { + return; + } + + // Serialize the message to be sent via postMessage. + const msg = { name, value, scope: this.scope }; + + // Send the message. + target.postMessage(msg, targetOrigin || this.origin); + } + + /** + * Subscribe to messages + * @param name string Name of the message + * @param handler PostMessageHandler + */ + public on(name: string, handler: PostMessageHandler) { + const listener = (event: MessageEvent) => { + if (!event.origin) { + if (process.env.NODE_ENV !== "test") { + // tslint:disable-next-line:no-console + console.warn("empty origin received in postMessage", name); + } + } else if (event.origin !== this.origin) { + return; + } + if (event.data.scope !== this.scope) { + return; + } + if (event.data.name !== name) { + return; + } + handler(event.data.value, event.data.name); + }; + // Attach the listener to the target. + window.addEventListener("message", listener); + return () => { + window.removeEventListener("message", listener); + }; + } +}