cheogram-www
created pr with
cheogram-www/tauri-alpha.1
commented
Changes are available at https://tangled.org/mrry.band/cheogram-www on branch tauri-alpha.
added cheogram-www/tauri-alpha.2
1: 45e8ad7 = 1: 45e8ad7 Refactor service worker access
2: fab4022 ! 2: ce66e8f Move notifications code to one file
-: ------- > 3: 10e3574 Refactor notification code
3: d127ee0 ! 4: ba0891f npm run tauri init
4: 9e006bd = 5: 5b3abf9 Request microphone and camera usage on mac
5: 03d4934 = 6: 994a2e9 Tauri file system media store
6: b96216a = 7: 320fa8f Don't register or use serviceworker in tauri
7: faace0a = 8: e76db17 Open links in browser
8: 9c07308 ! 9: 761dd22 Tauri notifications
cmds
checkout latest patchset:
ssh pr.pico.sh pull cheogram-www:tauri-alpha | git am -3checkout specific patchset revision:
ssh pr.pico.sh pull cheogram-www:tauri-alpha [rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh cheogram-www:tauri-alphaadd comment to patch request:
ssh pr.pico.sh comment cheogram-www:tauri-alpha "lgtm!"
Patchset
cheogram-www/tauri-alpha.2
Refactor service worker access
Eric Roberts
Move notifications code to one file
2026-10-29T14:50:23ZEric Roberts
→ Refactor notification code
2026-11-04T14:34:43ZEric Roberts
npm run tauri init
2026-11-04T16:17:25ZEric Roberts
Request microphone and camera usage on mac
2026-10-16T14:01:30ZEric Roberts
Tauri file system media store
2026-10-20T19:29:32ZEric Roberts
Don't register or use serviceworker in tauri
2026-10-16T18:10:16ZEric Roberts
Open links in browser
2026-10-19T18:23:24ZEric Roberts
Tauri notifications
2026-10-20T18:39:35ZEric Roberts
2026-10-19T19:40:11Z
Refactor notification code
Eric Roberts
2026-11-04T16:17:25ZWe currently have two ways to send notifications, via the service worker or new Notification. These things should behave the same, and I think functionally right now they do behave the same, but they don't actually use the same code paths. This change adds common click and close handlers that can be used everywhere, and will be used by the tauri click handling code in the future. It also adds a common showNotification which attaches the common handlers instead of the callsites that want to show a notification doing it themselves. showNotification will be extended in the future to support Tauri notifications. While doing this I realized the service worker repeats a lot of this constructing notification code as well. But I am leaving it for now.
Semantic diff summary
3 added,
17 modified,
0 signature changed,
0 removed
across 5 analyzed files
+32
-39
src/App.tsx
#
| ... | ... | @@ -6,15 +7,13 @@ import * as borogove from "borogove"; | |
| 6 | 7 | import { borogove_persistence_Sqlite as borogoveSqlite } from "borogove/sqlite-wasm"; | |
| 7 | 8 | import * as Sentry from "@sentry/browser"; | |
| 8 | 9 | import { stem } from "porter2"; | |
| 10 | + | import { Await, useAsyncValue, Outlet, useParams } from "react-router"; | |
| 11 | + | import { ios, webkit, useEffectEvent, serviceWorker } from "./util"; | |
| 9 | 12 | import { | |
| 10 | - | Await, | |
| 11 | - | useAsyncValue, | |
| 12 | - | Outlet, | |
| 13 | - | useNavigate, | |
| 14 | - | useParams, | |
| 15 | - | } from "react-router"; | |
| 16 | - | import { ios, webkit, href, useEffectEvent, serviceWorker } from "./util"; | |
| 17 | - | import { showMessageNotification } from "./notifications"; | |
| 13 | + | showMessageNotification, | |
| 14 | + | useNotifications, | |
| 15 | + | NotificationData, | |
| 16 | + | } from "./notifications"; | |
| 18 | 17 | import { SettingsContext, SettingsData } from "./components/Settings"; | |
| 19 | 18 | import { Call } from "./components/Call"; | |
| 20 | 19 | import { LoginRequest } from "./components/LoginRequest"; |
| ... | ... | @@ -67,7 +66,8 @@ const AppInner = () => { | |
| 67 | 66 | const [accounts, setAccounts] = useState(initialAccounts); | |
| 68 | 67 | const [settings, setSettings] = useState<SettingsData>(initialSettings); | |
| 69 | 68 | const params = useParams(); | |
| 70 | - | const navigate = useNavigate(); | |
| 69 | + | const { showNotification, onNotificationClick, onNotificationClose } = | |
| 70 | + | useNotifications(); | |
| 71 | 71 | ||
| 72 | 72 | const notificationEvent = useEffectEvent( | |
| 73 | 73 | ( |
| ... | ... | @@ -157,41 +159,32 @@ const AppInner = () => { | |
| 157 | 159 | ||
| 158 | 160 | useEffect(() => { | |
| 159 | 161 | if (!serviceWorker) return; | |
| 160 | - | const handler = (event: MessageEvent) => { | |
| 161 | - | const accountId = event.data?.data?.accountId; | |
| 162 | - | const chatId = event.data?.data?.chatId; | |
| 163 | - | if (event.data.event === "notificationclick") { | |
| 164 | - | navigate( | |
| 165 | - | href("/accounts/:accountId/chats/:chatId", { accountId, chatId }) + | |
| 166 | - | `#message:${event.data.data.messageId}`, | |
| 167 | - | ); | |
| 168 | - | if (event.data.data.type === borogove.MessageType.MessageCall) { | |
| 169 | - | if (event.data.data.callStatus === "propose") { | |
| 170 | - | setTimeout( | |
| 171 | - | () => accounts[accountId]?.client?.getChat(chatId)?.acceptCall(), | |
| 172 | - | 100, | |
| 173 | - | ); | |
| 174 | - | } | |
| 175 | - | } | |
| 176 | - | } | |
| 177 | - | if (event.data.event === "notificationclose") { | |
| 178 | - | if (event.data.data.type === borogove.MessageType.MessageCall) { | |
| 179 | - | setTimeout(() => { | |
| 180 | - | const chat = accounts[accountId]?.client?.getChat(chatId); | |
| 181 | - | if ( | |
| 182 | - | chat && | |
| 183 | - | chat.callStatus() === borogove.calls.CallStatus.Incoming | |
| 184 | - | ) { | |
| 185 | - | chat.hangup(); | |
| 186 | - | } | |
| 187 | - | }, 100); | |
| 188 | - | } | |
| 162 | + | const handler = ( | |
| 163 | + | event: MessageEvent<{ | |
| 164 | + | event: "notificationclick" | "notificationclose"; | |
| 165 | + | data: NotificationData; | |
| 166 | + | }>, | |
| 167 | + | ) => { | |
| 168 | + | const { event: action, data } = event.data; | |
| 169 | + | const chat = | |
| 170 | + | data.type === "loginRequest" | |
| 171 | + | ? null | |
| 172 | + | : (accounts[data.accountId]?.client.getChat(data.chatId) ?? null); | |
| 173 | + | switch (action) { | |
| 174 | + | case "notificationclick": | |
| 175 | + | onNotificationClick(data, chat); | |
| 176 | + | break; | |
| 177 | + | case "notificationclose": | |
| 178 | + | onNotificationClose(data, chat); | |
| 179 | + | break; | |
| 180 | + | default: | |
| 181 | + | return assertNever(action); | |
| 189 | 182 | } | |
| 190 | 183 | }; | |
| 191 | 184 | serviceWorker.addEventListener("message", handler); | |
| 192 | 185 | ||
| 193 | 186 | return () => serviceWorker.removeEventListener("message", handler); | |
| 194 | - | }, [accounts, navigate]); | |
| 187 | + | }, [accounts, onNotificationClick, onNotificationClose]); | |
| 195 | 188 | ||
| 196 | 189 | return ( | |
| 197 | 190 | <SettingsContext.Provider value={settings}> |
+5
-4
src/components/Call.tsx
#
| ... | ... | @@ -28,7 +28,7 @@ import { | |
| 28 | 28 | ||
| 29 | 29 | import { AvatarWithPlaceholder } from "./AvatarWithPlaceholder"; | |
| 30 | 30 | import { href, useEffectEvent, readyServiceWorker } from "../util"; | |
| 31 | - | import { showCallNotification } from "../notifications"; | |
| 31 | + | import { showCallNotification, useNotifications } from "../notifications"; | |
| 32 | 32 | ||
| 33 | 33 | const playTone = (frequency1: number, frequency2: number, duration: number) => { | |
| 34 | 34 | const audioContext = new AudioContext(); |
| ... | ... | @@ -129,6 +129,7 @@ export const Call = ({ | |
| 129 | 129 | accounts: Record<string, { client: borogove.Client }>; | |
| 130 | 130 | }) => { | |
| 131 | 131 | const navigate = useNavigate(); | |
| 132 | + | const { showNotification } = useNotifications(); | |
| 132 | 133 | const [searchParams] = useSearchParams(); | |
| 133 | 134 | ||
| 134 | 135 | const [showDtmf, setShowDtmf] = useState(false); |
| ... | ... | @@ -245,15 +246,15 @@ export const Call = ({ | |
| 245 | 246 | if ( | |
| 246 | 247 | callStatus === borogove.calls.CallStatus.Incoming && | |
| 247 | 248 | callAccountId && | |
| 248 | - | call | |
| 249 | + | callChat | |
| 249 | 250 | ) { | |
| 250 | 251 | playingCancelledRef.current = false; | |
| 251 | 252 | play = playRingtone; | |
| 252 | 253 | (async () => { | |
| 253 | 254 | notificationRef.current = await showCallNotification( | |
| 254 | 255 | callAccountId, | |
| 255 | - | call, | |
| 256 | - | callChat?.getDisplayName() ?? call.get_chatId(), | |
| 256 | + | callChat, | |
| 257 | + | showNotification, | |
| 257 | 258 | ); | |
| 258 | 259 | })(); | |
| 259 | 260 | } |
+7
-3
src/components/LoginRequest.tsx
#
| ... | ... | @@ -1,4 +1,7 @@ | |
| 1 | - | import { showLoginRequestNotification } from "../notifications"; | |
| 1 | + | import { | |
| 2 | + | showLoginRequestNotification, | |
| 3 | + | useNotifications, | |
| 4 | + | } from "../notifications"; | |
| 2 | 5 | import * as borogove from "borogove"; | |
| 3 | 6 | import { Paper } from "@mui/material"; | |
| 4 | 7 | import Draggable from "react-draggable"; |
| ... | ... | @@ -12,6 +15,7 @@ export const LoginRequest = ({ | |
| 12 | 15 | }: { | |
| 13 | 16 | accounts: Record<string, { client: borogove.Client }>; | |
| 14 | 17 | }) => { | |
| 18 | + | const { showNotification } = useNotifications(); | |
| 15 | 19 | const [loginRequest, setLoginRequest] = | |
| 16 | 20 | useState<borogove.LoginRequest | null>(null); | |
| 17 | 21 | const [reqAccountId, setReqAccountId] = useState<string | null>(null); |
+108
-48
src/notifications.ts
#
| ... | ... | @@ -1,10 +1,106 @@ | |
| 1 | + | import { useCallback } from "react"; | |
| 2 | + | import { useNavigate } from "react-router"; | |
| 3 | + | import { assertNever } from "./assert-never"; | |
| 1 | 4 | import * as borogove from "borogove"; | |
| 2 | 5 | import { href, readyServiceWorker } from "./util"; | |
| 3 | 6 | ||
| 7 | + | type ChatNotificationData = { | |
| 8 | + | accountId: string; | |
| 9 | + | chatId: string; | |
| 10 | + | messageId?: string | null; | |
| 11 | + | }; | |
| 12 | + | ||
| 13 | + | export type NotificationData = | |
| 14 | + | | ({ type: borogove.MessageType } & ChatNotificationData) | |
| 15 | + | | { type: "loginRequest" }; | |
| 16 | + | ||
| 17 | + | const chatUrl = ({ accountId, chatId, messageId }: ChatNotificationData) => | |
| 18 | + | href("/accounts/:accountId/chats/:chatId", { accountId, chatId }) + | |
| 19 | + | (messageId ? `#message:${messageId}` : ""); | |
| 20 | + | ||
| 21 | + | const onNotificationClose = ( | |
| 22 | + | data: NotificationData, | |
| 23 | + | chat: borogove.Chat | null, | |
| 24 | + | ) => { | |
| 25 | + | switch (data.type) { | |
| 26 | + | case "loginRequest": | |
| 27 | + | case borogove.MessageType.MessageChat: | |
| 28 | + | case borogove.MessageType.MessageChannel: | |
| 29 | + | case borogove.MessageType.MessageChannelPrivate: | |
| 30 | + | return; | |
| 31 | + | case borogove.MessageType.MessageCall: | |
| 32 | + | if (chat?.callStatus() === borogove.calls.CallStatus.Incoming) | |
| 33 | + | chat.hangup(); | |
| 34 | + | return; | |
| 35 | + | default: | |
| 36 | + | return assertNever(data); | |
| 37 | + | } | |
| 38 | + | }; | |
| 39 | + | ||
| 40 | + | export const useNotifications = () => { | |
| 41 | + | const navigate = useNavigate(); | |
| 42 | + | ||
| 43 | + | const onNotificationClick = useCallback( | |
| 44 | + | (data: NotificationData, chat: borogove.Chat | null) => { | |
| 45 | + | switch (data.type) { | |
| 46 | + | case "loginRequest": | |
| 47 | + | return; | |
| 48 | + | case borogove.MessageType.MessageChat: | |
| 49 | + | case borogove.MessageType.MessageChannel: | |
| 50 | + | case borogove.MessageType.MessageChannelPrivate: | |
| 51 | + | navigate(chatUrl(data)); | |
| 52 | + | return; | |
| 53 | + | case borogove.MessageType.MessageCall: | |
| 54 | + | navigate(chatUrl(data)); | |
| 55 | + | if (chat?.callStatus() === borogove.calls.CallStatus.Incoming) | |
| 56 | + | chat.acceptCall(); | |
| 57 | + | return; | |
| 58 | + | default: | |
| 59 | + | return assertNever(data); | |
| 60 | + | } | |
| 61 | + | }, | |
| 62 | + | [navigate], | |
| 63 | + | ); | |
| 64 | + | ||
| 65 | + | const showNotification = useCallback<ShowNotification>( | |
| 66 | + | async (title, options, chat) => { | |
| 67 | + | const worker = await readyServiceWorker; | |
| 68 | + | if (worker) { | |
| 69 | + | await worker.showNotification(title, options); | |
| 70 | + | return null; | |
| 71 | + | } else { | |
| 72 | + | const notification = new Notification(title, options); | |
| 73 | + | let clicked = false; | |
| 74 | + | notification.addEventListener("click", () => { | |
| 75 | + | clicked = true; | |
| 76 | + | onNotificationClick(options.data, chat); | |
| 77 | + | }); | |
| 78 | + | ||
| 79 | + | notification.addEventListener("close", () => { | |
| 80 | + | if (!clicked) onNotificationClose(options.data, chat); | |
| 81 | + | }); | |
| 82 | + | ||
| 83 | + | return notification; | |
| 84 | + | } | |
| 85 | + | }, | |
| 86 | + | [onNotificationClick], | |
| 87 | + | ); | |
| 88 | + | ||
| 89 | + | return { showNotification, onNotificationClick, onNotificationClose }; | |
| 90 | + | }; | |
| 91 | + | ||
| 92 | + | export type ShowNotification = ( | |
| 93 | + | title: string, | |
| 94 | + | options: NotificationOptions & { data: NotificationData }, | |
| 95 | + | chat: borogove.Chat | null, | |
| 96 | + | ) => Promise<Notification | null>; | |
| 97 | + | ||
| 4 | 98 | export const showMessageNotification = async ( | |
| 5 | 99 | accountId: string, | |
| 6 | 100 | sender: borogove.Member, | |
| 7 | 101 | message: borogove.ChatMessage, | |
| 102 | + | chat: borogove.Chat, | |
| 103 | + | showNotification: ShowNotification, | |
| 8 | 104 | ) => { | |
| 9 | 105 | if (!Notification?.permission) return; | |
| 10 | 106 |
| ... | ... | @@ -41,40 +137,24 @@ export const showMessageNotification = async ( | |
| 41 | 137 | timestamp: new Date(message.timeSent).getTime(), | |
| 42 | 138 | vibrate: [50, 50, 50], | |
| 43 | 139 | data: { | |
| 140 | + | type: message.type, | |
| 44 | 141 | accountId, | |
| 45 | 142 | chatId: message.chatId(), | |
| 46 | 143 | messageId: message.serverId || message.localId, | |
| 47 | - | timestamp: message.timeSent, | |
| 48 | 144 | }, | |
| 49 | 145 | }; | |
| 50 | - | if (serviceWorker) { | |
| 51 | - | await serviceWorker.showNotification(title, options); | |
| 52 | - | } else { | |
| 53 | - | const notif = new Notification(title, options); | |
| 54 | - | notif.addEventListener("click", () => { | |
| 55 | - | history.pushState( | |
| 56 | - | null, | |
| 57 | - | "", | |
| 58 | - | href("/accounts/:accountId/chats/:chatId", { | |
| 59 | - | accountId: accountId, | |
| 60 | - | chatId: message.chatId(), | |
| 61 | - | }) + `#message:${message.serverId || message.localId}`, | |
| 62 | - | ); | |
| 63 | - | window.dispatchEvent(new PopStateEvent("popstate")); // Notify React Router | |
| 64 | - | }); | |
| 65 | - | return notif; | |
| 66 | - | } | |
| 146 | + | return showNotification(title, options, chat); | |
| 67 | 147 | }; | |
| 68 | 148 | ||
| 69 | 149 | export const showCallNotification = async ( | |
| 70 | 150 | accountId: string, | |
| 71 | - | session: borogove.calls.Session, | |
| 72 | - | callerDisplayName: string, | |
| 151 | + | chat: borogove.Chat, | |
| 152 | + | showNotification: ShowNotification, | |
| 73 | 153 | ) => { | |
| 74 | 154 | if (!Notification?.permission) return null; | |
| 75 | 155 | ||
| 76 | 156 | const serviceWorker = await readyServiceWorker; | |
| 77 | - | const tag = accountId + ">" + session.get_chatId() + ">call"; | |
| 157 | + | const tag = accountId + ">" + chat.chatId + ">call"; | |
| 78 | 158 | if (serviceWorker) { | |
| 79 | 159 | const notifs = await serviceWorker.getNotifications({ | |
| 80 | 160 | tag: tag, |
| ... | ... | @@ -84,38 +164,22 @@ export const showCallNotification = async ( | |
| 84 | 164 | } | |
| 85 | 165 | const title = "Incoming Call"; | |
| 86 | 166 | const options = { | |
| 87 | - | body: "From " + callerDisplayName, | |
| 167 | + | body: "From " + chat.getDisplayName(), | |
| 88 | 168 | tag: tag, | |
| 89 | 169 | vibrate: [50, 100, 50, 100], | |
| 90 | 170 | data: { | |
| 91 | 171 | accountId: accountId, | |
| 92 | - | chatId: session.get_chatId(), | |
| 93 | - | type: borogove.MessageType.MessageCall, | |
| 94 | - | callStatus: "propose", | |
| 172 | + | chatId: chat.chatId, | |
| 173 | + | type: borogove.MessageType.MessageCall as const, | |
| 95 | 174 | }, | |
| 96 | 175 | }; | |
| 97 | 176 | ||
| 98 | - | let notification = null; | |
| 99 | - | if (serviceWorker) { | |
| 100 | - | await serviceWorker.showNotification(title, options); | |
| 101 | - | } else { | |
| 102 | - | notification = new Notification(title, options); | |
| 103 | - | ||
| 104 | - | let clicked = false; | |
| 105 | - | notification.addEventListener("click", () => { | |
| 106 | - | clicked = true; | |
| 107 | - | session.accept(); | |
| 108 | - | }); | |
| 109 | - | notification.addEventListener("close", () => { | |
| 110 | - | if (!clicked) session.hangup(); | |
| 111 | - | }); | |
| 112 | - | } | |
| 113 | - | ||
| 114 | - | return notification; | |
| 177 | + | return showNotification(title, options, chat); | |
| 115 | 178 | }; | |
| 116 | 179 | ||
| 117 | 180 | export const showLoginRequestNotification = async ( | |
| 118 | 181 | req: borogove.LoginRequest, | |
| 182 | + | showNotification: ShowNotification, | |
| 119 | 183 | ) => { | |
| 120 | 184 | if (!Notification?.permission) return null; | |
| 121 | 185 |
| ... | ... | @@ -123,12 +187,8 @@ export const showLoginRequestNotification = async ( | |
| 123 | 187 | const options = { | |
| 124 | 188 | body: req.method + " " + req.url, | |
| 125 | 189 | tag: req.id, | |
| 190 | + | data: { type: "loginRequest" as const }, | |
| 126 | 191 | }; | |
| 127 | 192 | ||
| 128 | - | const serviceWorker = await readyServiceWorker; | |
| 129 | - | if (serviceWorker) { | |
| 130 | - | await serviceWorker.showNotification(title, options); | |
| 131 | - | } else { | |
| 132 | - | new Notification(title, options); | |
| 133 | - | } | |
| 193 | + | return showNotification(title, options, null); | |
| 134 | 194 | }; |
+2
-1
src/sw.tsx
#
| ... | ... | @@ -10,6 +10,7 @@ | |
| 10 | 10 | // You can also remove this file if you'd prefer not to use a | |
| 11 | 11 | // service worker, and the Workbox build step will be skipped. | |
| 12 | 12 | ||
| 13 | + | import type { NotificationData } from "./notifications"; | |
| 13 | 14 | import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching"; | |
| 14 | 15 | import { clientsClaim } from "workbox-core"; | |
| 15 | 16 | import { ExpirationPlugin } from "workbox-expiration"; |
| ... | ... | @@ -166,7 +167,7 @@ self.addEventListener("push", (event: PushEvent) => { | |
| 166 | 167 | notif.type === borogove.MessageType.MessageCall | |
| 167 | 168 | ? [50, 100, 50, 100] | |
| 168 | 169 | : [50, 50, 50], | |
| 169 | - | data: { ...notif, raw: raw }, | |
| 170 | + | data: { ...notif, raw } satisfies NotificationData & { raw: string }, | |
| 170 | 171 | }); | |
| 171 | 172 | })(), | |
| 172 | 173 | ); |