From 8b6eb48161a112d16f211d502c07c50e13c59bc3 Mon Sep 17 00:00:00 2001 From: balzack Date: Tue, 14 Jan 2025 22:11:22 -0800 Subject: [PATCH] adding push notifications to web --- app/client/web/favicon.ico | Bin 0 -> 15406 bytes app/client/web/index.html | 2 +- .../web/src/settings/useSettings.hook.ts | 54 ++++++++++++++++-- app/sdk/src/api.ts | 4 +- app/sdk/src/net/setAccountNotifications.ts | 9 ++- app/sdk/src/settings.ts | 6 +- app/sdk/src/types.ts | 14 +++++ 7 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 app/client/web/favicon.ico diff --git a/app/client/web/favicon.ico b/app/client/web/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..90cedd96239f4c4a689204af580568fd9c1f3e01 GIT binary patch literal 15406 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

iJWWKx7 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 - + Databag diff --git a/app/client/web/src/settings/useSettings.hook.ts b/app/client/web/src/settings/useSettings.hook.ts index 6d2e3223..594d0cd5 100644 --- a/app/client/web/src/settings/useSettings.hook.ts +++ b/app/client/web/src/settings/useSettings.hook.ts @@ -2,12 +2,27 @@ import { useEffect, useState, useContext, useRef } from 'react' import { AppContext } from '../context/AppContext' import { DisplayContext } from '../context/DisplayContext' import { ContextType } from '../context/ContextType' -import { type Profile, type Config } from 'databag-client-sdk' +import { type Profile, type Config, type PushParams, PushType } from 'databag-client-sdk' import { Point, Area } from 'react-easy-crop/types' const IMAGE_DIM = 192 const DEBOUNCE_MS = 1000 +function urlB64ToUint8Array(b64: string) { + const padding = '='.repeat((4 - b64.length % 4) % 4); + const base64 = (b64 + padding) + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (var i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; +} + export function useSettings() { const display = useContext(DisplayContext) as ContextType const app = useContext(AppContext) as ContextType @@ -46,6 +61,7 @@ export function useSettings() { secretImage: '', code: '', editImage: '', + webPushKey: null, sealPassword: '', sealConfirm: '', sealDelete: '', @@ -127,10 +143,8 @@ export function useSettings() { updateState({ blockedMessages }); }, loadBlockedChannels: async () => { -console.log("LOAD BLOCKED"); const settings = app.state.session.getSettings(); const blockedChannels = await settings.getBlockedChannels(); -console.log("LOADED: ", blockedChannels); updateState({ blockedChannels }); }, unblockChannel: async (cardId: string | null, channelId: string) => { @@ -159,8 +173,38 @@ console.log("LOADED: ", blockedChannels); await settings.setLogin(state.handle, state.password) }, enableNotifications: async () => { - const { settings } = getSession() - await settings.enableNotifications() + const webPushKey = state.config?.webPushKey; + if (!webPushKey) { + throw new Error('web push key not set'); + } + const status = await Notification.requestPermission(); + if (status === 'granted') { + const registration = await navigator.serviceWorker.register('push.js'); + await navigator.serviceWorker.ready; + const params = { userVisibleOnly: true, applicationServerKey: urlB64ToUint8Array(webPushKey) }; + const subscription = await registration.pushManager.subscribe(params); + + const endpoint = subscription.endpoint; + const binPublicKey = subscription.getKey('p256dh'); + const binAuth = subscription.getKey('auth'); + + if (endpoint && binPublicKey && binAuth) { + const numPublicKey: number[] = []; + (new Uint8Array(binPublicKey)).forEach(val => { + numPublicKey.push(val); + }); + const numAuth: number[] = []; + (new Uint8Array(binAuth)).forEach(val => { + numAuth.push(val); + }); + const publicKey = btoa(String.fromCharCode.apply(null, numPublicKey)); + const auth = btoa(String.fromCharCode.apply(null, numAuth)); + + const pushParams = { endpoint, publicKey, auth, type: PushType.Web }; + const { settings } = getSession() + await settings.enableNotifications(pushParams) + } + } }, disableNotifications: async () => { const { settings } = getSession() diff --git a/app/sdk/src/api.ts b/app/sdk/src/api.ts index 4e6fd8dd..705b7969 100644 --- a/app/sdk/src/api.ts +++ b/app/sdk/src/api.ts @@ -1,4 +1,4 @@ -import type { Channel, Topic, AssetSource, Asset, Tag, Article, Group, Card, Profile, Call, FocusDetail, Config, NodeConfig, NodeAccount, Participant } from './types'; +import type { Channel, Topic, AssetSource, Asset, Tag, Article, Group, Card, Profile, Call, FocusDetail, Config, NodeConfig, NodeAccount, Participant, PushParams } from './types'; export interface Session { getSettings(): Settings; @@ -31,7 +31,7 @@ export interface Ring { export interface Settings { getUsernameStatus(username: string): Promise; setLogin(username: string, password: string): Promise; - enableNotifications(): Promise; + enableNotifications(params?: PushParams): Promise; disableNotifications(): Promise; enableRegistry(): Promise; disableRegistry(): Promise; diff --git a/app/sdk/src/net/setAccountNotifications.ts b/app/sdk/src/net/setAccountNotifications.ts index fe42536b..71950bee 100644 --- a/app/sdk/src/net/setAccountNotifications.ts +++ b/app/sdk/src/net/setAccountNotifications.ts @@ -1,7 +1,12 @@ import { checkResponse, fetchWithTimeout } from './fetchUtil'; +import { PushParams } from '../types'; -export async function setAccountNotifications(node: string, secure: boolean, token: string, flag: boolean) { - const endpoint = `http${secure ? 's' : ''}://${node}/account/notification?agent=${token}`; +export async function setAccountNotifications(node: string, secure: boolean, token: string, flag: boolean, pushParams: PushParams) { + const pushEndpoint = pushParams ? encodeURIComponent(pushParams.endpoint) : ''; + const publicKey = pushParams ? encodeURIComponent(pushParams.publicKey) : ''; + const auth = pushParams ? encodeURIComponent(pushParams.auth) : ''; + const params = pushParams ? `&webEndpoint=${pushEndpoint}&webPublicKey=${publicKey}&webAuth=${auth}&pushType=${pushParams.type}` : '' + const endpoint = `http${secure ? 's' : ''}://${node}/account/notification?agent=${token}${params}`; const { status } = await fetchWithTimeout(endpoint, { method: 'PUT', body: JSON.stringify(flag), diff --git a/app/sdk/src/settings.ts b/app/sdk/src/settings.ts index d245ed11..f9c02196 100644 --- a/app/sdk/src/settings.ts +++ b/app/sdk/src/settings.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'eventemitter3'; import type { Settings } from './api'; -import type { Config } from './types'; +import type { Config, PushParams } from './types'; import { Store } from './store'; import { Crypto } from './crypto'; import { Logging } from './logging'; @@ -152,9 +152,9 @@ export class SettingsModule implements Settings { await this.sync(); } - public async enableNotifications(): Promise { + public async enableNotifications(params?: PushParams): Promise { const { node, secure, token } = this; - await setAccountNotifications(node, secure, token, true); + await setAccountNotifications(node, secure, token, true, params); } public async disableNotifications(): Promise { diff --git a/app/sdk/src/types.ts b/app/sdk/src/types.ts index 7b2a1602..cd9beb0b 100644 --- a/app/sdk/src/types.ts +++ b/app/sdk/src/types.ts @@ -258,3 +258,17 @@ export type SessionParams = { version: string; appName: string; }; + +export enum PushType { + UPN = 'upn', // unified push notifications + Web = 'web', // browser push notifications + FCM = 'fcm', // firebase cloud messaging +} + +export type PushParams = { + endpoint: string; + publicKey: string; + auth: string; + type: PushType; +} +