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.1 on 2026-10-29T17:17:18Z · commit fab4022

Refactor notification related code
Eric Roberts 2026-10-29T15:06:32Z
With Tauri we'll need to send notifications a different way, so I wanted
a common path to show notifications. We had something called
showNotification but it was for a specific context, so I renamed it to
showMessageNotification and pulled out the part that decided how to send
into a new showNotification function.

Notification events also had data typed as any, and I wanted to make
things a little more explicit about the data we expect. So
showNotification expects data of a specific type, and then when we're
handling the event we parse it with valibot, instead of checking each
property with ? (which we didn't even have to do when it was type any).

I removed the return of notification from showMessageNotification
because there's only one caller and it doesn't use it.
Semantic diff summary
3 added, 7 modified, 0 signature changed, 0 removed across 4 analyzed files (1 file skipped: unsupported file type)
+1 -0 package.json #
......@@ -45,6 +45,7 @@
4545 "sasl-scram-sha-1": "^1.3.0",
4646 "unsplash-react": "^0.3.1",
4747 "uuidv7": "^0.6.3",
48+ "valibot": "^1.5.0",
4849 "visual-dom-diff": "^0.7.3",
4950 "vite-plugin-svgr": "^4.5.0",
5051 "workbox-core": "^6.6.0",
+20 -38 src/App.tsx #
......@@ -1,3 +1,10 @@
1+import { safeParse } from "valibot";
2+import {
3+ showMessageNotification,
4+ handleNotificationClick,
5+ handleNotificationClose,
6+ NotificationEventSchema,
7+} from "./notifications";
18 import "./App.scss";
29 import { CircularProgress } from "@mui/material";
310 import { newAccount } from "./components/Login";
......@@ -13,14 +20,7 @@ import {
1320 useNavigate,
1421 useParams,
1522 } from "react-router";
16-import {
17- ios,
18- webkit,
19- href,
20- useEffectEvent,
21- showNotification,
22- serviceWorker,
23-} from "./util";
23+import { ios, webkit, useEffectEvent, serviceWorker } from "./util";
2424 import { SettingsContext, SettingsData } from "./components/Settings";
2525 import { Call } from "./components/Call";
2626 import { LoginRequest } from "./components/LoginRequest";
......@@ -108,7 +108,7 @@ const AppInner = () => {
108108 (chat.notifyReply() && message.replyToMessage && members[1]?.isSelf)
109109 ) {
110110 if (settings.audibleNotifications) new Audio("/message.opus").play();
111- await showNotification(
111+ await showMessageNotification(
112112 client.accountId(),
113113 members[0] ?? message.senderMemberStub(),
114114 message,
......@@ -163,35 +163,17 @@ const AppInner = () => {
163163
164164 useEffect(() => {
165165 if (!serviceWorker) return;
166- const handler = (event: MessageEvent) => {
167- const accountId = event.data?.data?.accountId;
168- const chatId = event.data?.data?.chatId;
169- if (event.data.event === "notificationclick") {
170- navigate(
171- href("/accounts/:accountId/chats/:chatId", { accountId, chatId }) +
172- `#message:${event.data.data.messageId}`,
173- );
174- if (event.data.data.type === borogove.MessageType.MessageCall) {
175- if (event.data.data.callStatus === "propose") {
176- setTimeout(
177- () => accounts[accountId]?.client?.getChat(chatId)?.acceptCall(),
178- 100,
179- );
180- }
181- }
182- }
183- if (event.data.event === "notificationclose") {
184- if (event.data.data.type === borogove.MessageType.MessageCall) {
185- setTimeout(() => {
186- const chat = accounts[accountId]?.client?.getChat(chatId);
187- if (
188- chat &&
189- chat.callStatus() === borogove.calls.CallStatus.Incoming
190- ) {
191- chat.hangup();
192- }
193- }, 100);
194- }
166+ const handler = (event: MessageEvent<unknown>) => {
167+ const result = safeParse(NotificationEventSchema, event.data);
168+ if (!result.success) return;
169+ const { event: action, data } = result.output;
170+ switch (action) {
171+ case "notificationclick":
172+ handleNotificationClick(data, accounts, navigate);
173+ break;
174+ case "notificationclose":
175+ handleNotificationClose(data, accounts);
176+ break;
195177 }
196178 };
197179 serviceWorker.addEventListener("message", handler);
+3 -6 src/components/Call.tsx #
......@@ -1,3 +1,4 @@
1+import { showNotification } from "../notifications";
12 import * as borogove from "borogove";
23 import { Paper } from "@mui/material";
34 import Draggable from "react-draggable";
......@@ -151,12 +152,8 @@ const showCallNotification = async (
151152 },
152153 };
153154
154- let notification = null;
155- if (serviceWorker) {
156- await serviceWorker.showNotification(title, options);
157- } else {
158- notification = new Notification(title, options);
159-
155+ const notification = await showNotification(title, options);
156+ if (notification) {
160157 let clicked = false;
161158 notification.addEventListener("click", () => {
162159 clicked = true;
+135 -0 src/notifications.ts #
......@@ -0,0 +1,135 @@
1+import type { NavigateFunction } from "react-router";
2+import * as borogove from "borogove";
3+import * as v from "valibot";
4+import { href, readyServiceWorker } from "./util";
5+
6+export const NotificationDataSchema = v.object({
7+ accountId: v.string(),
8+ chatId: v.string(),
9+ messageId: v.nullish(v.string()),
10+ type: v.optional(
11+ v.picklist([
12+ borogove.MessageType.MessageChat,
13+ borogove.MessageType.MessageCall,
14+ borogove.MessageType.MessageChannel,
15+ borogove.MessageType.MessageChannelPrivate,
16+ ]),
17+ ),
18+ callStatus: v.optional(v.string()),
19+});
20+export type NotificationData = v.InferOutput<typeof NotificationDataSchema>;
21+
22+export const NotificationEventSchema = v.object({
23+ event: v.picklist(["notificationclick", "notificationclose"]),
24+ data: NotificationDataSchema,
25+});
26+
27+export const showNotification = async (
28+ title: string,
29+ options: NotificationOptions & { data: NotificationData },
30+) => {
31+ const serviceWorker = await readyServiceWorker;
32+ if (serviceWorker) {
33+ await serviceWorker.showNotification(title, options);
34+ return null;
35+ }
36+ return new Notification(title, options);
37+};
38+
39+export const showMessageNotification = async (
40+ accountId: string,
41+ sender: borogove.Member,
42+ message: borogove.ChatMessage,
43+) => {
44+ if (!Notification?.permission) return;
45+
46+ const serviceWorker = await readyServiceWorker;
47+ const tag = accountId + ">" + message.chatId();
48+ if (serviceWorker) {
49+ // Check if we already notified this message
50+ const notifs = await serviceWorker.getNotifications({
51+ tag: tag,
52+ });
53+ for (const notif of notifs) {
54+ if (notif.data.messageId === (message.serverId || message.localId))
55+ return;
56+ }
57+ }
58+
59+ const title = "Message from " + (sender?.displayName ?? message.chatId());
60+ let icon = sender.photoUri ?? sender.placeholderUri;
61+ if (!icon.startsWith("data:")) {
62+ const response = await fetch(icon);
63+ const mime = response.headers.get("Content-Type");
64+ if (response.ok && mime) {
65+ const buffer = await response.arrayBuffer();
66+ const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
67+ icon = `data:${mime};base64,${base64}`;
68+ }
69+ }
70+ const body = message.body().toPlainText();
71+ const options = {
72+ body: !body && message.attachments.length > 0 ? "Media" : body,
73+ icon,
74+ tag,
75+ renotify: true,
76+ timestamp: new Date(message.timeSent).getTime(),
77+ vibrate: [50, 50, 50],
78+ data: {
79+ accountId,
80+ chatId: message.chatId(),
81+ messageId: message.serverId || message.localId,
82+ timestamp: message.timeSent,
83+ },
84+ };
85+ const notif = await showNotification(title, options);
86+ if (notif) {
87+ notif.addEventListener("click", () => {
88+ history.pushState(
89+ null,
90+ "",
91+ href("/accounts/:accountId/chats/:chatId", {
92+ accountId: accountId,
93+ chatId: message.chatId(),
94+ }) + `#message:${message.serverId || message.localId}`,
95+ );
96+ window.dispatchEvent(new PopStateEvent("popstate")); // Notify React Router
97+ });
98+ }
99+};
100+
101+export function handleNotificationClick(
102+ data: NotificationData,
103+ accounts: Record<string, { client: borogove.Client }>,
104+ navigate: NavigateFunction,
105+) {
106+ const { accountId, chatId } = data;
107+ navigate(
108+ href("/accounts/:accountId/chats/:chatId", { accountId, chatId }) +
109+ (data.messageId ? `#message:${data.messageId}` : ""),
110+ );
111+ if (
112+ data.type === borogove.MessageType.MessageCall &&
113+ data.callStatus === "propose"
114+ ) {
115+ setTimeout(
116+ () => accounts[accountId]?.client?.getChat(chatId)?.acceptCall(),
117+ 100,
118+ );
119+ }
120+}
121+
122+export function handleNotificationClose(
123+ data: NotificationData,
124+ accounts: Record<string, { client: borogove.Client }>,
125+) {
126+ const { accountId, chatId } = data;
127+ if (data.type === borogove.MessageType.MessageCall) {
128+ setTimeout(() => {
129+ const chat = accounts[accountId]?.client?.getChat(chatId);
130+ if (chat && chat.callStatus() === borogove.calls.CallStatus.Incoming) {
131+ chat.hangup();
132+ }
133+ }, 100);
134+ }
135+}
+0 -65 src/util.ts #
......@@ -84,71 +84,6 @@ export const useChatExtras = (accountId: string) => {
8484 };
8585 };
8686
87-export const showNotification = async (
88- accountId: string,
89- sender: borogove.Member,
90- message: borogove.ChatMessage,
91-) => {
92- if (!Notification?.permission) return;
93-
94- const serviceWorker = await readyServiceWorker;
95- const tag = accountId + ">" + message.chatId();
96- if (serviceWorker) {
97- // Check if we already notified this message
98- const notifs = await serviceWorker.getNotifications({
99- tag: tag,
100- });
101- for (const notif of notifs) {
102- if (notif.data.messageId === (message.serverId || message.localId))
103- return;
104- }
105- }
106-
107- const title = "Message from " + (sender?.displayName ?? message.chatId());
108- let icon = sender.photoUri ?? sender.placeholderUri;
109- if (!icon.startsWith("data:")) {
110- const response = await fetch(icon);
111- const mime = response.headers.get("Content-Type");
112- if (response.ok && mime) {
113- const buffer = await response.arrayBuffer();
114- const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
115- icon = `data:${mime};base64,${base64}`;
116- }
117- }
118- const body = message.body().toPlainText();
119- const options = {
120- body: !body && message.attachments.length > 0 ? "Media" : body,
121- icon,
122- tag,
123- renotify: true,
124- timestamp: new Date(message.timeSent).getTime(),
125- vibrate: [50, 50, 50],
126- data: {
127- accountId,
128- chatId: message.chatId(),
129- messageId: message.serverId || message.localId,
130- timestamp: message.timeSent,
131- },
132- };
133- if (serviceWorker) {
134- await serviceWorker.showNotification(title, options);
135- } else {
136- const notif = new Notification(title, options);
137- notif.addEventListener("click", () => {
138- history.pushState(
139- null,
140- "",
141- href("/accounts/:accountId/chats/:chatId", {
142- accountId: accountId,
143- chatId: message.chatId(),
144- }) + `#message:${message.serverId || message.localId}`,
145- );
146- window.dispatchEvent(new PopStateEvent("popstate")); // Notify React Router
147- });
148- return notif;
149- }
150-};
151-
15287 const negatedScriptRegex = {
15388 Latn: /(?=\p{L})\P{Script=Latin}/u,
15489 Cyrl: /(?=\p{L})\P{Script=Cyrillic}/u,
Back to top