diff --git a/desktop/flow-typed/npm/electron_v22.0.0.js b/desktop/flow-typed/npm/electron_v22.0.0.js --- a/desktop/flow-typed/npm/electron_v22.0.0.js +++ b/desktop/flow-typed/npm/electron_v22.0.0.js @@ -297,6 +297,75 @@ ): Menu; } + declare export var pushNotifications: PushNotifications; + declare class PushNotifications { + registerForAPNSNotifications(): Promise; + unregisterForAPNSNotifications(): void; + + on>( + event: T, + listener: $ElementType, + ): void; + removeListener>( + event: T, + listener: $ElementType, + ): void; + } + declare type PushNotificationsEvents = { + 'received-apns-notification': ( + event: Event, + userInfo: { +[string]: mixed }, + ) => void, + }; + + declare export class Notification { + constructor(options?: { + +title?: string, + +subtitle?: string, + +body?: string, + +silent?: boolean, + +hasReply?: boolean, + +timeoutType?: 'default' | 'never', + +replyPlaceholder?: string, + +sound?: string, + +urgency?: 'normal' | 'critical' | 'low', + +actions?: NotificationAction[], + +closeButtonText?: string, + +toastXml?: string, + }): void; + static isSupported(): boolean; + show(): void; + close(): void; + +title: string; + +subtitle: string; + +body: string; + +replyPlaceholder: string; + +sound: string; + +closeButtonText: string; + +silent: boolean; + +hasReply: boolean; + +urgency: 'normal' | 'critical' | 'low'; + +timeoutType: 'default' | 'never'; + +toastXml: string; + + on>( + event: T, + listener: $ElementType, + ): void; + removeListener>( + event: T, + listener: $ElementType, + ): void; + } + declare type NotificationEvents = { + show: (event: Event) => void, + click: (event: Event) => void, + close: (event: Event) => void, + reply: (event: Event, reply: string) => void, + action: (event: Event, index: number) => void, + failed: (event: Event, error: string) => void, + }; + declare export var shell: Shell; declare type Shell = { openExternal( @@ -402,6 +471,8 @@ ipcMain, systemPreferences, autoUpdater, + pushNotifications, + Notification, } from 'electron'; } diff --git a/desktop/src/main.js b/desktop/src/main.js --- a/desktop/src/main.js +++ b/desktop/src/main.js @@ -15,6 +15,10 @@ import { initAutoUpdate } from './auto-update.js'; import { handleSquirrelEvent } from './handle-squirrel-event.js'; +import { + listenForNotifications, + registerForNotifications, +} from './push-notifications.js'; const isDev = process.env.ENV === 'dev'; const url = isDev ? 'http://localhost:3000/comm/' : 'https://web.comm.app'; @@ -83,7 +87,8 @@ Menu.setApplicationMenu(menu); }; -const createMainWindow = () => { +let mainWindow = null; +const createMainWindow = (urlPath?: string) => { const win = new BrowserWindow({ show: false, width: 1300, @@ -149,6 +154,7 @@ autoUpdater.on('update-downloaded', updateDownloaded); win.on('closed', () => { + mainWindow = null; ipcMain.removeListener('clear-history', clearHistory); ipcMain.removeListener('double-click-top-bar', doubleClickTopBar); autoUpdater.removeListener('update-downloaded', updateDownloaded); @@ -165,7 +171,8 @@ win.webContents.insertCSS(css); })(); - win.loadURL(url); + win.loadURL(url + (urlPath ?? '')); + mainWindow = win; return win; }; @@ -204,10 +211,10 @@ return win; }; -const show = () => { +const show = (urlPath?: string) => { const splash = createSplashWindow(); const error = createErrorWindow(); - const main = createMainWindow(); + const main = createMainWindow(urlPath); let loadedSuccessfully = true; main.webContents.on('did-fail-load', () => { @@ -235,6 +242,13 @@ } main.show(); + + if (app.isPackaged) { + (async () => { + const token = await registerForNotifications(); + main.webContents.send('on-device-token-registered', token); + })(); + } } }); }; @@ -252,6 +266,15 @@ } catch (error) { console.error(error); } + listenForNotifications(threadID => { + if (mainWindow) { + mainWindow.webContents.send('on-notification-clicked', { + threadID, + }); + } else { + show(`chat/thread/${threadID}/`); + } + }); } ipcMain.on('set-badge', (event, value) => { diff --git a/desktop/src/push-notifications.js b/desktop/src/push-notifications.js new file mode 100644 --- /dev/null +++ b/desktop/src/push-notifications.js @@ -0,0 +1,49 @@ +// @flow + +// eslint-disable-next-line import/extensions +import { pushNotifications, Notification } from 'electron/main'; + +async function registerForNotifications(): Promise { + if (process.platform !== 'darwin') { + return null; + } + + try { + const token = await pushNotifications.registerForAPNSNotifications(); + return token; + } catch (err) { + console.error(err); + } + + return null; +} + +function listenForNotifications(handleClick: (threadID: string) => void) { + if (process.platform !== 'darwin') { + return; + } + pushNotifications.on('received-apns-notification', (event, userInfo) => { + if ( + typeof userInfo.title !== 'string' || + typeof userInfo.body !== 'string' || + typeof userInfo.threadID !== 'string' + ) { + console.error( + 'Notification must contain a string title, body and threadID', + ); + return; + } + const { title, body, threadID } = userInfo; + + const notif = new Notification({ + title, + body, + }); + notif.on('click', () => { + handleClick(threadID); + }); + notif.show(); + }); +} + +export { listenForNotifications, registerForNotifications };