cheogram-www

created pr with cheogram-www/tauri-alpha.1 on 2026-10-29T17:17:18Z · by 3c31bc8c
commented on 2026-10-29T17:17:19Z · by 3c31bc8c
Changes are available at https://tangled.org/mrry.band/cheogram-www on branch tauri-alpha.
added cheogram-www/tauri-alpha.2 on 2026-11-04T17:46:08Z · by 3c31bc8c
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 -3
checkout specific patchset revision:
ssh pr.pico.sh pull cheogram-www:tauri-alpha [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh cheogram-www:tauri-alpha
add comment to patch request:
ssh pr.pico.sh comment cheogram-www:tauri-alpha "lgtm!"

Patchset cheogram-www/tauri-alpha.2 on 2026-11-04T17:46:08Z · commit 10e3574

Refactor notification code
Eric Roberts 2026-11-04T16:17:25Z
We 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 #
......@@ -1,3 +1,4 @@
1+import { assertNever } from "./assert-never";
12 import "./App.scss";
23 import { CircularProgress } from "@mui/material";
34 import { newAccount } from "./components/Login";
......@@ -6,15 +7,13 @@ import * as borogove from "borogove";
67 import { borogove_persistence_Sqlite as borogoveSqlite } from "borogove/sqlite-wasm";
78 import * as Sentry from "@sentry/browser";
89 import { stem } from "porter2";
10+import { Await, useAsyncValue, Outlet, useParams } from "react-router";
11+import { ios, webkit, useEffectEvent, serviceWorker } from "./util";
912 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";
1817 import { SettingsContext, SettingsData } from "./components/Settings";
1918 import { Call } from "./components/Call";
2019 import { LoginRequest } from "./components/LoginRequest";
......@@ -67,7 +66,8 @@ const AppInner = () => {
6766 const [accounts, setAccounts] = useState(initialAccounts);
6867 const [settings, setSettings] = useState<SettingsData>(initialSettings);
6968 const params = useParams();
70- const navigate = useNavigate();
69+ const { showNotification, onNotificationClick, onNotificationClose } =
70+ useNotifications();
7171
7272 const notificationEvent = useEffectEvent(
7373 (
......@@ -106,6 +106,8 @@ const AppInner = () => {
106106 client.accountId(),
107107 members[0] ?? message.senderMemberStub(),
108108 message,
109+ chat,
110+ showNotification,
109111 );
110112 }
111113 })();
......@@ -157,41 +159,32 @@ const AppInner = () => {
157159
158160 useEffect(() => {
159161 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);
189182 }
190183 };
191184 serviceWorker.addEventListener("message", handler);
192185
193186 return () => serviceWorker.removeEventListener("message", handler);
194- }, [accounts, navigate]);
187+ }, [accounts, onNotificationClick, onNotificationClose]);
195188
196189 return (
197190 <SettingsContext.Provider value={settings}>
+5 -4 src/components/Call.tsx #
......@@ -28,7 +28,7 @@ import {
2828
2929 import { AvatarWithPlaceholder } from "./AvatarWithPlaceholder";
3030 import { href, useEffectEvent, readyServiceWorker } from "../util";
31-import { showCallNotification } from "../notifications";
31+import { showCallNotification, useNotifications } from "../notifications";
3232
3333 const playTone = (frequency1: number, frequency2: number, duration: number) => {
3434 const audioContext = new AudioContext();
......@@ -129,6 +129,7 @@ export const Call = ({
129129 accounts: Record<string, { client: borogove.Client }>;
130130 }) => {
131131 const navigate = useNavigate();
132+ const { showNotification } = useNotifications();
132133 const [searchParams] = useSearchParams();
133134
134135 const [showDtmf, setShowDtmf] = useState(false);
......@@ -245,15 +246,15 @@ export const Call = ({
245246 if (
246247 callStatus === borogove.calls.CallStatus.Incoming &&
247248 callAccountId &&
248- call
249+ callChat
249250 ) {
250251 playingCancelledRef.current = false;
251252 play = playRingtone;
252253 (async () => {
253254 notificationRef.current = await showCallNotification(
254255 callAccountId,
255- call,
256- callChat?.getDisplayName() ?? call.get_chatId(),
256+ callChat,
257+ showNotification,
257258 );
258259 })();
259260 }
+7 -3 src/components/LoginRequest.tsx #
......@@ -1,4 +1,7 @@
1-import { showLoginRequestNotification } from "../notifications";
1+import {
2+ showLoginRequestNotification,
3+ useNotifications,
4+} from "../notifications";
25 import * as borogove from "borogove";
36 import { Paper } from "@mui/material";
47 import Draggable from "react-draggable";
......@@ -12,6 +15,7 @@ export const LoginRequest = ({
1215 }: {
1316 accounts: Record<string, { client: borogove.Client }>;
1417 }) => {
18+ const { showNotification } = useNotifications();
1519 const [loginRequest, setLoginRequest] =
1620 useState<borogove.LoginRequest | null>(null);
1721 const [reqAccountId, setReqAccountId] = useState<string | null>(null);
......@@ -25,7 +29,7 @@ export const LoginRequest = ({
2529 client.addLoginRequestListener((req) => {
2630 setLoginRequest(req);
2731 setReqAccountId(accountId);
28- showLoginRequestNotification(req);
32+ showLoginRequestNotification(req, showNotification);
2933 }),
3034 ];
3135 }),
......@@ -36,7 +40,7 @@ export const LoginRequest = ({
3640 accounts[accountId].client.removeEventListener(t);
3741 }
3842 };
39- }, [accounts, setLoginRequest, setReqAccountId]);
43+ }, [accounts, setLoginRequest, setReqAccountId, showNotification]);
4044
4145 return (
4246 <>
+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";
14 import * as borogove from "borogove";
25 import { href, readyServiceWorker } from "./util";
36
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+
498 export const showMessageNotification = async (
599 accountId: string,
6100 sender: borogove.Member,
7101 message: borogove.ChatMessage,
102+ chat: borogove.Chat,
103+ showNotification: ShowNotification,
8104 ) => {
9105 if (!Notification?.permission) return;
10106
......@@ -41,40 +137,24 @@ export const showMessageNotification = async (
41137 timestamp: new Date(message.timeSent).getTime(),
42138 vibrate: [50, 50, 50],
43139 data: {
140+ type: message.type,
44141 accountId,
45142 chatId: message.chatId(),
46143 messageId: message.serverId || message.localId,
47- timestamp: message.timeSent,
48144 },
49145 };
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);
67147 };
68148
69149 export const showCallNotification = async (
70150 accountId: string,
71- session: borogove.calls.Session,
72- callerDisplayName: string,
151+ chat: borogove.Chat,
152+ showNotification: ShowNotification,
73153 ) => {
74154 if (!Notification?.permission) return null;
75155
76156 const serviceWorker = await readyServiceWorker;
77- const tag = accountId + ">" + session.get_chatId() + ">call";
157+ const tag = accountId + ">" + chat.chatId + ">call";
78158 if (serviceWorker) {
79159 const notifs = await serviceWorker.getNotifications({
80160 tag: tag,
......@@ -84,38 +164,22 @@ export const showCallNotification = async (
84164 }
85165 const title = "Incoming Call";
86166 const options = {
87- body: "From " + callerDisplayName,
167+ body: "From " + chat.getDisplayName(),
88168 tag: tag,
89169 vibrate: [50, 100, 50, 100],
90170 data: {
91171 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,
95174 },
96175 };
97176
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);
115178 };
116179
117180 export const showLoginRequestNotification = async (
118181 req: borogove.LoginRequest,
182+ showNotification: ShowNotification,
119183 ) => {
120184 if (!Notification?.permission) return null;
121185
......@@ -123,12 +187,8 @@ export const showLoginRequestNotification = async (
123187 const options = {
124188 body: req.method + " " + req.url,
125189 tag: req.id,
190+ data: { type: "loginRequest" as const },
126191 };
127192
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);
134194 };
+2 -1 src/sw.tsx #
......@@ -10,6 +10,7 @@
1010 // You can also remove this file if you'd prefer not to use a
1111 // service worker, and the Workbox build step will be skipped.
1212
13+import type { NotificationData } from "./notifications";
1314 import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching";
1415 import { clientsClaim } from "workbox-core";
1516 import { ExpirationPlugin } from "workbox-expiration";
......@@ -166,7 +167,7 @@ self.addEventListener("push", (event: PushEvent) => {
166167 notif.type === borogove.MessageType.MessageCall
167168 ? [50, 100, 50, 100]
168169 : [50, 50, 50],
169- data: { ...notif, raw: raw },
170+ data: { ...notif, raw } satisfies NotificationData & { raw: string },
170171 });
171172 })(),
172173 );
Back to top