diff --git a/packages/app-cli/tests/support/plugins/post_messages/src/index.ts b/packages/app-cli/tests/support/plugins/post_messages/src/index.ts index 5e206880dc1..2486953b733 100644 --- a/packages/app-cli/tests/support/plugins/post_messages/src/index.ts +++ b/packages/app-cli/tests/support/plugins/post_messages/src/index.ts @@ -50,6 +50,20 @@ async function setupWebviewPanel() { console.info('PostMessagePlugin (Webview): Responding with:', response); return response; }); + + panels.show(view, true); + + var intervalID = setInterval( + () => { + console.info('check if webview is ready...'); + if(panels.visible(view)) { + console.info('plugin: sending message to webview. '); + panels.postMessage(view, 'testingPluginMessage'); + } + clearInterval(intervalID); + } + , 500 + ); } joplin.plugins.register({ diff --git a/packages/app-cli/tests/support/plugins/post_messages/src/webview.js b/packages/app-cli/tests/support/plugins/post_messages/src/webview.js index fde9ed56f80..aaf4582c1b8 100644 --- a/packages/app-cli/tests/support/plugins/post_messages/src/webview.js +++ b/packages/app-cli/tests/support/plugins/post_messages/src/webview.js @@ -5,6 +5,10 @@ document.addEventListener('click', async (event) => { console.info('webview.js: sending message'); const response = await webviewApi.postMessage('testingWebviewMessage'); - console.info('webiew.js: got response:', response); + console.info('webview.js: got response:', response); } -}) \ No newline at end of file +}) + +console.info('webview.js: registering message listener'); +webviewApi.onMessage((message) => console.info('webview.js: got message:', message)); + diff --git a/packages/app-desktop/services/plugins/UserWebviewIndex.js b/packages/app-desktop/services/plugins/UserWebviewIndex.js index a0931d98dac..f6841d004a5 100644 --- a/packages/app-desktop/services/plugins/UserWebviewIndex.js +++ b/packages/app-desktop/services/plugins/UserWebviewIndex.js @@ -1,5 +1,6 @@ // This is the API that JS files loaded from the webview can see const webviewApiPromises_ = {}; +let viewMessageHandler_ = () => {}; // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars const webviewApi = { @@ -22,6 +23,13 @@ const webviewApi = { return promise; }, + + onMessage: function(viewMessageHandler) { + viewMessageHandler_ = viewMessageHandler; + window.postMessage({ + target: 'postMessageService.registerViewMessageHandler', + }); + }, }; (function() { @@ -117,7 +125,7 @@ const webviewApi = { const message = event.message; const promise = webviewApiPromises_[message.responseId]; if (!promise) { - console.warn('postMessageService.response: could not find callback for message', message); + console.warn('postMessageService.response: Could not find recorded promise to process message response', message); return; } @@ -127,8 +135,17 @@ const webviewApi = { promise.resolve(message.response); } }, + + 'postMessageService.plugin_message': (message) => { + if (!viewMessageHandler_) { + console.warn('postMessageService.plugin_message: Could not process message because no onMessage handler was defined', message); + return; + } + viewMessageHandler_(message); + }, }; + // respond to window.postMessage({}) window.addEventListener('message', ((event) => { if (!event.data || event.data.target !== 'webview') return; diff --git a/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts b/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts index db59462a9b1..a75f3804933 100644 --- a/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts +++ b/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts @@ -16,13 +16,22 @@ export default function(frameWindow: any, isReady: boolean, pluginId: string, vi if (!frameWindow) return () => {}; function onMessage_(event: any) { - if (!event.data || event.data.target !== 'postMessageService.message') return; - void PostMessageService.instance().postMessage({ - pluginId, - viewId, - ...event.data.message, - }); + if (!event.data || !event.data.target) { + return; + } + + if (event.data.target === 'postMessageService.registerViewMessageHandler') { + PostMessageService.instance().registerViewMessageHandler(ResponderComponentType.UserWebview, viewId, (message: MessageResponse) => { + postMessage('postMessageService.plugin_message', { message }); + }); + } else if (event.data.target === 'postMessageService.message') { + void PostMessageService.instance().postMessage({ + pluginId, + viewId, + ...event.data.message, + }); + } } frameWindow.addEventListener('message', onMessage_); diff --git a/packages/lib/services/PostMessageService.ts b/packages/lib/services/PostMessageService.ts index b2961d9496f..1c5a8541f05 100644 --- a/packages/lib/services/PostMessageService.ts +++ b/packages/lib/services/PostMessageService.ts @@ -27,7 +27,7 @@ import PluginService from './plugins/PluginService'; const logger = Logger.create('PostMessageService'); -enum MessageParticipant { +export enum MessageParticipant { ContentScript = 'contentScript', Plugin = 'plugin', UserWebview = 'userWebview', @@ -46,6 +46,8 @@ export interface MessageResponse { type MessageResponder = (message: MessageResponse)=> void; +type ViewMessageHandler = (message: any)=> void; + interface Message { pluginId: string; contentScriptId: string; @@ -60,6 +62,7 @@ export default class PostMessageService { private static instance_: PostMessageService; private responders_: Record = {}; + private viewMessageHandlers_: Record = {}; public static instance(): PostMessageService { if (this.instance_) return this.instance_; @@ -68,26 +71,26 @@ export default class PostMessageService { } public async postMessage(message: Message) { - logger.debug('postMessage:', message); let response = null; let error = null; + if (message.from === MessageParticipant.Plugin && message.to === MessageParticipant.UserWebview) { + this.viewMessageHandler(message); + return; + } + try { if (message.from === MessageParticipant.ContentScript && message.to === MessageParticipant.Plugin) { - const pluginId = PluginService.instance().pluginIdByContentScriptId(message.contentScriptId); if (!pluginId) throw new Error(`Could not find plugin associated with content script "${message.contentScriptId}"`); response = await PluginService.instance().pluginById(pluginId).emitContentScriptMessage(message.contentScriptId, message.content); } else if (message.from === MessageParticipant.UserWebview && message.to === MessageParticipant.Plugin) { - response = await PluginService.instance().pluginById(message.pluginId).viewController(message.viewId).emitMessage({ message: message.content }); } else { - throw new Error(`Unhandled message: ${JSON.stringify(message)}`); - } } catch (e) { error = e; @@ -96,8 +99,18 @@ export default class PostMessageService { this.sendResponse(message, response, error); } + private viewMessageHandler(message: Message) { + + const viewMessageHandler = this.viewMessageHandlers_[[ResponderComponentType.UserWebview, message.viewId].join(':')]; + + if (!viewMessageHandler) { + logger.warn('Cannot receive message because no viewMessageHandler was found', message); + } else { + viewMessageHandler(message.content); + } + } + private sendResponse(message: Message, responseContent: any, error: any) { - logger.debug('sendResponse', message, responseContent, error); let responder: MessageResponder = null; @@ -126,6 +139,14 @@ export default class PostMessageService { this.responders_[[type, viewId].join(':')] = responder; } + public registerViewMessageHandler(type: ResponderComponentType, viewId: string, callback: ViewMessageHandler) { + this.viewMessageHandlers_[[type, viewId].join(':')] = callback; + } + + public unregisterViewMessageHandler(type: ResponderComponentType, viewId: string) { + delete this.viewMessageHandlers_[[type, viewId].join(':')]; + } + public unregisterResponder(type: ResponderComponentType, viewId: string) { delete this.responders_[[type, viewId].join(':')]; } diff --git a/packages/lib/services/plugins/ViewController.ts b/packages/lib/services/plugins/ViewController.ts index 1a94229dfc2..08856a4e494 100644 --- a/packages/lib/services/plugins/ViewController.ts +++ b/packages/lib/services/plugins/ViewController.ts @@ -44,4 +44,8 @@ export default class ViewController { console.info('Calling ViewController.emitMessage - but not implemented', event); } + public postMessage(message: any) { + console.info('Calling ViewController.postMessage - but not implemented', message); + } + } diff --git a/packages/lib/services/plugins/WebviewController.ts b/packages/lib/services/plugins/WebviewController.ts index aca19ee100c..d504d3913af 100644 --- a/packages/lib/services/plugins/WebviewController.ts +++ b/packages/lib/services/plugins/WebviewController.ts @@ -2,6 +2,7 @@ import ViewController, { EmitMessageEvent } from './ViewController'; import shim from '../../shim'; import { ButtonSpec, DialogResult, ViewHandle } from './api/types'; const { toSystemSlashes } = require('../../path-utils'); +import PostMessageService, { MessageParticipant } from '../PostMessageService'; export enum ContainerType { Panel = 'panel', @@ -103,7 +104,24 @@ export default class WebviewController extends ViewController { }); } + public postMessage(message: any) { + + const messageId = `plugin_${Date.now()}${Math.random()}`; + + void PostMessageService.instance().postMessage({ + pluginId: this.pluginId, + viewId: this.handle, + contentScriptId: null, + from: MessageParticipant.Plugin, + to: MessageParticipant.UserWebview, + id: messageId, + content: message, + }); + + } + public async emitMessage(event: EmitMessageEvent): Promise { + if (!this.messageListener_) return; return this.messageListener_(event.message); } diff --git a/packages/lib/services/plugins/api/JoplinViewsPanels.ts b/packages/lib/services/plugins/api/JoplinViewsPanels.ts index 1711fc31067..345b8117841 100644 --- a/packages/lib/services/plugins/api/JoplinViewsPanels.ts +++ b/packages/lib/services/plugins/api/JoplinViewsPanels.ts @@ -44,14 +44,14 @@ export default class JoplinViewsPanels { /** * Sets the panel webview HTML */ - public async setHtml(handle: ViewHandle, html: string) { + public async setHtml(handle: ViewHandle, html: string): Promise { return this.controller(handle).html = html; } /** * Adds and loads a new JS or CSS files into the panel. */ - public async addScript(handle: ViewHandle, scriptPath: string) { + public async addScript(handle: ViewHandle, scriptPath: string): Promise { return this.controller(handle).addScript(scriptPath); } @@ -74,10 +74,30 @@ export default class JoplinViewsPanels { * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details. * */ - public async onMessage(handle: ViewHandle, callback: Function) { + public async onMessage(handle: ViewHandle, callback: Function): Promise { return this.controller(handle).onMessage(callback); } + /** + * Sends a message to the webview. + * + * The webview must have registered a message handler prior, otherwise the message is ignored. Use; + * + * ```javascript + * webviewApi.onMessage((message) => { ... }); + * ``` + * + * - `message` can be any JavaScript object, string or number + * + * The view API may have only one onMessage handler defined. + * This method is fire and forget so no response is returned. + * + * It is particularly useful when the webview needs to react to events emitted by the plugin or the joplin api. + */ + public postMessage(handle: ViewHandle, message: any): void { + return this.controller(handle).postMessage(message); + } + /** * Shows the panel */