Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All : Feature: plugin postMessage to webview #5569

Merged
merged 4 commits into from
Nov 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/app-cli/tests/support/plugins/post_messages/src/index.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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