mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
## Summary - open Huddles in a focused companion window with a clean handoff back to the in-app drawer and backing channel - redesign the participant film strip, sidebar control, transcript surface, and themed shell treatment - preserve microphone and device control across windows, start agent voice on the first reply, and show agent speaking activity in the film strip - give each agent a distinct session voice, beginning with the configured default, plus compact per-agent text-to-speech and voice controls - enroll only agents explicitly mentioned or deliberately added through an agent panel into the live Huddle roster - keep temporary Huddle channels out of the sidebar unless the user explicitly brings one into the main app - remove Huddle-only avatar policy badges and filter short silence or noise segments before speech-to-text posts ## Why The previous flow exposed the temporary channel as product UI, obscured who was present or speaking, and split transcript and audio state between the main and companion windows. This keeps backing channels as implementation details unless a user explicitly brings a Huddle into the app, while sharing the live conversation and audio lifecycle across both surfaces. Agent participants now join only after an explicit invitation, distinct voices make multi-agent Huddles easier to follow, and short microphone noise no longer becomes stray transcript messages. ## Validation - `pnpm check` - `pnpm build:e2e` - `pnpm exec playwright test tests/e2e/huddle-transcription.spec.ts --project=smoke` (13 passed) - Huddle sidebar visibility unit coverage (4 passed) - focused managed-agent and persona-mention E2E coverage (2 passed) - `pnpm test` (3,910 passed) - `cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings` - `cargo test --manifest-path desktop/src-tauri/Cargo.toml` (2,093 passed, 14 ignored; 3 diagnostics passed) --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
190 lines
5.3 KiB
TypeScript
190 lines
5.3 KiB
TypeScript
import * as React from "react";
|
|
|
|
import {
|
|
shouldBounceForChannelNotification,
|
|
toSearchHit,
|
|
} from "@/app/AppShell.helpers";
|
|
import { getThreadReference } from "@/features/messages/lib/threading";
|
|
import { hasMentionForEvent } from "@/features/notifications/lib/shouldNotify";
|
|
import type { NotificationSettings } from "@/features/notifications/hooks";
|
|
import {
|
|
listenForDesktopNotificationActions,
|
|
requestDockBounce,
|
|
revealDesktopAppWindow,
|
|
sendDesktopNotification,
|
|
} from "@/features/notifications/lib/desktop";
|
|
import {
|
|
formatNotificationTitle,
|
|
truncateNotificationBody,
|
|
} from "@/features/notifications/lib/notificationFormat";
|
|
import {
|
|
playNotificationSound,
|
|
resolveSlotSound,
|
|
} from "@/features/notifications/lib/sound";
|
|
import type { Channel, RelayEvent } from "@/shared/api/types";
|
|
|
|
export function useAppShellDesktopNotifications({
|
|
channels,
|
|
enabled,
|
|
goChannel,
|
|
goHome,
|
|
notificationSettings,
|
|
openSearchHit,
|
|
pubkey,
|
|
}: {
|
|
channels: Channel[];
|
|
enabled: boolean;
|
|
goChannel: (channelId: string) => Promise<unknown>;
|
|
goHome: () => Promise<unknown>;
|
|
notificationSettings: NotificationSettings;
|
|
openSearchHit: (
|
|
hit: import("@/shared/api/types").SearchHit,
|
|
) => Promise<unknown>;
|
|
pubkey?: string;
|
|
}) {
|
|
const handleChannelNotification = React.useEffectEvent(
|
|
(_channelId: string, event: RelayEvent) => {
|
|
if (!enabled) return;
|
|
if (!shouldBounceForChannelNotification(event.tags)) return;
|
|
if (!notificationSettings.desktopEnabled) return;
|
|
void requestDockBounce();
|
|
},
|
|
);
|
|
|
|
const handleDmNotification = React.useEffectEvent(
|
|
(event: RelayEvent, channel: Channel) => {
|
|
if (!enabled) return;
|
|
if (
|
|
!notificationSettings.desktopEnabled ||
|
|
!notificationSettings.slotAlertsEnabled.dm
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const channelName = channel.name?.trim() || "Direct message";
|
|
const body = truncateNotificationBody(event.content, "New message");
|
|
const threadRootId = getThreadReference(event.tags).rootId ?? null;
|
|
|
|
void sendDesktopNotification({
|
|
title: channelName,
|
|
body,
|
|
target: {
|
|
channelId: channel.id,
|
|
channelName,
|
|
content: event.content,
|
|
createdAt: event.created_at,
|
|
eventId: event.id,
|
|
kind: event.kind,
|
|
pubkey: event.pubkey,
|
|
threadRootId,
|
|
},
|
|
}).then((didSend) => {
|
|
if (!didSend) return;
|
|
playNotificationSound(resolveSlotSound(notificationSettings, "dm"));
|
|
void requestDockBounce();
|
|
});
|
|
},
|
|
);
|
|
|
|
const handleThreadReplyDesktopNotification = React.useEffectEvent(
|
|
(channelId: string, event: RelayEvent) => {
|
|
if (!enabled) return;
|
|
if (
|
|
!notificationSettings.desktopEnabled ||
|
|
!notificationSettings.slotAlertsEnabled.thread_reply
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Replies that @-mention the user are owned by the home-feed mention
|
|
// path — skip them here so they don't notify (and sound) twice.
|
|
const normalizedPubkey = pubkey?.trim().toLowerCase() ?? "";
|
|
if (hasMentionForEvent(event, normalizedPubkey)) {
|
|
return;
|
|
}
|
|
|
|
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: formatNotificationTitle({ prefix: "Reply", channelLabel }),
|
|
body,
|
|
target: {
|
|
channelId,
|
|
channelName,
|
|
content: event.content,
|
|
createdAt: event.created_at,
|
|
eventId: event.id,
|
|
kind: event.kind,
|
|
pubkey: event.pubkey,
|
|
threadRootId,
|
|
},
|
|
}).then((didSend) => {
|
|
if (!didSend) return;
|
|
playNotificationSound(
|
|
resolveSlotSound(notificationSettings, "thread_reply"),
|
|
);
|
|
void requestDockBounce();
|
|
});
|
|
},
|
|
);
|
|
|
|
const handleDesktopNotificationAction = React.useEffectEvent(
|
|
async (
|
|
target: import("@/features/notifications/lib/desktop").DesktopNotificationTarget,
|
|
) => {
|
|
await revealDesktopAppWindow();
|
|
|
|
if (!target.channelId) {
|
|
void goHome();
|
|
return;
|
|
}
|
|
|
|
const anchor = toSearchHit(target);
|
|
if (!anchor) {
|
|
await goChannel(target.channelId);
|
|
return;
|
|
}
|
|
|
|
await openSearchHit(anchor);
|
|
},
|
|
);
|
|
|
|
React.useEffect(() => {
|
|
if (!enabled) return;
|
|
let isCancelled = false;
|
|
let cleanup = () => {};
|
|
|
|
void listenForDesktopNotificationActions((target) => {
|
|
if (isCancelled) {
|
|
return;
|
|
}
|
|
|
|
void handleDesktopNotificationAction(target);
|
|
}).then((dispose) => {
|
|
if (isCancelled) {
|
|
dispose();
|
|
return;
|
|
}
|
|
|
|
cleanup = dispose;
|
|
});
|
|
|
|
return () => {
|
|
isCancelled = true;
|
|
cleanup();
|
|
};
|
|
}, [enabled]);
|
|
|
|
return {
|
|
handleChannelNotification,
|
|
handleDmNotification,
|
|
handleThreadReplyDesktopNotification,
|
|
};
|
|
}
|