Skip to content

Commit

Permalink
Plugins: Allow posting messages from plugin to webview (laurent22#5569)
Browse files Browse the repository at this point in the history
  • Loading branch information
agerardin committed Nov 9, 2021
1 parent 200ba85 commit 6b31609
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 19 deletions.
14 changes: 14 additions & 0 deletions packages/app-cli/tests/support/plugins/post_messages/src/index.ts
Expand Up @@ -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({
Expand Down
Expand Up @@ -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);
}
})
})

console.info('webview.js: registering message listener');
webviewApi.onMessage((message) => console.info('webview.js: got message:', message));

19 changes: 18 additions & 1 deletion 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 = {
Expand All @@ -22,6 +23,13 @@ const webviewApi = {

return promise;
},

onMessage: function(viewMessageHandler) {
viewMessageHandler_ = viewMessageHandler;
window.postMessage({
target: 'postMessageService.registerViewMessageHandler',
});
},
};

(function() {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;

Expand Down
Expand Up @@ -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_);
Expand Down
35 changes: 28 additions & 7 deletions packages/lib/services/PostMessageService.ts
Expand Up @@ -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',
Expand All @@ -46,6 +46,8 @@ export interface MessageResponse {

type MessageResponder = (message: MessageResponse)=> void;

type ViewMessageHandler = (message: any)=> void;

interface Message {
pluginId: string;
contentScriptId: string;
Expand All @@ -60,6 +62,7 @@ export default class PostMessageService {

private static instance_: PostMessageService;
private responders_: Record<string, MessageResponder> = {};
private viewMessageHandlers_: Record<string, ViewMessageHandler> = {};

public static instance(): PostMessageService {
if (this.instance_) return this.instance_;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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(':')];
}
Expand Down
4 changes: 4 additions & 0 deletions packages/lib/services/plugins/ViewController.ts
Expand Up @@ -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);
}

}
18 changes: 18 additions & 0 deletions packages/lib/services/plugins/WebviewController.ts
Expand Up @@ -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',
Expand Down Expand Up @@ -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<any> {

if (!this.messageListener_) return;
return this.messageListener_(event.message);
}
Expand Down
26 changes: 23 additions & 3 deletions packages/lib/services/plugins/api/JoplinViewsPanels.ts
Expand Up @@ -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<string> {
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<void> {
return this.controller(handle).addScript(scriptPath);
}

Expand All @@ -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<void> {
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
*/
Expand Down

0 comments on commit 6b31609

Please sign in to comment.