diff --git a/desktop/src/preload.js b/desktop/src/preload.js index 1b37e0523..388514d54 100644 --- a/desktop/src/preload.js +++ b/desktop/src/preload.js @@ -1,28 +1,40 @@ // @flow // 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, ...args) => 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, ...args) => 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, ...args) => callback(...args); + ipcRenderer.on('on-device-token-registered', withEvent); + return () => + ipcRenderer.removeListener('on-device-token-registered', withEvent); + }, + onNotificationClicked: callback => { + const withEvent = (event, ...args) => callback(...args); + ipcRenderer.on('on-notification-clicked', withEvent); + return () => + ipcRenderer.removeListener('on-notification-clicked', withEvent); + }, }; contextBridge.exposeInMainWorld('electronContextBridge', bridge); diff --git a/lib/types/electron-types.js b/lib/types/electron-types.js index cbf20e36c..bb2675d38 100644 --- a/lib/types/electron-types.js +++ b/lib/types/electron-types.js @@ -1,21 +1,27 @@ // @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, }; diff --git a/web/push-notif/push-notifs-handler.js b/web/push-notif/push-notifs-handler.js index dd83fc514..f1aa1fb6b 100644 --- a/web/push-notif/push-notifs-handler.js +++ b/web/push-notif/push-notifs-handler.js @@ -1,124 +1,157 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; import { setDeviceToken, 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, useServerCall, } from 'lib/utils/action-utils.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'; +function useCreateDesktopPushSubscription() { + const dispatchActionPromise = useDispatchActionPromise(); + const callSetDeviceToken = useServerCall(setDeviceToken); + + React.useEffect( + () => + electron?.onDeviceTokenRegistered?.(token => { + dispatchActionPromise( + setDeviceTokenActionTypes, + callSetDeviceToken(token), + ); + }), + [callSetDeviceToken, dispatchActionPromise], + ); + + const dispatch = useDispatch(); + + React.useEffect( + () => + electron?.onNotificationClicked?.(({ threadID }) => { + const payload = { + chatMode: 'view', + activeChatThreadID: threadID, + tab: 'chat', + }; + + dispatch({ type: updateNavInfoActionType, payload }); + }), + [dispatch], + ); +} + function useCreatePushSubscription(): () => Promise { const publicKey = useSelector(state => state.pushApiPublicKey); const dispatchActionPromise = useDispatchActionPromise(); const callSetDeviceToken = useServerCall(setDeviceToken); return React.useCallback(async () => { if (!publicKey) { return; } const workerRegistration = await navigator.serviceWorker?.ready; if (!workerRegistration) { return; } const subscription = await workerRegistration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: publicKey, }); dispatchActionPromise( setDeviceTokenActionTypes, callSetDeviceToken(JSON.stringify(subscription)), ); }, [callSetDeviceToken, dispatchActionPromise, publicKey]); } function PushNotificationsHandler(): React.Node { + useCreateDesktopPushSubscription(); const createPushSubscription = useCreatePushSubscription(); const modalContext = useModalContext(); const loggedIn = useSelector(isLoggedIn); const dispatch = useDispatch(); React.useEffect(() => { (async () => { if (!navigator.serviceWorker || electron) { 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) { // Ask existing users that are already logged in for permission modalContext.pushModal(); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Ask for permission on login const prevLoggedIn = React.useRef(loggedIn); React.useEffect(() => { if (!navigator.serviceWorker || electron) { return; } if (!prevLoggedIn.current && loggedIn) { if (Notification.permission === 'granted') { createPushSubscription(); } else if (Notification.permission === 'default') { modalContext.pushModal(); } } prevLoggedIn.current = loggedIn; }, [createPushSubscription, loggedIn, modalContext, prevLoggedIn]); // Redirect to thread on notification click React.useEffect(() => { if (!navigator.serviceWorker || electron) { return; } 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]); return null; } export { PushNotificationsHandler, useCreatePushSubscription };