Skip to content

Commit 86a3ccb

Browse files
authored
fix: fetch room/sender avatar and display name for push notifications (#551)
### Description When the pusher is configured with `event_id_only` format, the service worker fetches event details itself rather than receiving them in the push payload. Previously only the sender display name and room name were retrieved — `room_avatar_url` was always `undefined`, so every notification showed the default app logo regardless of whether the room or sender had an avatar set. Display names could also be stale in the encrypted path when the app resumed from a cold state. **Changes in `src/sw.ts`:** - `fetchMemberDisplayName` → `fetchMemberInfo`: the same single HTTP request to `m.room.member` state now also extracts `avatar_url`, giving both display name and sender avatar for free - `fetchRoomAvatar`: new helper that fetches the `m.room.avatar` state event URL - `mxcToNotificationUrl`: converts `mxc://` to a legacy unauthenticated thumbnail URL (`/_matrix/media/v3/thumbnail/…`) so the OS can load the notification icon without an auth header - `handleMinimalPushPayload`: room avatar is now fetched in the initial parallel batch alongside the event and room name; icon is resolved as room avatar → member avatar (DM fallback) → default logo; `room_avatar_url` passed to all `handlePushNotificationPushData` call sites - Encrypted relay path now uses the server-fetched `senderDisplay` (authoritative) rather than the SDK cache value from the relay, which can be absent or stale when the app hasn't fully synced after a cold resume Fixes # #### Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings ### AI disclosure: - [ ] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [x] Fully AI generated (explain what all the generated code does in moderate detail). `fetchMemberInfo` extends the existing member state fetch to also return `avatar_url`. `fetchRoomAvatar` GETs `m.room.avatar` state. `mxcToNotificationUrl` builds a legacy `/_matrix/media/v3/thumbnail/` URL (no auth required, OS-loadable) from an `mxc://` URI. `handleMinimalPushPayload` adds `fetchRoomAvatar` to its parallel fetch batch, resolves the notification icon with room > member > default priority, and passes it through as `room_avatar_url` to every `handlePushNotificationPushData` invocation. The encrypted relay path is updated to prefer the server-fetched display name over the relay's potentially-stale SDK cache value.
2 parents 8e30086 + 32663bd commit 86a3ccb

2 files changed

Lines changed: 77 additions & 14 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
Fix push notifications missing sender/room avatar and showing stale display names when using event_id_only push format.

src/sw.ts

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -277,28 +277,73 @@ async function fetchRoomName(
277277
}
278278
}
279279

280+
type MemberInfo = {
281+
displayname: string | undefined;
282+
avatarUrl: string | undefined;
283+
};
284+
280285
/**
281-
* Fetch a room member's displayname from homeserver member state.
282-
* Returns undefined if the member has no displayname or the request fails.
286+
* Fetch a room member's state from the homeserver.
287+
* Returns displayname and avatar_url (both may be undefined).
283288
*/
284-
async function fetchMemberDisplayName(
289+
async function fetchMemberInfo(
285290
baseUrl: string,
286291
accessToken: string,
287292
roomId: string,
288293
userId: string
289-
): Promise<string | undefined> {
294+
): Promise<MemberInfo> {
290295
try {
291296
const url = `${baseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.member/${encodeURIComponent(userId)}`;
292297
const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } });
298+
if (!res.ok) return { displayname: undefined, avatarUrl: undefined };
299+
const data = (await res.json()) as Record<string, unknown>;
300+
const displayname =
301+
typeof data.displayname === 'string' && data.displayname.trim()
302+
? data.displayname.trim()
303+
: undefined;
304+
const avatarUrl =
305+
typeof data.avatar_url === 'string' && data.avatar_url.trim()
306+
? data.avatar_url.trim()
307+
: undefined;
308+
return { displayname, avatarUrl };
309+
} catch {
310+
return { displayname: undefined, avatarUrl: undefined };
311+
}
312+
}
313+
314+
/**
315+
* Fetch the m.room.avatar state event URL from the homeserver.
316+
* Returns undefined when the room has no avatar or the request fails.
317+
*/
318+
async function fetchRoomAvatar(
319+
baseUrl: string,
320+
accessToken: string,
321+
roomId: string
322+
): Promise<string | undefined> {
323+
try {
324+
const url = `${baseUrl}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.avatar`;
325+
const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } });
293326
if (!res.ok) return undefined;
294327
const data = (await res.json()) as Record<string, unknown>;
295-
const name = data.displayname;
296-
return typeof name === 'string' && name.trim() ? name.trim() : undefined;
328+
const avatarUrl = data.url;
329+
return typeof avatarUrl === 'string' && avatarUrl.trim() ? avatarUrl.trim() : undefined;
297330
} catch {
298331
return undefined;
299332
}
300333
}
301334

