fix(desktop): add channel names to feed notifications

Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf
2026-07-16 20:10:51 -04:00
parent 1d8d006aaf
commit f7bae829cf
5 changed files with 91 additions and 3 deletions
+1
View File
@@ -373,6 +373,7 @@ export function AppShell() {
threadActivityFeedItems,
getThreadReadAt,
getMessageReadAt,
channels,
);
const dueReminderBadge = useDueReminderBadgeCount(
+3 -1
View File
@@ -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:<root>` marker
// has not been advanced by opening Home.
getMessageReadAt: (messageId: string) => number | null = () => null,
channels: ReadonlyArray<Pick<Channel, "id" | "name" | "channelType">> = [],
) {
useFeedDesktopNotifications(
feed,
@@ -384,6 +385,7 @@ export function useHomeFeedNotificationState(
setDesktopEnabled,
profiles,
mutedChannelIds,
channels,
);
const normalizedPubkey = pubkey?.trim().toLowerCase() ?? "";
const [seenFeedIds, setSeenFeedIds] = React.useState<string[]>(() =>
@@ -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");
});
+23 -1
View File
@@ -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<Channel, "id" | "name" | "channelType">;
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()
@@ -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<boolean>,
profiles?: UserProfileLookup,
mutedChannelIds?: ReadonlySet<string>,
channels: readonly NotificationChannel[] = [],
) {
const normalizedPubkey = pubkey?.trim().toLowerCase() ?? "";
const seenItemIdsRef = React.useRef<Set<string>>(
@@ -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,