mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
co-authored by
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent
25396c06d6
commit
dd5592e34f
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user