335+
/**
336+
* Convert an mxc:// URL to a legacy unauthenticated thumbnail URL.
337+
* Notification icons are fetched by the OS without auth headers, so we use
338+
* the pre-MSC3916 media endpoint which most homeservers still serve publicly.
339+
*/
340+
function mxcToNotificationUrl(mxcUrl: string, baseUrl: string): string | undefined {
341+
const match = mxcUrl.match(/^mxc:\/\/([^/]+)\/([^?#]+)/);
342+
if (!match) return undefined;
343+
const [, server, mediaId] = match;
344+
return `${baseUrl}/_matrix/media/v3/thumbnail/${encodeURIComponent(server)}/${encodeURIComponent(mediaId)}?width=96&height=96&method=crop`;
345+
}
346+
302347
/**
303348
* Return the first any-session we have stored (used for push fetches where we
304349
* don't have a client ID, e.g. when the app is backgrounded but still loaded).
@@ -386,10 +431,11 @@ async function handleMinimalPushPayload(
386431
return;
387432
}
388433

389-
// Fetch the raw event and room name state in parallel — both need only roomId.
390-
const [rawEvent, roomNameFromState] = await Promise.all([
434+
// Fetch the raw event, room name, and room avatar in parallel — all need only roomId.
435+
const [rawEvent, roomNameFromState, roomAvatarMxc] = await Promise.all([
391436
fetchRawEvent(session.baseUrl, session.accessToken, roomId, eventId),
392437
fetchRoomName(session.baseUrl, session.accessToken, roomId),
438+
fetchRoomAvatar(session.baseUrl, session.accessToken, roomId),
393439
]);
394440

395441
if (!rawEvent) {
@@ -406,13 +452,20 @@ async function handleMinimalPushPayload(
406452

407453
const eventType = rawEvent.type as string | undefined;
408454
const sender = rawEvent.sender as string | undefined;
409-
// Fetch sender's display name from room member state; fall back to MXID localpart.
410-
const senderDisplay =
411-
(sender
412-
? await fetchMemberDisplayName(session.baseUrl, session.accessToken, roomId, sender)
413-
: undefined) ?? (sender ? mxidLocalpart(sender) : 'Someone');
455+
// Fetch sender's member state — gives us both display name and avatar URL.
456+
const memberInfo = sender
457+
? await fetchMemberInfo(session.baseUrl, session.accessToken, roomId, sender)
458+
: { displayname: undefined, avatarUrl: undefined };
459+
// Fall back to MXID localpart when the server returns no displayname.
460+
const senderDisplay = memberInfo.displayname ?? (sender ? mxidLocalpart(sender) : 'Someone');
414461
// For DMs (no m.room.name state), use the sender's display name as the room name.
415462
const resolvedRoomName = roomNameFromState ?? senderDisplay;
463+
// Room avatar takes priority (group rooms); for DMs fall back to sender's member avatar.
464+
// Convert mxc:// to a legacy unauthenticated thumbnail URL so the OS can fetch it.
465+
const notificationAvatarUrl =
466+
(roomAvatarMxc ?? memberInfo.avatarUrl) !== undefined
467+
? mxcToNotificationUrl((roomAvatarMxc ?? memberInfo.avatarUrl)!, session.baseUrl)
468+
: undefined;
416469
const baseData = {
417470
room_id: roomId,
418471
event_id: eventId,
@@ -432,13 +485,16 @@ async function handleMinimalPushPayload(
432485

433486
if (result?.success) {
434487
// App was backgrounded but not frozen — decryption succeeded.
488+
// Prefer the server-fetched display name (authoritative) over the relay's SDK cache
489+
// value, which may be stale or missing if the SDK hasn't fully synced yet.
435490
await handlePushNotificationPushData({
436491
...baseData,
437492
type: result.eventType,
438493
content: result.content,
439-
sender_display_name: result.sender_display_name ?? senderDisplay,
494+
sender_display_name: senderDisplay,
440495
// Prefer relay's room name (has m.direct / computed SDK name); fall back to state fetch.
441496
room_name: result.room_name || resolvedRoomName,
497+
room_avatar_url: notificationAvatarUrl,
442498
});
443499
} else {
444500
// App is frozen or fully closed — show "Encrypted message" fallback.
@@ -448,6 +504,7 @@ async function handleMinimalPushPayload(
448504
content: {},
449505
sender_display_name: senderDisplay,
450506
room_name: resolvedRoomName,
507+
room_avatar_url: notificationAvatarUrl,
451508
});
452509
}
453510
} else {
@@ -458,6 +515,7 @@ async function handleMinimalPushPayload(
458515
content: rawEvent.content,
459516
sender_display_name: senderDisplay,
460517
room_name: resolvedRoomName,
518+
room_avatar_url: notificationAvatarUrl,
461519
});
462520
}
463521
}

0 commit comments

Comments
 (0)