diff --git a/desktop/flow-typed/npm/electron_v22.0.0.js b/desktop/flow-typed/npm/electron_v22.0.0.js index d8be703a9..1320e8bdb 100644 --- a/desktop/flow-typed/npm/electron_v22.0.0.js +++ b/desktop/flow-typed/npm/electron_v22.0.0.js @@ -1,513 +1,513 @@ // @flow // flow-typed signature: f8bfa3876f1890f644b65b1ebd801ed8 // flow-typed version: <>/electron_v22.0.0/flow_v0.182.0 declare module 'electron' { declare export var app: App; declare type App = { quit(): void, whenReady(): Promise, hide(): void, show(): void, setName: (name: string) => void, setAppUserModelId: (id: string) => void, getVersion: () => string, dock: Dock, isPackaged: boolean, name: string, on: >( event: T, listener: $ElementType, ) => void, removeListener: >( event: T, listener: $ElementType, ) => void, }; declare type AppEvents = { 'window-all-closed': () => void, 'activate': (event: Event, hasVisibleWindows: boolean) => void, 'quit': (event: Event, exitCode: number) => void, 'render-process-gone': ( event: Event, webContents: WebContents, details: { reason: string, exitCode: number, }, ) => void, 'child-process-gone': ( event: Event, details: { type: string, reason: string, exitCode: number, serviceName?: string, name?: string, }, ) => void, }; declare export class BrowserWindow { constructor(options?: { +width?: number, +height?: number, +x?: number, +y?: number, +useContentSize?: boolean, +center?: boolean, +minWidth?: number, +minHeight?: number, +maxWidth?: number, +maxHeight?: number, +resizable?: boolean, +movable?: boolean, +minimizable?: boolean, +maximizable?: boolean, +closable?: boolean, +focusable?: boolean, +alwaysOnTop?: boolean, +fullscreen?: boolean, +fullscreenable?: boolean, +simpleFullscreen?: boolean, +skipTaskbar?: boolean, +kiosk?: boolean, +title?: string, +icon?: string, +show?: boolean, +paintWhenInitiallyHidden?: boolean, +frame?: boolean, +parent?: BrowserWindow, +modal?: boolean, +acceptFirstMouse?: boolean, +disableAutoHideCursor?: boolean, +autoHideMenuBar?: boolean, +enableLargerThanScreen?: boolean, +backgroundColor?: string, +hasShadow?: boolean, +opacity?: number, +darkTheme?: boolean, +transparent?: boolean, +type?: string, +visualEffectState?: 'followWindow' | 'active' | 'inactive', +titleBarStyle?: | 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover', +trafficLightPosition?: { +x: number, +y: number }, +roundedCorners?: boolean, +fullscreenWindowTitle?: boolean, +thickFrame?: boolean, +vibrancy?: | 'appearance-based' | 'light' | 'dark' | 'titlebar' | 'selection' | 'menu' | 'popover' | 'sidebar' | 'medium-light' | 'ultra-dark' | 'header' | 'sheet' | 'window' | 'hud' | 'fullscreen-ui' | 'tooltip' | 'content' | 'under-window' | 'under-page', +zoomToPageWidth?: boolean, +tabbingIdentifier?: string, +webPreferences?: { +preload?: string, }, +titleBarOverlay?: | { +color?: string, +symbolColor?: string, +height?: number } | boolean, }): void; destroy(): void; close(): void; show(): void; hide(): void; maximize(): void; unmaximize(): void; isMaximized(): boolean; minimize(): void; loadURL( url: string, options?: { +userAgent?: string, +extraHeaders?: string, +baseURLForDataURL?: string, }, ): Promise; loadFile( filePath: string, options?: { +query?: { [string]: string }, +search?: string, +hash?: string, }, ): Promise; reload(): void; setMenu(menu: Menu | null): void; isDestroyed(): boolean; static getAllWindows(): $ReadOnlyArray; webContents: WebContents; on>( event: T, listener: $ElementType, ): void; removeListener>( event: T, listener: $ElementType, ): void; } declare type BrowserWindowEvents = { close: (event: Event) => void, closed: () => void, }; declare export type Event = { preventDefault: () => void, }; declare export var contextBridge: ContextBridge; declare type ContextBridge = { exposeInMainWorld(apiKey: string, api: mixed): void, }; declare export var autoUpdater: AutoUpdater; declare class AutoUpdater { setFeedURL(options: { +url: string, +headers?: { +[string]: string }, +serverType?: 'json' | 'default', }): void; getFeedURL(): string; checkForUpdates(): void; quitAndInstall(): void; on>( event: T, listener: $ElementType, ): void; removeListener>( event: T, listener: $ElementType, ): void; } declare type AutoUpdaterEvents = { 'error': Error => void, 'checking-for-update': () => void, 'update-available': () => void, 'update-not-available': () => void, 'update-downloaded': ( event: Event, releaseNotes?: string, releaseName: string, releaseDate?: Date, updateURL?: string, ) => void, }; declare class Dock { setBadge(text: string): void; getBadge(): string; hide(): void; show(): Promise; } declare export var ipcMain: IpcMain; declare type IpcMain = { on( channel: string, - listener: (event: IpcMainEvent, ...args: $ReadOnlyArray) => void, + listener: (event: IpcMainEvent, ...args: $ReadOnlyArray) => mixed, ): void, removeListener( channel: string, listener: (...args: $ReadOnlyArray) => void, ): void, }; declare export type IpcMainEvent = { +processId: number, +frameId: number, returnValue: mixed, +sender: WebContents, +reply: (channel: string, ...args: $ReadOnlyArray) => void, }; declare export var ipcRenderer: IpcRenderer; declare type IpcRenderer = { on( channel: string, listener: (event: IpcRendererEvent, ...args: $ReadOnlyArray) => void, ): void, removeListener( channel: string, listener: (...args: $ReadOnlyArray) => void, ): void, send(channel: string, ...args: $ReadOnlyArray): void, sendSync(channel: string, ...args: $ReadOnlyArray): any, }; declare export type MenuItemConstructorOptions = { click?: () => void, label?: string, submenu?: $ReadOnlyArray, type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio', role?: | 'undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'pasteAndMatchStyle' | 'delete' | 'selectAll' | 'reload' | 'forceReload' | 'toggleDevTools' | 'resetZoom' | 'zoomIn' | 'zoomOut' | 'toggleSpellChecker' | 'togglefullscreen' | 'window' | 'minimize' | 'close' | 'help' | 'about' | 'services' | 'hide' | 'hideOthers' | 'unhide' | 'quit' | 'showSubstitutions' | 'toggleSmartQuotes' | 'toggleSmartDashes' | 'toggleTextReplacement' | 'startSpeaking' | 'stopSpeaking' | 'zoom' | 'front' | 'appMenu' | 'fileMenu' | 'editMenu' | 'viewMenu' | 'shareMenu' | 'recentDocuments' | 'toggleTabBar' | 'selectNextTab' | 'selectPreviousTab' | 'mergeAllWindows' | 'clearRecentDocuments' | 'moveTabToNewWindow' | 'windowMenu', }; declare export class Menu { constructor(): void; static setApplicationMenu(menu: Menu | null): void; static buildFromTemplate( template: $ReadOnlyArray, ): 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, +icon?: 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( url: string, options?: { +activate?: boolean, +workingDirectory?: string }, ): Promise, }; declare export var systemPreferences: SystemPreferences; declare type SystemPreferences = { getUserDefault>( key: string, type: Type, ): $ElementType, }; declare export type UserDefaultTypes = { string: string, boolean: boolean, integer: number, float: number, double: number, url: string, array: Array, dictionary: { [string]: mixed }, }; declare class WebContents { loadURL( url: string, options?: { +userAgent?: string, +extraHeaders?: string, +baseURLForDataURL?: string, }, ): Promise; loadFile( filePath: string, options?: { +query?: { [string]: string }, +search?: string, +hash?: string, }, ): Promise; canGoBack(): boolean; canGoForward(): boolean; clearHistory(): void; insertCSS(css: string, options?: { +cssOrigin?: string }): Promise; setWindowOpenHandler( handler: (details: { +url: string, +frameName: string, +features: string, +disposition: | 'default' | 'foreground-tab' | 'background-tab' | 'new-window' | 'save-to-disk' | 'other', }) => | { +action: 'deny' } | { +action: 'allow', +outlivesOpener?: boolean }, ): void; send(channel: string, ...args: $ReadOnlyArray): void; on>( event: T, listener: $ElementType, ): void; removeListener: >( event: T, listener: $ElementType, ) => void; inspectSharedWorker(): void; } declare type WebContentsEvents = { 'did-finish-load': () => void, 'did-fail-load': ( event: Event, errorCode: number, errorDescription: string, validatedURL: string, isMainFrame: boolean, frameProcessId: number, frameRoutingId: number, ) => void, 'did-navigate-in-page': ( event: Event, url: string, isMainFrame: boolean, frameProcessId: number, frameRoutingId: number, ) => void, }; declare export type IpcRendererEvent = { sender: IpcRenderer, senderId: number, ports: $ReadOnlyArray, }; } declare module 'electron/main' { declare export { app, BrowserWindow, shell, Menu, ipcMain, systemPreferences, autoUpdater, pushNotifications, Notification, } from 'electron'; } declare module 'electron/renderer' { declare export { IpcRendererEvent, contextBridge, ipcRenderer, } from 'electron'; } diff --git a/desktop/src/main.js b/desktop/src/main.js index 40bd7b53c..d9fc2c402 100644 --- a/desktop/src/main.js +++ b/desktop/src/main.js @@ -1,350 +1,358 @@ // @flow import type { Event, MenuItemConstructorOptions } from 'electron'; import { app, BrowserWindow, shell, Menu, ipcMain, systemPreferences, autoUpdater, // eslint-disable-next-line import/extensions } from 'electron/main'; import contextMenu from 'electron-context-menu'; import fs from 'fs'; import path from 'path'; 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/webapp/' : 'https://web.comm.app'; const isMac = process.platform === 'darwin'; const scrollbarCSS = fs.promises.readFile( path.resolve(__dirname, '../scrollbar.css'), 'utf8', ); let mainWindow = null; const setApplicationMenu = () => { let mainMenu: MenuItemConstructorOptions[] = []; if (isMac) { mainMenu = [ { label: app.name, submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' }, ], }, ]; } const viewMenu = { label: 'View', submenu: [ { role: 'reload' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' }, { role: 'toggleDevTools' }, { label: 'Toggle Shared Worker Developer Tools', click: () => { if (mainWindow) { mainWindow.webContents.inspectSharedWorker(); } }, }, ], }; const windowMenu = { label: 'Window', submenu: [ { role: 'minimize' }, ...(isMac ? [ { type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' }, ] : [{ role: 'close' }]), ], }; const menu = Menu.buildFromTemplate([ ...mainMenu, { role: 'fileMenu' }, { role: 'editMenu' }, viewMenu, windowMenu, ]); Menu.setApplicationMenu(menu); }; const createMainWindow = (urlPath?: string) => { const win = new BrowserWindow({ show: false, width: 1300, height: 800, minWidth: 1100, minHeight: 600, titleBarStyle: 'hidden', trafficLightPosition: { x: 20, y: 24 }, titleBarOverlay: { color: '#0A0A0A', symbolColor: '#FFFFFF', height: 64, }, backgroundColor: '#0A0A0A', webPreferences: { preload: path.resolve(__dirname, 'preload.js'), }, }); const updateNavigationState = () => { win.webContents.send('on-navigate', { canGoBack: win.webContents.canGoBack(), canGoForward: win.webContents.canGoForward(), }); }; win.webContents.on('did-navigate-in-page', updateNavigationState); const clearHistory = () => { win.webContents.clearHistory(); updateNavigationState(); }; ipcMain.on('clear-history', clearHistory); const doubleClickTopBar = () => { if (isMac) { // Possible values for AppleActionOnDoubleClick are Maximize, // Minimize or None. We handle the last two inside this if. // Maximize (which is the only behaviour for other platforms) // is handled in the later block. const action = systemPreferences.getUserDefault( 'AppleActionOnDoubleClick', 'string', ); if (action === 'None') { return; } else if (action === 'Minimize') { win.minimize(); return; } } if (win.isMaximized()) { win.unmaximize(); } else { win.maximize(); } }; ipcMain.on('double-click-top-bar', doubleClickTopBar); const updateDownloaded = ( event: Event, releaseNotes?: string, releaseName: string, ) => { win.webContents.send('on-new-version-available', releaseName); }; 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); }); win.webContents.setWindowOpenHandler(({ url: openURL }) => { shell.openExternal(openURL); // Returning 'deny' prevents a new electron window from being created return { action: 'deny' }; }); (async () => { const css = await scrollbarCSS; win.webContents.insertCSS(css); })(); win.loadURL(url + (urlPath ?? '')); mainWindow = win; return win; }; const createSplashWindow = () => { const win = new BrowserWindow({ width: 300, height: 300, resizable: false, frame: false, alwaysOnTop: true, center: true, backgroundColor: '#111827', }); win.loadFile(path.resolve(__dirname, '../pages/splash.html')); return win; }; const createErrorWindow = () => { const win = new BrowserWindow({ show: false, width: 400, height: 300, resizable: false, center: true, titleBarStyle: 'hidden', trafficLightPosition: { x: 20, y: 24 }, backgroundColor: '#111827', }); win.on('close', () => { app.quit(); }); win.loadFile(path.resolve(__dirname, '../pages/error.html')); return win; }; +const sendDeviceTokenToWebApp = async () => { + if (!mainWindow) { + return; + } + + const token = await registerForNotifications(); + mainWindow?.webContents.send('on-device-token-registered', token); +}; + const show = (urlPath?: string) => { const splash = createSplashWindow(); const error = createErrorWindow(); const main = createMainWindow(urlPath); let loadedSuccessfully = true; const failedLoadHandler = () => { loadedSuccessfully = false; if (!splash.isDestroyed()) { splash.destroy(); } if (!error.isDestroyed()) { error.show(); } setTimeout(() => { loadedSuccessfully = true; main.loadURL(url); }, 1000); }; main.webContents.on('did-fail-load', failedLoadHandler); main.webContents.on('did-finish-load', () => { if (loadedSuccessfully) { if (!splash.isDestroyed()) { splash.destroy(); } if (!error.isDestroyed()) { error.destroy(); } main.webContents.removeListener('did-fail-load', failedLoadHandler); main.show(); if (app.isPackaged) { - (async () => { - const token = await registerForNotifications(); - main.webContents.send('on-device-token-registered', token); - })(); + sendDeviceTokenToWebApp(); } } }); }; const run = () => { app.setName('Comm'); contextMenu({ showSaveImageAs: true, showSaveVideoAs: true, }); if (process.platform === 'win32') { app.setAppUserModelId('Comm'); } setApplicationMenu(); (async () => { await app.whenReady(); if (app.isPackaged) { try { initAutoUpdate(); } catch (error) { console.error(error); } + listenForNotifications(threadID => { if (mainWindow) { mainWindow.webContents.send('on-notification-clicked', { threadID, }); } else { show(`chat/thread/${threadID}/`); } }); + ipcMain.on('fetch-device-token', sendDeviceTokenToWebApp); } ipcMain.on('set-badge', (event, value) => { if (isMac) { app.dock.setBadge(value?.toString() ?? ''); } }); ipcMain.on('get-version', event => { event.returnValue = app.getVersion().toString(); }); show(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { show(); } }); })(); app.on('window-all-closed', () => { if (!isMac) { app.quit(); } }); app.on('render-process-gone', (event, webContents, details) => { console.error( `EVENT: render-process-gone. Reason: '${details.reason}'. ` + `ExitCode: '${details.exitCode}'.`, ); }); app.on('child-process-gone', (event, details) => { console.error( `EVENT: child-process-gone. Process type: '${details.type}'. ` + `Reason: '${details.reason}'. ExitCode: '${details.exitCode}'.`, ); }); }; if (app.isPackaged && process.platform === 'win32') { if (!handleSquirrelEvent()) { run(); } } else { run(); } diff --git a/desktop/src/preload.js b/desktop/src/preload.js index 969c29528..d9022f173 100644 --- a/desktop/src/preload.js +++ b/desktop/src/preload.js @@ -1,45 +1,46 @@ // @flow import type { IpcRendererEvent } from 'electron'; // eslint-disable-next-line import/extensions import { contextBridge, ipcRenderer } from 'electron/renderer'; import type { ElectronBridge } from 'lib/types/electron-types.js'; const bridge: ElectronBridge = { onNavigate: callback => { const withEvent = (event: IpcRendererEvent, ...args: $ReadOnlyArray) => callback(...args); ipcRenderer.on('on-navigate', withEvent); return () => ipcRenderer.removeListener('on-navigate', withEvent); }, clearHistory: () => ipcRenderer.send('clear-history'), doubleClickTopBar: () => ipcRenderer.send('double-click-top-bar'), setBadge: value => ipcRenderer.send('set-badge', value), version: ipcRenderer.sendSync('get-version'), onNewVersionAvailable: callback => { const withEvent = (event: IpcRendererEvent, ...args: $ReadOnlyArray) => callback(...args); ipcRenderer.on('on-new-version-available', withEvent); return () => ipcRenderer.removeListener('on-new-version-available', withEvent); }, updateToNewVersion: () => ipcRenderer.send('update-to-new-version'), platform: { win32: 'windows', darwin: 'macos' }[process.platform], onDeviceTokenRegistered: callback => { const withEvent = (event: IpcRendererEvent, ...args: $ReadOnlyArray) => callback(...args); ipcRenderer.on('on-device-token-registered', withEvent); return () => ipcRenderer.removeListener('on-device-token-registered', withEvent); }, onNotificationClicked: callback => { const withEvent = (event: IpcRendererEvent, ...args: $ReadOnlyArray) => callback(...args); ipcRenderer.on('on-notification-clicked', withEvent); return () => ipcRenderer.removeListener('on-notification-clicked', withEvent); }, + fetchDeviceToken: () => ipcRenderer.send('fetch-device-token'), }; contextBridge.exposeInMainWorld('electronContextBridge', bridge); diff --git a/lib/types/electron-types.js b/lib/types/electron-types.js index bb2675d38..ff3648b0b 100644 --- a/lib/types/electron-types.js +++ b/lib/types/electron-types.js @@ -1,27 +1,28 @@ // @flow type OnNavigateListener = ({ +canGoBack: boolean, +canGoForward: boolean, }) => void; type OnNewVersionAvailableListener = (version: string) => void; type OnDeviceTokenRegisteredListener = (token: ?string) => void; type OnNotificationClickedListener = (data: { threadID: string }) => void; export type ElectronBridge = { // Returns a callback that you can call to remove the listener +onNavigate: OnNavigateListener => () => void, +clearHistory: () => void, +doubleClickTopBar: () => void, +setBadge: (number | string | null) => void, +version?: string, // Returns a callback that you can call to remove the listener +onNewVersionAvailable?: OnNewVersionAvailableListener => () => void, +updateToNewVersion?: () => void, +platform?: 'windows' | 'macos', +onDeviceTokenRegistered?: OnDeviceTokenRegisteredListener => () => void, +onNotificationClicked?: OnNotificationClickedListener => () => void, + +fetchDeviceToken: () => void, }; diff --git a/web/push-notif/push-notifs-handler.js b/web/push-notif/push-notifs-handler.js index f66bcf4ba..cf40fb25f 100644 --- a/web/push-notif/push-notifs-handler.js +++ b/web/push-notif/push-notifs-handler.js @@ -1,211 +1,215 @@ // @flow import * as React from 'react'; import { useSetDeviceTokenFanout, setDeviceTokenActionTypes, } from 'lib/actions/device-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import { convertNonPendingIDToNewSchema } from 'lib/utils/migration-utils.js'; import { shouldSkipPushPermissionAlert, recordNotifPermissionAlertActionType, } from 'lib/utils/push-alerts.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { WORKERS_MODULES_DIR_PATH, DEFAULT_OLM_FILENAME, } from '../database/utils/constants.js'; import electron from '../electron.js'; import PushNotifModal from '../modals/push-notif-modal.react.js'; import { updateNavInfoActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; declare var baseURL: string; declare var olmFilename: string; function useCreateDesktopPushSubscription() { const dispatchActionPromise = useDispatchActionPromise(); const callSetDeviceToken = useSetDeviceTokenFanout(); React.useEffect( () => electron?.onDeviceTokenRegistered?.((token: ?string) => { dispatchActionPromise( setDeviceTokenActionTypes, callSetDeviceToken(token), ); }), [callSetDeviceToken, dispatchActionPromise], ); + React.useEffect(() => { + electron?.fetchDeviceToken(); + }, []); + const dispatch = useDispatch(); React.useEffect( () => electron?.onNotificationClicked?.( ({ threadID }: { +threadID: string }) => { const convertedThreadID = convertNonPendingIDToNewSchema( threadID, ashoatKeyserverID, ); const payload = { chatMode: 'view', activeChatThreadID: convertedThreadID, tab: 'chat', }; dispatch({ type: updateNavInfoActionType, payload }); }, ), [dispatch], ); } function useCreatePushSubscription(): () => Promise { const publicKey = useSelector(state => state.pushApiPublicKey); const dispatchActionPromise = useDispatchActionPromise(); const callSetDeviceToken = useSetDeviceTokenFanout(); const staffCanSee = useStaffCanSee(); return React.useCallback(async () => { if (!publicKey) { return; } const workerRegistration = await navigator.serviceWorker?.ready; if (!workerRegistration) { return; } const origin = window.location.origin; const olmWasmDirPath = `${origin}${baseURL}${WORKERS_MODULES_DIR_PATH}`; const olmWasmFilename = olmFilename ? olmFilename : DEFAULT_OLM_FILENAME; const olmWasmPath = `${olmWasmDirPath}/${olmWasmFilename}`; workerRegistration.active?.postMessage({ olmWasmPath, staffCanSee }); const subscription = await workerRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: publicKey, }); dispatchActionPromise( setDeviceTokenActionTypes, callSetDeviceToken(JSON.stringify(subscription)), ); }, [callSetDeviceToken, dispatchActionPromise, publicKey, staffCanSee]); } function PushNotificationsHandler(): React.Node { useCreateDesktopPushSubscription(); const createPushSubscription = useCreatePushSubscription(); const notifPermissionAlertInfo = useSelector( state => state.notifPermissionAlertInfo, ); const modalContext = useModalContext(); const loggedIn = useSelector(isLoggedIn); const dispatch = useDispatch(); const supported = 'Notification' in window && !electron; React.useEffect(() => { (async () => { if (!navigator.serviceWorker || !supported) { return; } await navigator.serviceWorker.register('worker/notif', { scope: '/' }); if (Notification.permission === 'granted') { // Make sure the subscription is current if we have the permissions await createPushSubscription(); } else if ( Notification.permission === 'default' && loggedIn && !shouldSkipPushPermissionAlert(notifPermissionAlertInfo) ) { // Ask existing users that are already logged in for permission modalContext.pushModal(); dispatch({ type: recordNotifPermissionAlertActionType, payload: { time: Date.now() }, }); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Ask for permission on login const prevLoggedIn = React.useRef(loggedIn); React.useEffect(() => { if (!navigator.serviceWorker || !supported) { return; } if (!prevLoggedIn.current && loggedIn) { if (Notification.permission === 'granted') { createPushSubscription(); } else if ( Notification.permission === 'default' && !shouldSkipPushPermissionAlert(notifPermissionAlertInfo) ) { modalContext.pushModal(); dispatch({ type: recordNotifPermissionAlertActionType, payload: { time: Date.now() }, }); } } prevLoggedIn.current = loggedIn; }, [ createPushSubscription, dispatch, loggedIn, modalContext, notifPermissionAlertInfo, prevLoggedIn, supported, ]); // Redirect to thread on notification click React.useEffect(() => { if (!navigator.serviceWorker || !supported) { return undefined; } const callback = (event: MessageEvent) => { if (typeof event.data !== 'object' || !event.data) { return; } if (event.data.targetThreadID) { const payload = { chatMode: 'view', activeChatThreadID: event.data.targetThreadID, tab: 'chat', }; dispatch({ type: updateNavInfoActionType, payload }); } }; navigator.serviceWorker.addEventListener('message', callback); return () => navigator.serviceWorker?.removeEventListener('message', callback); }, [dispatch, supported]); return null; } export { PushNotificationsHandler, useCreatePushSubscription };