feat(notifications): inline reply from a browser notification (#203)
CI / Build & Quality Checks (push) Successful in 1m38s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 7s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Successful in 2m50s

Message notifications shown through the service worker now carry a text-input
'Reply' action (Chrome desktop/Android). On notificationclick with
action==='reply' the SW sends the typed text itself — it already holds the
newest session's access token for authenticated media — as m.room.message
(threaded when the notification was for a thread), so it works with the tab in
the background or closed; a failed send shows a 'Reply not sent' notification
that opens the room. Not offered for encrypted rooms (the SW cannot encrypt).
The sender lives in swReply.ts so it is unit-tested; verified headless that
the SW notification carries actions + {roomId, threadId}.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-19 14:12:23 -04:00
co-authored by Claude Opus 5
parent 53a80adb57
commit d4420905e6
5 changed files with 152 additions and 2 deletions
+27
View File
@@ -1,6 +1,7 @@
/// <reference lib="WebWorker" />
import { precacheAndRoute, type PrecacheEntry } from 'workbox-precaching';
import { sendNotificationReply } from './swReply';
export type {};
declare const self: ServiceWorkerGlobalScope & {
@@ -147,6 +148,32 @@ self.addEventListener('notificationclick', (event: NotificationEvent) => {
const data = event.notification.data ?? {};
const path = typeof data.path === 'string' ? data.path : undefined;
// [Gitea #203] Reply action: send it and stop — no need to raise the app.
if (event.action === 'reply' && typeof data.roomId === 'string') {
const text = (event as NotificationEvent & { reply?: string }).reply?.trim();
if (text) {
event.waitUntil(
sendNotificationReply(
Array.from(sessions.values()).pop(),
data.roomId,
data.threadId,
text,
).then((ok) => {
if (!ok) {
// Let the user know it didn't go out, and keep their text reachable.
return self.registration.showNotification('Reply not sent', {
body: `Couldn't send "${text.slice(0, 60)}" — open the app to retry.`,
tag: `swreply-fail-${data.roomId}`,
data: { path },
});
}
return undefined;
}),
);
return;
}
}
event.waitUntil(
(async () => {
const windowClients = (