From 65903dd057269b69ca88fdbc02ac0e267638f547 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 22 Sep 2020 11:32:04 -0300 Subject: [PATCH 1/2] Move injected script --- rollup.config.js | 24 +++++++++++++++ src/app/main/dev.ts | 5 +++- src/injected.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++ src/preload.ts | 73 ++++++--------------------------------------- 4 files changed, 110 insertions(+), 65 deletions(-) create mode 100644 src/injected.ts diff --git a/rollup.config.js b/rollup.config.js index d56cdc2583..9ac67a9fa2 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -101,6 +101,30 @@ export default [ }, ], }, + { + input: 'src/injected.ts', + plugins: [ + json(), + replace({ + 'process.env.NODE_ENV': JSON.stringify(NODE_ENV), + }), + typescript({ noEmitOnError: false }), + babel({ + babelHelpers: 'bundled', + }), + nodeResolve({ + browser: true, + }), + commonjs(), + ], + output: [ + { + dir: 'app', + format: 'iife', + sourcemap: 'inline', + }, + ], + }, { external: [ ...builtinModules, diff --git a/src/app/main/dev.ts b/src/app/main/dev.ts index 3ef92d3420..d54340e0eb 100644 --- a/src/app/main/dev.ts +++ b/src/app/main/dev.ts @@ -26,7 +26,10 @@ export const setupRootWindowReload = async (webContents: WebContents): Promise => { const chokidar = await import('chokidar'); - chokidar.watch(path.join(app.getAppPath(), 'app/preload.js'), { + chokidar.watch([ + path.join(app.getAppPath(), 'app/preload.js'), + path.join(app.getAppPath(), 'app/injected.js'), + ], { awaitWriteFinish: true, }).on('change', () => { if (webContents.isDestroyed()) { diff --git a/src/injected.ts b/src/injected.ts new file mode 100644 index 0000000000..851f19b76f --- /dev/null +++ b/src/injected.ts @@ -0,0 +1,73 @@ +import { RocketChatDesktopAPI } from './servers/preload/api'; + +declare global { + interface Window { + RocketChatDesktop: RocketChatDesktopAPI; + } +} + +const start = (): void => { + if (typeof window.require !== 'function') { + return; + } + + const { Info: serverInfo = {} } = window.require('/app/utils/rocketchat.info') ?? {}; + + if (!serverInfo.version) { + return; + } + + window.RocketChatDesktop.setServerInfo(serverInfo); + + const { Meteor } = window.require('meteor/meteor'); + const { Session } = window.require('meteor/session'); + const { Tracker } = window.require('meteor/tracker'); + const { UserPresence } = window.require('meteor/konecty:user-presence'); + const { settings } = window.require('/app/settings'); + const { getUserPreference } = window.require('/app/utils'); + + window.RocketChatDesktop.setUrlResolver(Meteor.absoluteUrl); + + Tracker.autorun(() => { + const unread = Session.get('unread'); + window.RocketChatDesktop.setBadge(unread); + }); + + Tracker.autorun(() => { + const { url, defaultUrl } = settings.get('Assets_favicon') || {}; + window.RocketChatDesktop.setFavicon(url || defaultUrl); + }); + + Tracker.autorun(() => { + const { url, defaultUrl } = settings.get('Assets_background') || {}; + window.RocketChatDesktop.setBackground(url || defaultUrl); + }); + + Tracker.autorun(() => { + const siteName = settings.get('Site_Name'); + window.RocketChatDesktop.setTitle(siteName); + }); + + Tracker.autorun(() => { + const uid = Meteor.userId(); + const isAutoAwayEnabled = getUserPreference(uid, 'enableAutoAway'); + const idleThreshold = getUserPreference(uid, 'idleTimeLimit'); + + if (isAutoAwayEnabled) { + delete UserPresence.awayTime; + UserPresence.start(); + } + + window.RocketChatDesktop.setUserPresenceDetection({ + isAutoAwayEnabled, + idleThreshold, + setUserOnline: (online) => { + Meteor.call('UserPresence:setDefaultStatus', online ? 'online' : 'away'); + }, + }); + }); + + window.Notification = window.RocketChatDesktop.Notification; +}; + +start(); diff --git a/src/preload.ts b/src/preload.ts index fa59399592..99676dca29 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,3 +1,6 @@ +import fs from 'fs'; +import path from 'path'; + import { contextBridge, ipcRenderer, webFrame } from 'electron'; import { satisfies, coerce } from 'semver'; @@ -8,7 +11,7 @@ import { listenToScreenSharingRequests } from './screenSharing/preload'; import { RocketChatDesktop, RocketChatDesktopAPI, serverInfo } from './servers/preload/api'; import { setServerUrl } from './servers/preload/urls'; import { setupSpellChecking } from './spellChecking/preload'; -import { createRendererReduxStore } from './store'; +import { createRendererReduxStore, select } from './store'; import { listenToMessageBoxEvents } from './ui/preload/messageBox'; import { handleTrafficLightsSpacing } from './ui/preload/sidebar'; import { listenToUserPresenceChanges } from './userPresence/preload'; @@ -35,69 +38,11 @@ const start = async (): Promise => { setupRendererErrorHandling('webviewPreload'); setupSpellChecking(); - await webFrame.executeJavaScript(`(() => { - if (typeof window.require !== 'function') { - return; - } - - const { Info: serverInfo = {} } = window.require('/app/utils/rocketchat.info') ?? {}; - - if (!serverInfo.version) { - return; - } - - window.RocketChatDesktop.setServerInfo(serverInfo); - - const { Meteor } = window.require('meteor/meteor'); - const { Session } = window.require('meteor/session'); - const { Tracker } = window.require('meteor/tracker'); - const { UserPresence } = window.require('meteor/konecty:user-presence'); - const { settings } = window.require('/app/settings'); - const { getUserPreference } = window.require('/app/utils'); - - window.RocketChatDesktop.setUrlResolver(Meteor.absoluteUrl); - - Tracker.autorun(() => { - const unread = Session.get('unread'); - window.RocketChatDesktop.setBadge(unread); - }); - - Tracker.autorun(() => { - const { url, defaultUrl } = settings.get('Assets_favicon') || {}; - window.RocketChatDesktop.setFavicon(url || defaultUrl); - }); - - Tracker.autorun(() => { - const { url, defaultUrl } = settings.get('Assets_background') || {}; - window.RocketChatDesktop.setBackground(url || defaultUrl); - }); - - Tracker.autorun(() => { - const siteName = settings.get('Site_Name'); - window.RocketChatDesktop.setTitle(siteName); - }); - - Tracker.autorun(() => { - const uid = Meteor.userId(); - const isAutoAwayEnabled = getUserPreference(uid, 'enableAutoAway'); - const idleThreshold = getUserPreference(uid, 'idleTimeLimit'); - - if (isAutoAwayEnabled) { - delete UserPresence.awayTime; - UserPresence.start(); - } - - window.RocketChatDesktop.setUserPresenceDetection({ - isAutoAwayEnabled, - idleThreshold, - setUserOnline: (online) => { - Meteor.call('UserPresence:setDefaultStatus', online ? 'online' : 'away'); - }, - }); - }); - - window.Notification = window.RocketChatDesktop.Notification; - })()`); + const injectedCode = await fs.promises.readFile( + path.join(select(({ appPath }) => appPath), 'app/injected.js'), + 'utf8', + ); + await webFrame.executeJavaScript(injectedCode); if (!satisfies(coerce(serverInfo?.version), '>=3.0.x')) { return; From de9627b9c15ae87c07b57698304fb008d3f32531 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 22 Sep 2020 16:03:06 -0300 Subject: [PATCH 2/2] Inject functions and callbacks only through the context bridge --- src/injected.ts | 105 +++++++++++++++++++++- src/notifications/preload.ts | 165 +++++++++-------------------------- src/servers/preload/api.ts | 12 ++- 3 files changed, 156 insertions(+), 126 deletions(-) diff --git a/src/injected.ts b/src/injected.ts index 851f19b76f..250b8e2d19 100644 --- a/src/injected.ts +++ b/src/injected.ts @@ -67,7 +67,110 @@ const start = (): void => { }); }); - window.Notification = window.RocketChatDesktop.Notification; + const destroyPromiseSymbol = Symbol('destroyPromise'); + + window.Notification = class RocketChatDesktopNotification extends EventTarget implements Notification { + static readonly permission: NotificationPermission = 'granted'; + + static readonly maxActions: number = process.platform === 'darwin' ? Number.MAX_SAFE_INTEGER : 0; + + static requestPermission(): Promise { + return Promise.resolve(RocketChatDesktopNotification.permission); + } + + [destroyPromiseSymbol]: Promise<() => void> + + constructor(title: string, options: (NotificationOptions & { canReply?: boolean }) = {}) { + super(); + + for (const eventType of ['show', 'close', 'click', 'reply', 'action']) { + const propertyName = `on${ eventType }`; + const propertySymbol = Symbol(propertyName); + + Object.defineProperty(this, propertyName, { + get: () => this[propertySymbol], + set: (value) => { + if (this[propertySymbol]) { + this.removeEventListener(eventType, this[propertySymbol]); + } + + this[propertySymbol] = value; + + if (this[propertySymbol]) { + this.addEventListener(eventType, this[propertySymbol]); + } + }, + }); + } + + this[destroyPromiseSymbol] = window.RocketChatDesktop.createNotification({ + title, + ...options, + onEvent: this.handleEvent, + }).then((id) => () => { + window.RocketChatDesktop.destroyNotification(id); + }); + + Object.assign(this, { title, ...options }); + } + + handleEvent = ({ type, detail }: CustomEvent): void => { + const mainWorldEvent = new CustomEvent(type, { detail }); + if (type === 'reply') { + (mainWorldEvent as any).response = detail?.reply; + } + this.dispatchEvent(mainWorldEvent); + } + + actions: NotificationAction[]; + + badge: string; + + body: string; + + data: any; + + dir: NotificationDirection; + + icon: string; + + image: string; + + lang: string; + + onclick: (this: Notification, ev: Event) => any; + + onclose: (this: Notification, ev: Event) => any; + + onerror: (this: Notification, ev: Event) => any; + + onshow: (this: Notification, ev: Event) => any; + + renotify: boolean; + + requireInteraction: boolean; + + silent: boolean; + + tag: string; + + timestamp: number; + + title: string; + + vibrate: readonly number[]; + + close(): void { + if (!this[destroyPromiseSymbol]) { + return; + } + + this[destroyPromiseSymbol].then((destroy) => { + delete this[destroyPromiseSymbol]; + destroy(); + }); + } + }; }; start(); diff --git a/src/notifications/preload.ts b/src/notifications/preload.ts index 07a68d8c3a..c20e2704d9 100644 --- a/src/notifications/preload.ts +++ b/src/notifications/preload.ts @@ -24,144 +24,68 @@ const normalizeIconUrl = (iconUrl: string): string => { return iconUrl; }; -const notifications = new Map(); - -export class RocketChatNotification extends EventTarget implements Notification { - static readonly permission: NotificationPermission = 'granted'; - - static readonly maxActions: number = process.platform === 'darwin' ? Number.MAX_SAFE_INTEGER : 0; - - static requestPermission(): Promise { - return Promise.resolve(RocketChatNotification.permission); - } - - private _destroy: Promise<() => void>; - - constructor(title: string, { icon, ...options }: (NotificationOptions & { canReply?: boolean }) = {}) { - super(); - - for (const eventType of ['show', 'close', 'click', 'reply', 'action']) { - const propertyName = `on${ eventType }`; - const propertySymbol = Symbol(propertyName); - - Object.defineProperty(this, propertyName, { - get: () => this[propertySymbol], - set: (value) => { - if (this[propertySymbol]) { - this.removeEventListener(eventType, this[propertySymbol]); - } - - this[propertySymbol] = value; - - if (this[propertySymbol]) { - this.addEventListener(eventType, this[propertySymbol]); - } - }, - }); - } - - this._destroy = request< - typeof NOTIFICATIONS_CREATE_REQUESTED, - typeof NOTIFICATIONS_CREATE_RESPONDED - >({ - type: NOTIFICATIONS_CREATE_REQUESTED, - payload: { - title, - ...icon ? { - icon: normalizeIconUrl(icon), - } : {}, - ...options, - }, - }).then((id) => { - notifications.set(id, this); - - return () => { - dispatch({ type: NOTIFICATIONS_NOTIFICATION_DISMISSED, payload: { id } }); - notifications.delete(id); - }; - }); - - Object.assign(this, { title, icon, ...options }); - } - - actions: NotificationAction[]; - - badge: string; - - body: string; - - data: any; - - dir: NotificationDirection; - - icon: string; - - image: string; - - lang: string; - - onclick: (this: Notification, ev: Event) => any; - - onclose: (this: Notification, ev: Event) => any; - - onerror: (this: Notification, ev: Event) => any; - - onshow: (this: Notification, ev: Event) => any; - - renotify: boolean; - - requireInteraction: boolean; - - silent: boolean; - - tag: string; - - timestamp: number; - - title: string; +const eventHandlers = new Map void>(); + +export const createNotification = async ({ + title, + icon, + onEvent, + ...options +}: NotificationOptions & { + canReply?: boolean, + title: string, + onEvent: (eventDescriptor: { type: string; detail?: unknown }) => void, +}): Promise => { + const id = await request< + typeof NOTIFICATIONS_CREATE_REQUESTED, + typeof NOTIFICATIONS_CREATE_RESPONDED + >({ + type: NOTIFICATIONS_CREATE_REQUESTED, + payload: { + title, + ...icon ? { + icon: normalizeIconUrl(icon), + } : {}, + ...options, + }, + }); - vibrate: readonly number[]; + eventHandlers.set(id, (event) => onEvent({ type: event.type, detail: event.detail })); - close(): void { - if (!this._destroy) { - return; - } + return id; +}; - this._destroy.then((destroy) => { - delete this._destroy; - destroy(); - }); - } -} +export const destroyNotification = (id: unknown): void => { + dispatch({ type: NOTIFICATIONS_NOTIFICATION_DISMISSED, payload: { id } }); + eventHandlers.delete(id); +}; export const listenToNotificationsRequests = (): void => { listen(NOTIFICATIONS_NOTIFICATION_SHOWN, (action) => { const { payload: { id } } = action; - if (!notifications.has(id)) { + if (!eventHandlers.has(id)) { return; } - const showEvent = new CustomEvent('show'); - notifications.get(id).dispatchEvent(showEvent); + eventHandlers.get(id)({ type: 'show' }); }); listen(NOTIFICATIONS_NOTIFICATION_CLOSED, (action) => { const { payload: { id } } = action; - if (!notifications.has(id)) { + if (!eventHandlers.has(id)) { return; } - const closeEvent = new CustomEvent('close'); - notifications.get(id).dispatchEvent(closeEvent); - notifications.delete(id); + eventHandlers.get(id)({ type: 'close' }); + eventHandlers.delete(id); }); listen(NOTIFICATIONS_NOTIFICATION_CLICKED, (action) => { const { payload: { id } } = action; - if (!notifications.has(id)) { + if (!eventHandlers.has(id)) { return; } @@ -172,29 +96,26 @@ export const listenToNotificationsRequests = (): void => { }, }); - const clickEvent = new CustomEvent('click'); - notifications.get(id).dispatchEvent(clickEvent); + eventHandlers.get(id)({ type: 'click' }); }); listen(NOTIFICATIONS_NOTIFICATION_REPLIED, (action) => { const { payload: { id, reply } } = action; - if (!notifications.has(id)) { + if (!eventHandlers.has(id)) { return; } - const replyEvent = new CustomEvent<{ reply: string }>('reply', { detail: { reply } }); - notifications.get(id).dispatchEvent(Object.assign(replyEvent, { response: reply })); + eventHandlers.get(id)({ type: 'reply', detail: { reply } }); }); listen(NOTIFICATIONS_NOTIFICATION_ACTIONED, (action) => { const { payload: { id, index } } = action; - if (!notifications.has(id)) { + if (!eventHandlers.has(id)) { return; } - const actionEvent = new CustomEvent<{ index: number }>('action', { detail: { index } }); - notifications.get(id).dispatchEvent(actionEvent); + eventHandlers.get(id)({ type: 'action', detail: { index } }); }); }; diff --git a/src/servers/preload/api.ts b/src/servers/preload/api.ts index d82f369eb1..16c2c6dab5 100644 --- a/src/servers/preload/api.ts +++ b/src/servers/preload/api.ts @@ -1,4 +1,4 @@ -import { RocketChatNotification } from '../../notifications/preload'; +import { createNotification, destroyNotification } from '../../notifications/preload'; import { setUserPresenceDetection } from '../../userPresence/preload'; import { Server } from '../common'; import { setBadge } from './badge'; @@ -25,7 +25,12 @@ export type RocketChatDesktopAPI = { idleThreshold: number; setUserOnline: (online: boolean) => void; }) => void; - Notification: typeof Notification; + createNotification: (options: NotificationOptions & { + canReply?: boolean, + title: string, + onEvent: (eventDescriptor: { type: string; detail: unknown }) => void, + }) => Promise; + destroyNotification: (id: unknown) => void; }; export const RocketChatDesktop: RocketChatDesktopAPI = { @@ -38,5 +43,6 @@ export const RocketChatDesktop: RocketChatDesktopAPI = { setBackground, setTitle, setUserPresenceDetection, - Notification: RocketChatNotification, + createNotification, + destroyNotification, };