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
Move notifications code to one file
Eric Roberts
2026-11-04T14:34:43ZI need to refactor the way we actually send notifications for our Tauri work, and we have a bunch of places doing the same service worker check. We had one method in util, others in specific components. I think keeping it together here makes sense for now, until I change my mind about this later. This commit simply moves the files and renames functions without behaviour changes, so that the actual refactoring work will be easier to read in the diff.
Semantic diff summary
0 added,
9 modified,
0 signature changed,
0 removed
across 5 analyzed files
+3
-9
src/App.tsx
#
| ... | ... | @@ -13,14 +13,8 @@ import { | |
| 13 | 13 | useNavigate, | |
| 14 | 14 | useParams, | |
| 15 | 15 | } from "react-router"; | |
| 16 | - | import { | |
| 17 | - | ios, | |
| 18 | - | webkit, | |
| 19 | - | href, | |
| 20 | - | useEffectEvent, | |
| 21 | - | showNotification, | |
| 22 | - | serviceWorker, | |
| 23 | - | } from "./util"; | |
| 16 | + | import { ios, webkit, href, useEffectEvent, serviceWorker } from "./util"; | |
| 17 | + | import { showMessageNotification } from "./notifications"; | |
| 24 | 18 | import { SettingsContext, SettingsData } from "./components/Settings"; | |
| 25 | 19 | import { Call } from "./components/Call"; | |
| 26 | 20 | import { LoginRequest } from "./components/LoginRequest"; |
| ... | ... | @@ -108,7 +102,7 @@ const AppInner = () => { | |
| 108 | 102 | (chat.notifyReply() && message.replyToMessage && members[1]?.isSelf) | |
| 109 | 103 | ) { | |
| 110 | 104 | if (settings.audibleNotifications) new Audio("/message.opus").play(); | |
| 111 | - | await showNotification( | |
| 105 | + | await showMessageNotification( | |
| 112 | 106 | client.accountId(), | |
| 113 | 107 | members[0] ?? message.senderMemberStub(), | |
| 114 | 108 | message, |
+1
-48
src/components/Call.tsx
#
| ... | ... | @@ -28,6 +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 | 32 | ||
| 32 | 33 | const playTone = (frequency1: number, frequency2: number, duration: number) => { | |
| 33 | 34 | const audioContext = new AudioContext(); |
| ... | ... | @@ -122,54 +123,6 @@ const playRingtone = () => { | |
| 122 | 123 | }; | |
| 123 | 124 | }; | |
| 124 | 125 | ||
| 125 | - | const showCallNotification = async ( | |
| 126 | - | accountId: string, | |
| 127 | - | session: borogove.calls.Session, | |
| 128 | - | callerDisplayName: string, | |
| 129 | - | ) => { | |
| 130 | - | if (!Notification?.permission) return null; | |
| 131 | - | ||
| 132 | - | const serviceWorker = await readyServiceWorker; | |
| 133 | - | const tag = accountId + ">" + session.get_chatId() + ">call"; | |
| 134 | - | if (serviceWorker) { | |
| 135 | - | const notifs = await serviceWorker.getNotifications({ | |
| 136 | - | tag: tag, | |
| 137 | - | }); | |
| 138 | - | if (notifs.length > 0) return null; // Already notified, probably from push | |
| 139 | - | // We could check session id here, but we only do one call at a time in UI anyway | |
| 140 | - | } | |
| 141 | - | const title = "Incoming Call"; | |
| 142 | - | const options = { | |
| 143 | - | body: "From " + callerDisplayName, | |
| 144 | - | tag: tag, | |
| 145 | - | vibrate: [50, 100, 50, 100], | |
| 146 | - | data: { | |
| 147 | - | accountId: accountId, | |
| 148 | - | chatId: session.get_chatId(), | |
| 149 | - | type: borogove.MessageType.MessageCall, | |
| 150 | - | callStatus: "propose", | |
| 151 | - | }, | |
| 152 | - | }; | |
| 153 | - | ||
| 154 | - | let notification = null; | |
| 155 | - | if (serviceWorker) { | |
| 156 | - | await serviceWorker.showNotification(title, options); | |
| 157 | - | } else { | |
| 158 | - | notification = new Notification(title, options); | |
| 159 | - | ||
| 160 | - | let clicked = false; | |
| 161 | - | notification.addEventListener("click", () => { | |
| 162 | - | clicked = true; | |
| 163 | - | session.accept(); | |
| 164 | - | }); | |
| 165 | - | notification.addEventListener("close", () => { | |
| 166 | - | if (!clicked) session.hangup(); | |
| 167 | - | }); | |
| 168 | - | } | |
| 169 | - | ||
| 170 | - | return notification; | |
| 171 | - | }; | |
| 172 | - | ||
| 173 | 126 | export const Call = ({ | |
| 174 | 127 | accounts, | |
| 175 | 128 | }: { |
+2
-19
src/components/LoginRequest.tsx
#
| ... | ... | @@ -7,23 +7,6 @@ import { Button } from "@chatscope/chat-ui-kit-react"; | |
| 7 | 7 | import { faGripHorizontal } from "@fortawesome/free-solid-svg-icons"; | |
| 8 | 8 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | |
| 9 | 9 | ||
| 10 | - | const showNotification = async (req: borogove.LoginRequest) => { | |
| 11 | - | if (!Notification?.permission) return null; | |
| 12 | - | ||
| 13 | - | const title = "Login Request"; | |
| 14 | - | const options = { | |
| 15 | - | body: req.method + " " + req.url, | |
| 16 | - | tag: req.id, | |
| 17 | - | }; | |
| 18 | - | ||
| 19 | - | const serviceWorker = await readyServiceWorker; | |
| 20 | - | if (serviceWorker) { | |
| 21 | - | await serviceWorker.showNotification(title, options); | |
| 22 | - | } else { | |
| 23 | - | new Notification(title, options); | |
| 24 | - | } | |
| 25 | - | }; | |
| 26 | - | ||
| 27 | 10 | export const LoginRequest = ({ | |
| 28 | 11 | accounts, | |
| 29 | 12 | }: { |
+134
-0
src/notifications.ts
#
| ... | ... | @@ -0,0 +1,134 @@ | |
| 1 | + | import * as borogove from "borogove"; | |
| 2 | + | import { href, readyServiceWorker } from "./util"; | |
| 3 | + | ||
| 4 | + | export const showMessageNotification = async ( | |
| 5 | + | accountId: string, | |
| 6 | + | sender: borogove.Member, | |
| 7 | + | message: borogove.ChatMessage, | |
| 8 | + | ) => { | |
| 9 | + | if (!Notification?.permission) return; | |
| 10 | + | ||
| 11 | + | const serviceWorker = await readyServiceWorker; | |
| 12 | + | const tag = accountId + ">" + message.chatId(); | |
| 13 | + | if (serviceWorker) { | |
| 14 | + | // Check if we already notified this message | |
| 15 | + | const notifs = await serviceWorker.getNotifications({ | |
| 16 | + | tag: tag, | |
| 17 | + | }); | |
| 18 | + | for (const notif of notifs) { | |
| 19 | + | if (notif.data.messageId === (message.serverId || message.localId)) | |
| 20 | + | return; | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | const title = "Message from " + (sender?.displayName ?? message.chatId()); | |
| 25 | + | let icon = sender.photoUri ?? sender.placeholderUri; | |
| 26 | + | if (!icon.startsWith("data:")) { | |
| 27 | + | const response = await fetch(icon); | |
| 28 | + | const mime = response.headers.get("Content-Type"); | |
| 29 | + | if (response.ok && mime) { | |
| 30 | + | const buffer = await response.arrayBuffer(); | |
| 31 | + | const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer))); | |
| 32 | + | icon = `data:${mime};base64,${base64}`; | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | const body = message.body().toPlainText(); | |
| 36 | + | const options = { | |
| 37 | + | body: !body && message.attachments.length > 0 ? "Media" : body, | |
| 38 | + | icon, | |
| 39 | + | tag, | |
| 40 | + | renotify: true, | |
| 41 | + | timestamp: new Date(message.timeSent).getTime(), | |
| 42 | + | vibrate: [50, 50, 50], | |
| 43 | + | data: { | |
| 44 | + | accountId, | |
| 45 | + | chatId: message.chatId(), | |
| 46 | + | messageId: message.serverId || message.localId, | |
| 47 | + | timestamp: message.timeSent, | |
| 48 | + | }, | |
| 49 | + | }; | |
| 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 | + | } | |
| 67 | + | }; | |
| 68 | + | ||
| 69 | + | export const showCallNotification = async ( | |
| 70 | + | accountId: string, | |
| 71 | + | session: borogove.calls.Session, | |
| 72 | + | callerDisplayName: string, | |
| 73 | + | ) => { | |
| 74 | + | if (!Notification?.permission) return null; | |
| 75 | + | ||
| 76 | + | const serviceWorker = await readyServiceWorker; | |
| 77 | + | const tag = accountId + ">" + session.get_chatId() + ">call"; | |
| 78 | + | if (serviceWorker) { | |
| 79 | + | const notifs = await serviceWorker.getNotifications({ | |
| 80 | + | tag: tag, | |
| 81 | + | }); | |
| 82 | + | if (notifs.length > 0) return null; // Already notified, probably from push | |
| 83 | + | // We could check session id here, but we only do one call at a time in UI anyway | |
| 84 | + | } | |
| 85 | + | const title = "Incoming Call"; | |
| 86 | + | const options = { | |
| 87 | + | body: "From " + callerDisplayName, | |
| 88 | + | tag: tag, | |
| 89 | + | vibrate: [50, 100, 50, 100], | |
| 90 | + | data: { | |
| 91 | + | accountId: accountId, | |
| 92 | + | chatId: session.get_chatId(), | |
| 93 | + | type: borogove.MessageType.MessageCall, | |
| 94 | + | callStatus: "propose", | |
| 95 | + | }, | |
| 96 | + | }; | |
| 97 | + | ||
| 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; | |
| 115 | + | }; | |
| 116 | + | ||
| 117 | + | export const showLoginRequestNotification = async ( | |
| 118 | + | req: borogove.LoginRequest, | |
| 119 | + | ) => { | |
| 120 | + | if (!Notification?.permission) return null; | |
| 121 | + | ||
| 122 | + | const title = "Login Request"; | |
| 123 | + | const options = { | |
| 124 | + | body: req.method + " " + req.url, | |
| 125 | + | tag: req.id, | |
| 126 | + | }; | |
| 127 | + | ||
| 128 | + | const serviceWorker = await readyServiceWorker; | |
| 129 | + | if (serviceWorker) { | |
| 130 | + | await serviceWorker.showNotification(title, options); | |
| 131 | + | } else { | |
| 132 | + | new Notification(title, options); | |
| 133 | + | } | |
| 134 | + | }; |
+0
-65
src/util.ts
#
| ... | ... | @@ -84,71 +84,6 @@ export const useChatExtras = (accountId: string) => { | |
| 84 | 84 | }; | |
| 85 | 85 | }; | |
| 86 | 86 | ||
| 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 | - | ||
| 152 | 87 | const negatedScriptRegex = { | |
| 153 | 88 | Latn: /(?=\p{L})\P{Script=Latin}/u, | |
| 154 | 89 | Cyrl: /(?=\p{L})\P{Script=Cyrillic}/u, |