mirror of
https://github.com/block/buzz.git
synced 2026-11-08 18:31:56 +00:00
fix(desktop): make activity windows passive
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -684,10 +684,9 @@ export function AppShell() {
|
||||
open: isCompanionWindow ? undefined : settingsOpen,
|
||||
});
|
||||
useMarkAsReadShortcuts({
|
||||
activeChannelId: ownedEffects.markAsReadShortcuts
|
||||
? (activeChannel?.id ?? null)
|
||||
: null,
|
||||
activeChannelId: activeChannel?.id ?? null,
|
||||
activeChannelLastMessageAt: activeChannel?.lastMessageAt,
|
||||
enabled: ownedEffects.markAsReadShortcuts,
|
||||
markAllChannelsRead,
|
||||
markChannelRead,
|
||||
selectedView,
|
||||
|
||||
73
desktop/src/app/useMarkAsReadShortcuts.test.mjs
Normal file
73
desktop/src/app/useMarkAsReadShortcuts.test.mjs
Normal file
@@ -0,0 +1,73 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, afterEach, before, test } from "node:test";
|
||||
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
|
||||
url: "http://localhost",
|
||||
});
|
||||
|
||||
before(() => {
|
||||
Object.assign(globalThis, {
|
||||
document: dom.window.document,
|
||||
HTMLElement: dom.window.HTMLElement,
|
||||
IS_REACT_ACT_ENVIRONMENT: true,
|
||||
KeyboardEvent: dom.window.KeyboardEvent,
|
||||
window: dom.window,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const { cleanup } = await import("@testing-library/react");
|
||||
cleanup();
|
||||
});
|
||||
|
||||
after(() => dom.window.close());
|
||||
|
||||
async function renderShortcuts(enabled) {
|
||||
const { renderHook } = await import("@testing-library/react");
|
||||
const { useMarkAsReadShortcuts } = await import(
|
||||
"./useMarkAsReadShortcuts.ts"
|
||||
);
|
||||
const calls = [];
|
||||
const view = renderHook(() =>
|
||||
useMarkAsReadShortcuts({
|
||||
activeChannelId: "general",
|
||||
activeChannelLastMessageAt: "2026-08-24T00:00:00.000Z",
|
||||
enabled,
|
||||
markAllChannelsRead: () => calls.push("all"),
|
||||
markChannelRead: () => calls.push("channel"),
|
||||
selectedView: "channel",
|
||||
}),
|
||||
);
|
||||
return { calls, view };
|
||||
}
|
||||
|
||||
function pressEscape({ shiftKey = false } = {}) {
|
||||
window.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
key: "Escape",
|
||||
shiftKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test("disabled shortcuts ignore Escape and Shift+Escape", async () => {
|
||||
const { calls } = await renderShortcuts(false);
|
||||
|
||||
pressEscape();
|
||||
pressEscape({ shiftKey: true });
|
||||
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test("enabled shortcuts preserve channel and all-channel actions", async () => {
|
||||
const { calls } = await renderShortcuts(true);
|
||||
|
||||
pressEscape();
|
||||
pressEscape({ shiftKey: true });
|
||||
|
||||
assert.deepEqual(calls, ["channel", "all"]);
|
||||
});
|
||||
@@ -9,6 +9,7 @@ export function useMarkAsReadShortcuts({
|
||||
markAllChannelsRead,
|
||||
markChannelRead,
|
||||
selectedView,
|
||||
enabled = true,
|
||||
}: {
|
||||
activeChannelId: string | null;
|
||||
activeChannelLastMessageAt: string | null | undefined;
|
||||
@@ -18,8 +19,11 @@ export function useMarkAsReadShortcuts({
|
||||
lastMessageAt: string | null | undefined,
|
||||
) => void;
|
||||
selectedView: string;
|
||||
enabled?: boolean;
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
if (!enabled) return;
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key !== "Escape") return;
|
||||
if (event.defaultPrevented) return;
|
||||
@@ -51,6 +55,7 @@ export function useMarkAsReadShortcuts({
|
||||
}, [
|
||||
activeChannelId,
|
||||
activeChannelLastMessageAt,
|
||||
enabled,
|
||||
markAllChannelsRead,
|
||||
markChannelRead,
|
||||
selectedView,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { shouldShowAgentSessionUnavailable } from "./ChannelPane.helpers.ts";
|
||||
|
||||
test("unavailable activity state is exclusive to dedicated windows", () => {
|
||||
const unresolvedSession = {
|
||||
openAgentSessionPubkey: "agent-pubkey",
|
||||
selectedAgent: null,
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
shouldShowAgentSessionUnavailable({
|
||||
...unresolvedSession,
|
||||
isDedicatedActivityWindow: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldShowAgentSessionUnavailable({
|
||||
...unresolvedSession,
|
||||
isDedicatedActivityWindow: false,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -57,6 +57,22 @@ export function isChannelCreatedSystemMessage(message: TimelineMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldShowAgentSessionUnavailable({
|
||||
isDedicatedActivityWindow,
|
||||
openAgentSessionPubkey,
|
||||
selectedAgent,
|
||||
}: {
|
||||
isDedicatedActivityWindow: boolean;
|
||||
openAgentSessionPubkey: string | null;
|
||||
selectedAgent: unknown;
|
||||
}): boolean {
|
||||
return (
|
||||
isDedicatedActivityWindow &&
|
||||
openAgentSessionPubkey !== null &&
|
||||
selectedAgent == null
|
||||
);
|
||||
}
|
||||
|
||||
export function mentionsKnownAgent(
|
||||
mentionPubkeys: string[],
|
||||
knownAgentPubkeys: ReadonlySet<string>,
|
||||
|
||||
@@ -44,7 +44,10 @@ import {
|
||||
WelcomeComposerGuidanceLayer,
|
||||
} from "@/features/channels/ui/WelcomeComposerBanner";
|
||||
import { useWelcomeComposerBanner } from "@/features/channels/ui/useWelcomeComposerBanner";
|
||||
import { mentionsKnownAgent } from "@/features/channels/ui/ChannelPane.helpers";
|
||||
import {
|
||||
mentionsKnownAgent,
|
||||
shouldShowAgentSessionUnavailable,
|
||||
} from "@/features/channels/ui/ChannelPane.helpers";
|
||||
import { HuddleStartingView, HuddleTranscriptIntro } from "@/features/huddle";
|
||||
import { useChannelIntro } from "@/features/channels/ui/useChannelIntro";
|
||||
import type { ChannelPaneProps } from "@/features/channels/ui/ChannelPane.types";
|
||||
@@ -81,6 +84,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
historyExhausted,
|
||||
isFetchingOlder,
|
||||
isHuddleTranscript = false,
|
||||
isDedicatedActivityWindow = false,
|
||||
followThreadById,
|
||||
isFollowingThread,
|
||||
isFollowingThreadById,
|
||||
@@ -915,7 +919,11 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
);
|
||||
return wrapAux(panel, "agent-session-thread-panel");
|
||||
})()
|
||||
) : isSinglePanelView && openAgentSessionPubkey ? (
|
||||
) : shouldShowAgentSessionUnavailable({
|
||||
isDedicatedActivityWindow,
|
||||
openAgentSessionPubkey,
|
||||
selectedAgent,
|
||||
}) ? (
|
||||
<div
|
||||
className="flex min-h-0 flex-1 items-center justify-center p-8 text-center"
|
||||
data-testid="agent-session-unavailable"
|
||||
|
||||
@@ -52,6 +52,7 @@ export type ChannelPaneProps = {
|
||||
isFetchingOlder?: boolean;
|
||||
/** A companion huddle window presents the channel only as a transcript. */
|
||||
isHuddleTranscript?: boolean;
|
||||
isDedicatedActivityWindow?: boolean;
|
||||
isJoining?: boolean;
|
||||
isSinglePanelView?: boolean;
|
||||
isAgentSessionLoading?: boolean;
|
||||
|
||||
@@ -257,6 +257,7 @@ export function ChannelScreen({
|
||||
activeChannelId,
|
||||
activeChannelIsMember: activeChannel?.isMember,
|
||||
isHuddleTranscript,
|
||||
enabled: !isDedicatedActivityWindow,
|
||||
markChannelRead,
|
||||
messages: messagesQuery.data,
|
||||
resolvedMessages,
|
||||
@@ -701,8 +702,6 @@ export function ChannelScreen({
|
||||
const isNarrowPanelViewport =
|
||||
channelContentWidthPx > 0 &&
|
||||
channelContentWidthPx < AUXILIARY_PANEL_SINGLE_COLUMN_BREAKPOINT_PX;
|
||||
// A dedicated activity window is the panel itself, not a responsive channel
|
||||
// view. Keep the channel timeline out of that window at every width.
|
||||
const isSinglePanelView =
|
||||
isDedicatedActivityWindow ||
|
||||
(isNarrowPanelViewport &&
|
||||
@@ -871,6 +870,7 @@ export function ChannelScreen({
|
||||
onOpenMembers={handleOpenMembersSidebar}
|
||||
isFetchingOlder={isFetchingOlder}
|
||||
isHuddleTranscript={isHuddleTranscript}
|
||||
isDedicatedActivityWindow={isDedicatedActivityWindow}
|
||||
entranceMessageId={welcomeEntranceMessageId}
|
||||
onEntranceMessageComplete={handleWelcomeEntranceComplete}
|
||||
welcomeKickoffStage={welcomeKickoffStage}
|
||||
|
||||
@@ -39,7 +39,16 @@ export function useChannelOpenReadState(
|
||||
useAppShell();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!activeChannelId || !enabled || isChannelMember === false) return;
|
||||
if (
|
||||
!shouldAdvanceChannelReadState({
|
||||
activeChannelId,
|
||||
enabled,
|
||||
isChannelMember,
|
||||
}) ||
|
||||
!activeChannelId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
for (const itemId of getTopLevelInboxUnreadOverrideIds(
|
||||
locallyUnreadFeedItems,
|
||||
activeChannelId,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, afterEach, before, test } from "node:test";
|
||||
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
|
||||
url: "http://localhost",
|
||||
});
|
||||
|
||||
before(() => {
|
||||
Object.assign(globalThis, {
|
||||
document: dom.window.document,
|
||||
HTMLElement: dom.window.HTMLElement,
|
||||
IS_REACT_ACT_ENVIRONMENT: true,
|
||||
window: dom.window,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const { cleanup } = await import("@testing-library/react");
|
||||
cleanup();
|
||||
});
|
||||
|
||||
after(() => dom.window.close());
|
||||
|
||||
const topLevelMessage = {
|
||||
content: "message",
|
||||
created_at: 1_724_457_600,
|
||||
id: "message-id",
|
||||
kind: 9,
|
||||
pubkey: "author",
|
||||
sig: "signature",
|
||||
tags: [["h", "general"]],
|
||||
};
|
||||
|
||||
async function mountReadMarker(enabled) {
|
||||
const { renderHook } = await import("@testing-library/react");
|
||||
const { useHuddleReadMarker } = await import("./useHuddleReadMarker.ts");
|
||||
const calls = [];
|
||||
const view = renderHook(() =>
|
||||
useHuddleReadMarker({
|
||||
activeChannelId: "general",
|
||||
activeChannelIsMember: true,
|
||||
enabled,
|
||||
isHuddleTranscript: false,
|
||||
markChannelRead: (...args) => calls.push(args),
|
||||
messages: [topLevelMessage],
|
||||
resolvedMessages: [topLevelMessage],
|
||||
}),
|
||||
);
|
||||
return { calls, view };
|
||||
}
|
||||
|
||||
test("disabled activity companions do not advance the huddle read marker", async () => {
|
||||
const { calls } = await mountReadMarker(false);
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test("enabled channel screens retain the top-level read marker", async () => {
|
||||
const { calls } = await mountReadMarker(true);
|
||||
assert.deepEqual(calls, [
|
||||
["general", "2024-08-24T00:00:00.000Z", { topLevelOnly: true }],
|
||||
]);
|
||||
});
|
||||
@@ -13,6 +13,7 @@ type HuddleReadMarkerOptions = {
|
||||
activeChannelId: string | null;
|
||||
activeChannelIsMember: boolean | undefined;
|
||||
isHuddleTranscript: boolean;
|
||||
enabled?: boolean;
|
||||
markChannelRead: MarkChannelRead;
|
||||
messages: RelayEvent[] | undefined;
|
||||
resolvedMessages: RelayEvent[];
|
||||
@@ -22,6 +23,7 @@ export function useHuddleReadMarker({
|
||||
activeChannelId,
|
||||
activeChannelIsMember,
|
||||
isHuddleTranscript,
|
||||
enabled = true,
|
||||
markChannelRead,
|
||||
messages,
|
||||
resolvedMessages,
|
||||
@@ -65,7 +67,7 @@ export function useHuddleReadMarker({
|
||||
const lastHuddleReadKeyRef = React.useRef<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!activeChannelId || activeChannelIsMember === false) return;
|
||||
if (!enabled || !activeChannelId || activeChannelIsMember === false) return;
|
||||
const huddleReadKey = hasFlattenedHuddleReplies
|
||||
? `${activeChannelId}:${huddleReadAt}`
|
||||
: null;
|
||||
@@ -82,6 +84,7 @@ export function useHuddleReadMarker({
|
||||
}, [
|
||||
activeChannelId,
|
||||
activeChannelIsMember,
|
||||
enabled,
|
||||
hasFlattenedHuddleReplies,
|
||||
huddleReadAt,
|
||||
markChannelRead,
|
||||
|
||||
Reference in New Issue
Block a user