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
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:
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* [Gitea #203] Build and send an inline reply typed into a notification's
|
||||
* text action. Lives outside sw.ts so it can be unit-tested; the SW passes in
|
||||
* its newest known session and the global fetch.
|
||||
*/
|
||||
export type SwReplySession = { accessToken: string; baseUrl: string };
|
||||
|
||||
export const buildReplyContent = (text: string, threadId?: string): Record<string, unknown> => {
|
||||
const content: Record<string, unknown> = { msgtype: 'm.text', body: text, 'm.mentions': {} };
|
||||
if (threadId) {
|
||||
content['m.relates_to'] = {
|
||||
rel_type: 'm.thread',
|
||||
event_id: threadId,
|
||||
is_falling_back: true,
|
||||
'm.in_reply_to': { event_id: threadId },
|
||||
};
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
export const replySendUrl = (baseUrl: string, roomId: string, txnId: string): string =>
|
||||
new URL(
|
||||
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`,
|
||||
baseUrl,
|
||||
).href;
|
||||
|
||||
export async function sendNotificationReply(
|
||||
session: SwReplySession | undefined,
|
||||
roomId: string,
|
||||
threadId: string | undefined,
|
||||
text: string,
|
||||
fetchFn: typeof fetch = fetch,
|
||||
): Promise<boolean> {
|
||||
if (!session?.accessToken || !session.baseUrl) return false;
|
||||
const txn = `swreply-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
try {
|
||||
const res = await fetchFn(replySendUrl(session.baseUrl, roomId, txn), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(buildReplyContent(text, threadId)),
|
||||
});
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user