mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
290 lines
7.4 KiB
TypeScript
290 lines
7.4 KiB
TypeScript
import { isThreadReply } from "@/features/messages/lib/threading";
|
|
import type { DesktopNotificationTarget } from "@/features/notifications/lib/desktop";
|
|
import type { SearchHit } from "@/shared/api/types";
|
|
|
|
export type MainOwnedEffects = {
|
|
agentRuntimeReconciliation: boolean;
|
|
autoRestart: boolean;
|
|
membershipNotifications: boolean;
|
|
presenceSession: boolean;
|
|
reminderNotifications: boolean;
|
|
markAsReadShortcuts: boolean;
|
|
};
|
|
|
|
export function mainOwnedEffects(isCompanionWindow: boolean): MainOwnedEffects {
|
|
const enabled = !isCompanionWindow;
|
|
return {
|
|
agentRuntimeReconciliation: enabled,
|
|
autoRestart: enabled,
|
|
membershipNotifications: enabled,
|
|
presenceSession: enabled,
|
|
reminderNotifications: enabled,
|
|
markAsReadShortcuts: enabled,
|
|
};
|
|
}
|
|
|
|
export type AppView =
|
|
| "home"
|
|
| "channel"
|
|
| "messages"
|
|
| "agents"
|
|
| "workflows"
|
|
| "pulse"
|
|
| "projects";
|
|
|
|
const WINDOW_DRAG_HANDLE_HEIGHT = 44;
|
|
const TAURI_DRAG_REGION_ATTR = "data-tauri-drag-region";
|
|
const WINDOW_DRAG_INTERACTIVE_SELECTOR =
|
|
'button, a, input, textarea, select, label, summary, [role="button"], [role="link"], [role="menuitem"], [role="tab"], [role="checkbox"], [role="radio"], [role="switch"], [role="option"], [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
|
|
|
|
const CLICKABLE_TAGS = new Set([
|
|
"A",
|
|
"BUTTON",
|
|
"INPUT",
|
|
"SELECT",
|
|
"TEXTAREA",
|
|
"LABEL",
|
|
"SUMMARY",
|
|
]);
|
|
const INTERACTIVE_ROLES = new Set([
|
|
"button",
|
|
"link",
|
|
"menuitem",
|
|
"tab",
|
|
"checkbox",
|
|
"radio",
|
|
"switch",
|
|
"option",
|
|
]);
|
|
|
|
function isClickableElement(element: HTMLElement) {
|
|
return (
|
|
CLICKABLE_TAGS.has(element.tagName) ||
|
|
(element.hasAttribute("contenteditable") &&
|
|
element.getAttribute("contenteditable") !== "false") ||
|
|
(element.hasAttribute("tabindex") &&
|
|
element.getAttribute("tabindex") !== "-1") ||
|
|
INTERACTIVE_ROLES.has(element.getAttribute("role") ?? "")
|
|
);
|
|
}
|
|
|
|
function isTauriDragRegionEvent(event: MouseEvent | PointerEvent) {
|
|
const path = event.composedPath();
|
|
const directTarget = path[0];
|
|
|
|
for (const item of path) {
|
|
if (!(item instanceof HTMLElement)) continue;
|
|
|
|
const attr = item.getAttribute(TAURI_DRAG_REGION_ATTR);
|
|
|
|
if (isClickableElement(item) && attr === null) return false;
|
|
if (attr === null) continue;
|
|
if (attr === "false") return false;
|
|
if (attr === "deep") return true;
|
|
if (attr === "" || attr === "true") return item === directTarget;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function isWindowDragHandleEvent(event: MouseEvent | PointerEvent) {
|
|
if (isTauriDragRegionEvent(event)) {
|
|
return true;
|
|
}
|
|
|
|
if (event.clientY > WINDOW_DRAG_HANDLE_HEIGHT) {
|
|
return false;
|
|
}
|
|
|
|
const target = event.target;
|
|
return !(
|
|
target instanceof Element &&
|
|
target.closest(WINDOW_DRAG_INTERACTIVE_SELECTOR)
|
|
);
|
|
}
|
|
|
|
export function shouldBounceForChannelNotification(tags: string[][]): boolean {
|
|
return !isThreadReply(tags);
|
|
}
|
|
|
|
export function markAllReadSources({
|
|
activeChannelId,
|
|
channelActivityItems,
|
|
markAllChannelReadMarkers,
|
|
markActiveChannelRead,
|
|
undoUnreadFeedItem,
|
|
unreadFeedItemIds,
|
|
}: {
|
|
activeChannelId: string | null;
|
|
channelActivityItems: ReadonlyArray<{
|
|
channelId: string | null;
|
|
createdAt: number;
|
|
}>;
|
|
markAllChannelReadMarkers: () => void;
|
|
markActiveChannelRead: (channelId: string, createdAt: number) => void;
|
|
undoUnreadFeedItem: (itemId: string) => void;
|
|
unreadFeedItemIds: ReadonlySet<string>;
|
|
}) {
|
|
for (const itemId of unreadFeedItemIds) {
|
|
undoUnreadFeedItem(itemId);
|
|
}
|
|
markAllChannelReadMarkers();
|
|
|
|
if (!activeChannelId) return;
|
|
|
|
let latestActivityAt: number | null = null;
|
|
for (const item of channelActivityItems) {
|
|
if (item.channelId !== activeChannelId) continue;
|
|
latestActivityAt = Math.max(latestActivityAt ?? 0, item.createdAt);
|
|
}
|
|
if (latestActivityAt !== null) {
|
|
markActiveChannelRead(activeChannelId, latestActivityAt);
|
|
}
|
|
}
|
|
|
|
export function toSearchHit(
|
|
target: DesktopNotificationTarget,
|
|
): SearchHit | null {
|
|
if (!target.eventId) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
eventId: target.eventId,
|
|
content: target.content ?? "",
|
|
kind: target.kind ?? 9,
|
|
pubkey: target.pubkey ?? "",
|
|
channelId: target.channelId,
|
|
channelName: target.channelName ?? null,
|
|
createdAt: target.createdAt ?? Math.floor(Date.now() / 1_000),
|
|
score: 0,
|
|
threadRootId: target.threadRootId ?? null,
|
|
};
|
|
}
|
|
|
|
export function createDesktopNotificationActivationQueue(
|
|
activate: (
|
|
target: DesktopNotificationTarget,
|
|
signal: AbortSignal,
|
|
) => Promise<void>,
|
|
onError?: (error: unknown) => void,
|
|
): {
|
|
cancel: () => void;
|
|
enqueue: (target: DesktopNotificationTarget) => void;
|
|
} {
|
|
const controller = new AbortController();
|
|
let pending = Promise.resolve();
|
|
|
|
return {
|
|
cancel: () => {
|
|
controller.abort();
|
|
},
|
|
enqueue: (target) => {
|
|
// Preserve native click order when macOS drains multiple queued targets.
|
|
// Contain failures so one rejected navigation cannot poison later clicks.
|
|
pending = pending
|
|
.then(() => {
|
|
if (!controller.signal.aborted) {
|
|
return activate(target, controller.signal);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
try {
|
|
onError?.(error);
|
|
} catch {
|
|
// Reporting must not poison the activation queue either.
|
|
}
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function activateDesktopNotificationTarget(
|
|
target: DesktopNotificationTarget,
|
|
actions: {
|
|
goChannel: (
|
|
channelId: string,
|
|
options?: { force?: boolean },
|
|
) => Promise<unknown>;
|
|
goHome: () => Promise<unknown>;
|
|
openSearchHit: (
|
|
hit: SearchHit,
|
|
behavior?: { force?: boolean; signal?: AbortSignal },
|
|
) => Promise<unknown>;
|
|
revealWindow: () => Promise<void>;
|
|
},
|
|
signal?: AbortSignal,
|
|
): Promise<void> {
|
|
if (signal?.aborted) {
|
|
return;
|
|
}
|
|
|
|
let navigation: Promise<unknown>;
|
|
if (!target.channelId) {
|
|
navigation = actions.goHome();
|
|
} else {
|
|
const anchor = toSearchHit(target);
|
|
navigation = anchor
|
|
? actions.openSearchHit(anchor, { force: true, signal })
|
|
: actions.goChannel(target.channelId, { force: true });
|
|
}
|
|
|
|
// Native activation already foregrounds the app on macOS. Other platforms
|
|
// still get a best-effort reveal, but it must never gate click-through.
|
|
void actions.revealWindow().catch(() => undefined);
|
|
await navigation;
|
|
}
|
|
|
|
export function deriveShellRoute(pathname: string): {
|
|
selectedChannelId: string | null;
|
|
selectedView: AppView;
|
|
} {
|
|
if (pathname.startsWith("/channels/")) {
|
|
const [, , rawChannelId] = pathname.split("/");
|
|
return {
|
|
selectedChannelId: rawChannelId ? decodeURIComponent(rawChannelId) : null,
|
|
selectedView: "channel",
|
|
};
|
|
}
|
|
|
|
if (pathname === "/messages/new") {
|
|
return {
|
|
selectedChannelId: null,
|
|
selectedView: "messages",
|
|
};
|
|
}
|
|
|
|
if (pathname === "/agents") {
|
|
return {
|
|
selectedChannelId: null,
|
|
selectedView: "agents",
|
|
};
|
|
}
|
|
|
|
if (pathname === "/workflows" || pathname.startsWith("/workflows/")) {
|
|
return {
|
|
selectedChannelId: null,
|
|
selectedView: "workflows",
|
|
};
|
|
}
|
|
|
|
if (pathname === "/projects" || pathname.startsWith("/projects/")) {
|
|
return {
|
|
selectedChannelId: null,
|
|
selectedView: "projects",
|
|
};
|
|
}
|
|
|
|
if (pathname === "/pulse") {
|
|
return {
|
|
selectedChannelId: null,
|
|
selectedView: "pulse",
|
|
};
|
|
}
|
|
|
|
return {
|
|
selectedChannelId: null,
|
|
selectedView: "home",
|
|
};
|
|
}
|