refactor(desktop): consolidate notification helpers and add channel names to toasts (#1286)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Will Pfleger
2026-06-25 13:07:04 -04:00
committed by GitHub
co-authored by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent 25396c06d6
commit dd5592e34f
5 changed files with 111 additions and 45 deletions
+3 -2
View File
@@ -154,9 +154,12 @@ export function AppShell() {
const { feedProfilesQuery, homeFeedQuery, notificationSettings } =
useHomeFeedNotifications(identityQuery.data?.pubkey);
const feedItemState = useFeedItemState(identityQuery.data?.pubkey);
const channelsQuery = useChannelsQuery();
const channels = channelsQuery.data ?? [];
useReminderNotifications(
identityQuery.data?.pubkey,
notificationSettings.settings,
channels,
);
const refetchHomeFeedFromLiveSignal = React.useEffectEvent(() => {
void homeFeedQuery.refetch();
@@ -165,9 +168,7 @@ export function AppShell() {
identityQuery.data?.pubkey,
refetchHomeFeedFromLiveSignal,
);
const channelsQuery = useChannelsQuery();
const { refetch: refetchChannels } = channelsQuery;
const channels = channelsQuery.data ?? [];
const channelsErrorMessage =
channelsQuery.error instanceof Error
? channelsQuery.error.message
@@ -13,6 +13,10 @@ import {
revealDesktopAppWindow,
sendDesktopNotification,
} from "@/features/notifications/lib/desktop";
import {
formatNotificationTitle,
truncateNotificationBody,
} from "@/features/notifications/lib/notificationFormat";
import {
playNotificationSound,
resolveSlotSound,
@@ -54,13 +58,7 @@ export function useAppShellDesktopNotifications({
}
const channelName = channel.name?.trim() || "Direct message";
const content = event.content.trim();
const body =
content.length > 0
? content.length > 140
? `${content.slice(0, 137).trimEnd()}...`
: content
: "New message";
const body = truncateNotificationBody(event.content, "New message");
const threadRootId = getThreadReference(event.tags).rootId ?? null;
void sendDesktopNotification({
@@ -100,19 +98,16 @@ export function useAppShellDesktopNotifications({
return;
}
const channel = channels.find((entry) => entry.id === channelId);
const channelName = channel?.name?.trim() || "Thread";
const content = event.content.trim();
const body =
content.length > 0
? content.length > 140
? `${content.slice(0, 137).trimEnd()}...`
: content
: "New reply";
const resolvedChannel = channels.find((c) => c.id === channelId);
const channelName = resolvedChannel?.name?.trim() ?? null;
// channelLabel is "#name" for the toast title; channelName is the raw
// name stored in the navigation target for click-through routing.
const channelLabel = channelName ? `#${channelName}` : null;
const body = truncateNotificationBody(event.content, "New reply");
const threadRootId = getThreadReference(event.tags).rootId ?? null;
void sendDesktopNotification({
title: `Reply in ${channelName}`,
title: formatNotificationTitle({ prefix: "Reply", channelLabel }),
body,
target: {
channelId,
+23 -22
View File
@@ -1,46 +1,47 @@
import type { FeedItem, HomeFeedResponse } from "@/shared/api/types";
const FEED_NOTIFICATION_BODY_MAX_LENGTH = 140;
import {
formatNotificationTitle,
truncateNotificationBody,
} from "@/features/notifications/lib/notificationFormat";
export function notificationTitle(item: FeedItem, senderName?: string) {
const channelLabel = item.channelName.trim()
? ` in #${item.channelName.trim()}`
: "";
const channelLabel =
item.channelType !== "dm" && item.channelName.trim()
? `#${item.channelName.trim()}`
: null;
if (item.channelType === "dm") {
return senderName || "Direct message";
}
if (item.category === "mention") {
return senderName
? `${senderName} mentioned you${channelLabel}`
: `@Mention${channelLabel}`;
return formatNotificationTitle({
prefix: senderName ? `${senderName} mentioned you` : "@Mention",
channelLabel,
});
}
if (item.kind === 46010) {
return senderName
? `${senderName} requested approval${channelLabel}`
: `Approval Requested${channelLabel}`;
return formatNotificationTitle({
prefix: senderName
? `${senderName} requested approval`
: "Approval Requested",
channelLabel,
});
}
return senderName
? `${senderName}${channelLabel}`
: `Needs Action${channelLabel}`;
return formatNotificationTitle({
prefix: senderName ? senderName : "Needs Action",
channelLabel,
});
}
export function notificationBody(item: FeedItem) {
const content = item.content.trim();
const fallback =
item.kind === 46010
? "A workflow is waiting for your approval."
: "Something in Buzz needs your attention.";
const body = content.length > 0 ? content : fallback;
if (body.length <= FEED_NOTIFICATION_BODY_MAX_LENGTH) {
return body;
}
return `${body.slice(0, FEED_NOTIFICATION_BODY_MAX_LENGTH - 3).trimEnd()}...`;
return truncateNotificationBody(item.content, fallback);
}
export function collectHomeAlertItems(feed: HomeFeedResponse) {
@@ -0,0 +1,49 @@
const NOTIFICATION_BODY_MAX_LENGTH = 140;
/**
* Resolve a channel's display label for use in notification titles.
*
* Returns `"#channelName"` when the channel is found and has a non-empty name,
* or `null` when the channelId is absent or the channel is not yet in the list
* (e.g. channels query hasn't resolved — the caller should fall back gracefully
* rather than blocking the toast).
*/
export function resolveNotificationChannelLabel(
channelId: string | null | undefined,
channels: ReadonlyArray<{ id: string; name?: string | null }>,
): string | null {
if (!channelId) return null;
const channel = channels.find((c) => c.id === channelId);
const name = channel?.name?.trim();
return name ? `#${name}` : null;
}
/**
* Truncate notification body text to {@link NOTIFICATION_BODY_MAX_LENGTH}
* characters, appending "..." when truncated. Returns `fallback` when
* `content` is blank after trimming.
*/
export function truncateNotificationBody(
content: string,
fallback: string,
): string {
const trimmed = content.trim();
if (trimmed.length === 0) return fallback;
if (trimmed.length <= NOTIFICATION_BODY_MAX_LENGTH) return trimmed;
return `${trimmed.slice(0, NOTIFICATION_BODY_MAX_LENGTH - 3).trimEnd()}...`;
}
/**
* Format a notification title with optional channel context.
*
* - With a channel label: `"prefix in #channel"`
* - Without: `"prefix"`
*/
export function formatNotificationTitle(opts: {
prefix: string;
channelLabel: string | null;
}): string {
return opts.channelLabel
? `${opts.prefix} in ${opts.channelLabel}`
: opts.prefix;
}
@@ -12,6 +12,11 @@ import {
sendDesktopNotification,
} from "@/features/notifications/lib/desktop";
import type { NotificationSettings } from "@/features/notifications/hooks";
import {
formatNotificationTitle,
resolveNotificationChannelLabel,
truncateNotificationBody,
} from "@/features/notifications/lib/notificationFormat";
import {
playNotificationSound,
resolveSlotSound,
@@ -53,6 +58,7 @@ function readWatermark(pubkey: string): number {
export function useReminderNotifications(
pubkey: string | undefined,
settings: NotificationSettings,
channels: ReadonlyArray<{ id: string; name?: string | null }>,
): void {
const reminders = useRemindersQuery(pubkey).data;
const queryClient = useQueryClient();
@@ -60,6 +66,8 @@ export function useReminderNotifications(
remindersRef.current = reminders ?? [];
const settingsRef = React.useRef(settings);
settingsRef.current = settings;
const channelsRef = React.useRef(channels);
channelsRef.current = channels;
// Track whether the query has resolved at least once. On mount,
// useRemindersQuery is still loading (data === undefined), so
@@ -78,15 +86,27 @@ export function useReminderNotifications(
return;
}
// For a single reminder, try to resolve the channel name from its target.
// Multiple reminders may span different channels, so we omit channel context
// in that case and let the body count speak for itself.
const channelLabel =
due.length === 1
? resolveNotificationChannelLabel(
due[0].content.target?.channelId ?? null,
channelsRef.current,
)
: null;
const body =
due.length === 1
? (due[0].content.target?.preview ??
due[0].content.note ??
"A reminder is waiting")
? truncateNotificationBody(
due[0].content.target?.preview ?? due[0].content.note ?? "",
"A reminder is waiting",
)
: `${due.length} reminders are due`;
void sendDesktopNotification({
title: "Reminder due",
title: formatNotificationTitle({ prefix: "Reminder due", channelLabel }),
body,
}).then((didSend) => {
if (!didSend) return;