diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index ff21b07a6..05f8e2581 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -373,6 +373,7 @@ export function AppShell() { threadActivityFeedItems, getThreadReadAt, getMessageReadAt, + channels, ); const dueReminderBadge = useDueReminderBadgeCount( diff --git a/desktop/src/features/notifications/hooks.ts b/desktop/src/features/notifications/hooks.ts index e039d0eae..ccc9544f9 100644 --- a/desktop/src/features/notifications/hooks.ts +++ b/desktop/src/features/notifications/hooks.ts @@ -3,7 +3,7 @@ import * as React from "react"; import { useHomeFeedQuery } from "@/features/home/hooks"; import { useUsersBatchQuery } from "@/features/profile/hooks"; import type { UserProfileLookup } from "@/features/profile/lib/identity"; -import type { FeedItem, HomeFeedResponse } from "@/shared/api/types"; +import type { Channel, FeedItem, HomeFeedResponse } from "@/shared/api/types"; import { getDesktopNotificationPermissionState, requestDesktopNotificationAccess, @@ -376,6 +376,7 @@ export function useHomeFeedNotificationState( // has been revealed in-channel, even if the aggregate `thread:` marker // has not been advanced by opening Home. getMessageReadAt: (messageId: string) => number | null = () => null, + channels: ReadonlyArray> = [], ) { useFeedDesktopNotifications( feed, @@ -384,6 +385,7 @@ export function useHomeFeedNotificationState( setDesktopEnabled, profiles, mutedChannelIds, + channels, ); const normalizedPubkey = pubkey?.trim().toLowerCase() ?? ""; const [seenFeedIds, setSeenFeedIds] = React.useState(() => diff --git a/desktop/src/features/notifications/lib/feed.test.mjs b/desktop/src/features/notifications/lib/feed.test.mjs new file mode 100644 index 000000000..4f1c40f55 --- /dev/null +++ b/desktop/src/features/notifications/lib/feed.test.mjs @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { enrichFeedItemChannel, notificationTitle } from "./feed.ts"; + +const feedItem = (overrides = {}) => ({ + id: "event-id", + kind: 46011, + pubkey: "author", + content: "Please review", + createdAt: 1, + channelId: "channel-id", + channelName: "", + channelType: undefined, + tags: [["h", "channel-id"]], + category: "needs_action", + ...overrides, +}); + +const channels = [ + { id: "channel-id", name: "ship-room", channelType: "stream" }, +]; + +test("enriches a feed notification with its loaded channel name", () => { + const item = enrichFeedItemChannel(feedItem(), channels); + + assert.equal(item.channelName, "ship-room"); + assert.equal(item.channelType, "stream"); + assert.equal(notificationTitle(item), "Needs Action in #ship-room"); +}); + +test("preserves feed-provided channel metadata", () => { + const original = feedItem({ + channelName: "backend-name", + channelType: "forum", + }); + const item = enrichFeedItemChannel(original, channels); + + assert.equal(item, original); + assert.equal(notificationTitle(item), "Needs Action in #backend-name"); +}); + +test("falls back safely when the channel list has not loaded the channel", () => { + const original = feedItem(); + const item = enrichFeedItemChannel(original, []); + + assert.equal(item, original); + assert.equal(notificationTitle(item), "Needs Action"); +}); + +test("does not replace direct-message notification titles", () => { + const original = feedItem({ channelType: "dm" }); + const item = enrichFeedItemChannel(original, [ + { id: "channel-id", name: "not-a-title", channelType: "dm" }, + ]); + + assert.equal(notificationTitle(item, "Taylor"), "Taylor"); +}); diff --git a/desktop/src/features/notifications/lib/feed.ts b/desktop/src/features/notifications/lib/feed.ts index ff468e841..85a0680ad 100644 --- a/desktop/src/features/notifications/lib/feed.ts +++ b/desktop/src/features/notifications/lib/feed.ts @@ -1,9 +1,31 @@ -import type { FeedItem, HomeFeedResponse } from "@/shared/api/types"; +import type { Channel, FeedItem, HomeFeedResponse } from "@/shared/api/types"; import { formatNotificationTitle, truncateNotificationBody, } from "@/features/notifications/lib/notificationFormat"; +export type NotificationChannel = Pick; + +export function enrichFeedItemChannel( + item: FeedItem, + channels: readonly NotificationChannel[], +): FeedItem { + if (!item.channelId || item.channelName.trim()) { + return item; + } + + const channel = channels.find((candidate) => candidate.id === item.channelId); + if (!channel) { + return item; + } + + return { + ...item, + channelName: channel.name, + channelType: item.channelType ?? channel.channelType, + }; +} + export function notificationTitle(item: FeedItem, senderName?: string) { const channelLabel = item.channelType !== "dm" && item.channelName.trim() diff --git a/desktop/src/features/notifications/use-feed-desktop-notifications.ts b/desktop/src/features/notifications/use-feed-desktop-notifications.ts index ac2373c31..501a8fb65 100644 --- a/desktop/src/features/notifications/use-feed-desktop-notifications.ts +++ b/desktop/src/features/notifications/use-feed-desktop-notifications.ts @@ -10,6 +10,8 @@ import type { FeedItem, HomeFeedResponse } from "@/shared/api/types"; import { collectHomeAlertItems, eligibleFeedNotificationItems, + enrichFeedItemChannel, + type NotificationChannel, notificationBody, notificationTitle, } from "./lib/feed"; @@ -74,6 +76,7 @@ export function useFeedDesktopNotifications( setDesktopEnabled: (enabled: boolean) => Promise, profiles?: UserProfileLookup, mutedChannelIds?: ReadonlySet, + channels: readonly NotificationChannel[] = [], ) { const normalizedPubkey = pubkey?.trim().toLowerCase() ?? ""; const seenItemIdsRef = React.useRef>( @@ -192,7 +195,8 @@ export function useFeedDesktopNotifications( void autoRequestPermissionIfNeeded(); } - for (const item of newItems) { + for (const rawItem of newItems) { + const item = enrichFeedItemChannel(rawItem, channels); const resolvedLabel = profiles ? resolveUserLabel({ pubkey: item.pubkey, @@ -209,6 +213,7 @@ export function useFeedDesktopNotifications( } }, [ feed, + channels, mutedChannelIds, normalizedPubkey, profiles,