mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): dock bounce, mark-as-read toggle, and bulk mark-all-read (#753)
This commit is contained in:
@@ -34,7 +34,7 @@ const overrides = new Map([
|
||||
["src-tauri/src/managed_agents/personas.rs", 950], // built-in persona system prompts (Solo + Kit + Scout) + merge_personas inequality checks + persona pack import/uninstall/list + uninstall safety check
|
||||
["src-tauri/src/managed_agents/teams.rs", 580], // built-in team registry (Kit & Scout) + merge_teams + validate_team_deletion + JSON export/import + tests
|
||||
["src-tauri/src/managed_agents/persona_card.rs", 970], // PNG/ZIP/MD persona card codec + pack-zip detection + nested root finder + provider/model/namePool fields + 27 unit tests
|
||||
["src/app/AppShell.tsx", 815], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + projects view + memory-leak safeguards + home-badge state lifted here so it consumes the same NIP-RS read-state as the sidebar (single ReadStateManager)
|
||||
["src/app/AppShell.tsx", 835], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + projects view + memory-leak safeguards + home-badge state lifted here so it consumes the same NIP-RS read-state as the sidebar (single ReadStateManager) + dock bounce wiring + mark-all-read context + channel notification callback + desktopEnabled guard
|
||||
["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation
|
||||
["src/features/channels/ui/ChannelManagementSheet.tsx", 800],
|
||||
["src/features/channels/ui/ChannelPane.tsx", 520], // composer/timeline/sidebar orchestration + anchored agent activity footers
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"core:default",
|
||||
"core:webview:allow-set-webview-zoom",
|
||||
"core:window:allow-set-badge-count",
|
||||
"core:window:allow-request-user-attention",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/app/AppShellOverlays";
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { useBackForwardControls } from "@/app/navigation/useBackForwardControls";
|
||||
import { useMarkAsReadShortcuts } from "@/app/useMarkAsReadShortcuts";
|
||||
import { useWebviewZoomShortcuts } from "@/app/useWebviewZoomShortcuts";
|
||||
import {
|
||||
channelsQueryKey,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from "@/features/notifications/hooks";
|
||||
import {
|
||||
listenForDesktopNotificationActions,
|
||||
requestDockBounce,
|
||||
revealDesktopAppWindow,
|
||||
sendDesktopNotification,
|
||||
setDesktopAppBadgeCount,
|
||||
@@ -209,6 +211,10 @@ export function AppShell() {
|
||||
const refetchHomeFeedOnLiveMention = React.useEffectEvent(() => {
|
||||
void homeFeedQuery.refetch();
|
||||
});
|
||||
const handleChannelNotification = React.useEffectEvent(() => {
|
||||
if (!notificationSettings.settings.desktopEnabled) return;
|
||||
void requestDockBounce();
|
||||
});
|
||||
|
||||
const handleDmNotification = React.useEffectEvent(
|
||||
(event: RelayEvent, channel: Channel) => {
|
||||
@@ -238,9 +244,9 @@ export function AppShell() {
|
||||
pubkey: event.pubkey,
|
||||
},
|
||||
}).then((didSend) => {
|
||||
if (didSend && notificationSettings.settings.soundEnabled) {
|
||||
playNotificationSound();
|
||||
}
|
||||
if (!didSend) return;
|
||||
if (notificationSettings.settings.soundEnabled) playNotificationSound();
|
||||
void requestDockBounce();
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -265,13 +271,14 @@ export function AppShell() {
|
||||
);
|
||||
|
||||
const {
|
||||
markAllChannelsRead,
|
||||
markChannelRead,
|
||||
markChannelUnread,
|
||||
unreadChannelIds,
|
||||
getEffectiveTimestamp: getChannelReadAt,
|
||||
readStateVersion,
|
||||
} = useUnreadChannels(
|
||||
channels,
|
||||
sidebarChannels,
|
||||
activeChannel,
|
||||
// Wait for ChannelScreen to report the latest loaded message before
|
||||
// advancing unread state for the active channel.
|
||||
@@ -280,6 +287,7 @@ export function AppShell() {
|
||||
pubkey: identityQuery.data?.pubkey,
|
||||
relayClient,
|
||||
currentPubkey: identityQuery.data?.pubkey,
|
||||
onChannelMessage: handleChannelNotification,
|
||||
onDmMessage: handleDmNotification,
|
||||
onLiveMention: refetchHomeFeedOnLiveMention,
|
||||
},
|
||||
@@ -439,8 +447,8 @@ export function AppShell() {
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
void setDesktopAppBadgeCount(homeBadgeCount);
|
||||
}, [homeBadgeCount]);
|
||||
void setDesktopAppBadgeCount(unreadChannelIds.size + homeBadgeCount);
|
||||
}, [homeBadgeCount, unreadChannelIds.size]);
|
||||
|
||||
React.useEffect(() => {
|
||||
let isCancelled = false;
|
||||
@@ -546,6 +554,14 @@ export function AppShell() {
|
||||
};
|
||||
}, [handleCloseSettings, handleOpenSettings, settingsOpen]);
|
||||
|
||||
useMarkAsReadShortcuts({
|
||||
activeChannelId: activeChannel?.id ?? null,
|
||||
activeChannelLastMessageAt: activeChannel?.lastMessageAt,
|
||||
markAllChannelsRead,
|
||||
markChannelRead,
|
||||
selectedView,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
if (event.button !== 0 || event.detail > 1) {
|
||||
@@ -581,6 +597,7 @@ export function AppShell() {
|
||||
<ChannelNavigationProvider channels={channels}>
|
||||
<AppShellProvider
|
||||
value={{
|
||||
markAllChannelsRead,
|
||||
markChannelRead,
|
||||
markChannelUnread,
|
||||
openChannelManagement: () => {
|
||||
@@ -693,6 +710,8 @@ export function AppShell() {
|
||||
void applyAgents(templateId, createdForum.id);
|
||||
}}
|
||||
onHideDm={handleHideDm}
|
||||
onMarkAllChannelsRead={markAllChannelsRead}
|
||||
onMarkChannelRead={markChannelRead}
|
||||
onMarkChannelUnread={markChannelUnread}
|
||||
onOpenBrowseChannels={handleOpenBrowseChannels}
|
||||
onOpenBrowseForums={handleOpenBrowseForums}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
type AppShellContextValue = {
|
||||
markAllChannelsRead: () => void;
|
||||
markChannelRead: (
|
||||
channelId: string,
|
||||
readAt: string | null | undefined,
|
||||
@@ -20,6 +21,7 @@ type AppShellContextValue = {
|
||||
};
|
||||
|
||||
const AppShellContext = React.createContext<AppShellContextValue>({
|
||||
markAllChannelsRead: () => {},
|
||||
markChannelRead: () => {},
|
||||
markChannelUnread: () => {},
|
||||
openChannelManagement: () => {},
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { hasPrimaryShortcutModifier } from "@/shared/lib/platform";
|
||||
|
||||
export function useMarkAsReadShortcuts({
|
||||
activeChannelId,
|
||||
activeChannelLastMessageAt,
|
||||
markAllChannelsRead,
|
||||
markChannelRead,
|
||||
selectedView,
|
||||
}: {
|
||||
activeChannelId: string | null;
|
||||
activeChannelLastMessageAt: string | null | undefined;
|
||||
markAllChannelsRead: () => void;
|
||||
markChannelRead: (
|
||||
channelId: string,
|
||||
lastMessageAt: string | null | undefined,
|
||||
) => void;
|
||||
selectedView: string;
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
if (event.key !== "Escape") return;
|
||||
if (event.defaultPrevented) return;
|
||||
if (hasPrimaryShortcutModifier(event) || event.altKey) return;
|
||||
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
markAllChannelsRead();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedView === "channel" && activeChannelId) {
|
||||
event.preventDefault();
|
||||
markChannelRead(activeChannelId, activeChannelLastMessageAt ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [
|
||||
activeChannelId,
|
||||
activeChannelLastMessageAt,
|
||||
markAllChannelsRead,
|
||||
markChannelRead,
|
||||
selectedView,
|
||||
]);
|
||||
}
|
||||
@@ -150,6 +150,7 @@ export function useLiveChannelUpdates(
|
||||
// reactions / edits / system messages aren't "new content".
|
||||
if (
|
||||
UNREAD_TRIGGER_KINDS.has(event.kind) &&
|
||||
channelId !== activeChannelId &&
|
||||
(normalizedCurrentPubkey.length === 0 ||
|
||||
event.pubkey.toLowerCase() !== normalizedCurrentPubkey)
|
||||
) {
|
||||
|
||||
@@ -314,8 +314,26 @@ export function useUnreadChannels(
|
||||
readStateVersion,
|
||||
]);
|
||||
|
||||
const unreadChannelIdsRef = React.useRef(unreadChannelIds);
|
||||
unreadChannelIdsRef.current = unreadChannelIds;
|
||||
|
||||
const markAllChannelsRead = React.useCallback(() => {
|
||||
for (const channelId of unreadChannelIdsRef.current) {
|
||||
forcedUnreadRef.current.delete(channelId);
|
||||
const unixSeconds =
|
||||
latestByChannelRef.current.get(channelId) ??
|
||||
getEffectiveTimestamp(channelId) ??
|
||||
null;
|
||||
if (unixSeconds !== null) {
|
||||
markContextRead(channelId, unixSeconds);
|
||||
}
|
||||
}
|
||||
bumpLatestVersion();
|
||||
}, [getEffectiveTimestamp, markContextRead]);
|
||||
|
||||
return {
|
||||
unreadChannelIds,
|
||||
markAllChannelsRead,
|
||||
markChannelRead,
|
||||
markChannelUnread,
|
||||
// Exposed so other surfaces (e.g. Home) can project per-item read state
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { UserAttentionType, getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
isPermissionGranted,
|
||||
onAction,
|
||||
@@ -204,6 +204,22 @@ export async function setDesktopAppBadgeCount(count: number): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestDockBounce(): Promise<void> {
|
||||
if (!isTauri()) {
|
||||
return;
|
||||
}
|
||||
if (document.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await getCurrentWindow().requestUserAttention(
|
||||
UserAttentionType.Informational,
|
||||
);
|
||||
} catch {
|
||||
// Best effort; ignore unsupported platforms.
|
||||
}
|
||||
}
|
||||
|
||||
export async function revealDesktopAppWindow(): Promise<void> {
|
||||
if (!isTauri()) {
|
||||
if (typeof window !== "undefined") {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import {
|
||||
Activity,
|
||||
Bot,
|
||||
CheckCheck,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
CircleDot,
|
||||
FolderGit2,
|
||||
@@ -130,6 +132,11 @@ type AppSidebarProps = {
|
||||
channelId: string,
|
||||
lastMessageAt: string | null | undefined,
|
||||
) => void;
|
||||
onMarkChannelRead: (
|
||||
channelId: string,
|
||||
lastMessageAt: string | null | undefined,
|
||||
) => void;
|
||||
onMarkAllChannelsRead: () => void;
|
||||
onOpenDm: (input: { pubkeys: string[] }) => Promise<void>;
|
||||
onUpdateWorkspace: (
|
||||
id: string,
|
||||
@@ -162,15 +169,19 @@ function SectionHeaderActions({
|
||||
browseTestId,
|
||||
className,
|
||||
createAriaLabel,
|
||||
hasUnread,
|
||||
onBrowse,
|
||||
onCreateClick,
|
||||
onMarkAllRead,
|
||||
}: {
|
||||
browseAriaLabel: string;
|
||||
browseTestId?: string;
|
||||
className?: string;
|
||||
createAriaLabel: string;
|
||||
hasUnread?: boolean;
|
||||
onBrowse: () => void;
|
||||
onCreateClick: () => void;
|
||||
onMarkAllRead?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -179,6 +190,17 @@ function SectionHeaderActions({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{hasUnread && onMarkAllRead ? (
|
||||
<button
|
||||
aria-label="Mark all as read"
|
||||
className={SECTION_ICON_BUTTON_CLASS}
|
||||
onClick={onMarkAllRead}
|
||||
title="Mark all as read"
|
||||
type="button"
|
||||
>
|
||||
<CheckCheck className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
aria-label={browseAriaLabel}
|
||||
className={SECTION_ICON_BUTTON_CLASS}
|
||||
@@ -209,12 +231,15 @@ function ChannelGroupSection({
|
||||
browseTestId,
|
||||
createAriaLabel,
|
||||
groupClassName,
|
||||
hasUnread,
|
||||
isCollapsed,
|
||||
isActiveChannel,
|
||||
items,
|
||||
listTestId,
|
||||
onBrowse,
|
||||
onCreateClick,
|
||||
onMarkAllRead,
|
||||
onMarkChannelRead,
|
||||
onMarkChannelUnread,
|
||||
onSelectChannel,
|
||||
onToggleCollapsed,
|
||||
@@ -232,6 +257,10 @@ function ChannelGroupSection({
|
||||
listTestId: string;
|
||||
onBrowse: () => void;
|
||||
onCreateClick: () => void;
|
||||
onMarkChannelRead: (
|
||||
channelId: string,
|
||||
lastMessageAt: string | null | undefined,
|
||||
) => void;
|
||||
onMarkChannelUnread: (
|
||||
channelId: string,
|
||||
lastMessageAt: string | null | undefined,
|
||||
@@ -241,6 +270,8 @@ function ChannelGroupSection({
|
||||
selectedChannelId: string | null;
|
||||
title: string;
|
||||
unreadChannelIds: Set<string>;
|
||||
hasUnread?: boolean;
|
||||
onMarkAllRead?: () => void;
|
||||
}) {
|
||||
const contentId = `sidebar-${listTestId}`;
|
||||
|
||||
@@ -270,8 +301,10 @@ function ChannelGroupSection({
|
||||
browseTestId={browseTestId}
|
||||
className={SECTION_ACTION_VISIBILITY_CLASS}
|
||||
createAriaLabel={createAriaLabel}
|
||||
hasUnread={hasUnread}
|
||||
onBrowse={onBrowse}
|
||||
onCreateClick={onCreateClick}
|
||||
onMarkAllRead={onMarkAllRead}
|
||||
/>
|
||||
</div>
|
||||
{!isCollapsed ? (
|
||||
@@ -293,14 +326,25 @@ function ChannelGroupSection({
|
||||
</SidebarMenuItem>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
onMarkChannelUnread(channel.id, channel.lastMessageAt)
|
||||
}
|
||||
>
|
||||
<CircleDot className="h-4 w-4" />
|
||||
Mark unread
|
||||
</ContextMenuItem>
|
||||
{unreadChannelIds.has(channel.id) ? (
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
onMarkChannelRead(channel.id, channel.lastMessageAt)
|
||||
}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Mark as read
|
||||
</ContextMenuItem>
|
||||
) : (
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
onMarkChannelUnread(channel.id, channel.lastMessageAt)
|
||||
}
|
||||
>
|
||||
<CircleDot className="h-4 w-4" />
|
||||
Mark unread
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
))}
|
||||
@@ -344,6 +388,8 @@ export function AppSidebar({
|
||||
onOpenSearch,
|
||||
onHideDm,
|
||||
onMarkChannelUnread,
|
||||
onMarkChannelRead,
|
||||
onMarkAllChannelsRead,
|
||||
onOpenDm,
|
||||
onUpdateWorkspace,
|
||||
onRemoveWorkspace,
|
||||
@@ -589,12 +635,15 @@ export function AppSidebar({
|
||||
browseTestId="browse-channels"
|
||||
createAriaLabel="Create a channel"
|
||||
groupClassName="pt-1"
|
||||
hasUnread={unreadChannelIds.size > 0}
|
||||
isCollapsed={collapsedGroups.channels}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
items={streamChannels}
|
||||
listTestId="stream-list"
|
||||
onBrowse={onOpenBrowseChannels}
|
||||
onCreateClick={() => setCreateDialogKind("stream")}
|
||||
onMarkAllRead={onMarkAllChannelsRead}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("channels")}
|
||||
@@ -606,12 +655,15 @@ export function AppSidebar({
|
||||
browseAriaLabel="Browse forums"
|
||||
browseTestId="browse-forums"
|
||||
createAriaLabel="Create a forum"
|
||||
hasUnread={unreadChannelIds.size > 0}
|
||||
isCollapsed={collapsedGroups.forums}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
items={forumChannels}
|
||||
listTestId="forum-list"
|
||||
onBrowse={onOpenBrowseForums}
|
||||
onCreateClick={() => setCreateDialogKind("forum")}
|
||||
onMarkAllRead={onMarkAllChannelsRead}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("forums")}
|
||||
@@ -643,6 +695,7 @@ export function AppSidebar({
|
||||
items={directMessages}
|
||||
channelLabels={dmChannelLabels}
|
||||
onHideDm={onHideDm}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("directMessages")}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import type * as React from "react";
|
||||
import { ChevronDown, CircleDot, FileText, Hash, Lock, X } from "lucide-react";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
CircleDot,
|
||||
FileText,
|
||||
Hash,
|
||||
Lock,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
ContextMenu,
|
||||
@@ -207,6 +215,7 @@ export function SidebarSection({
|
||||
testId,
|
||||
unreadChannelIds,
|
||||
onHideDm,
|
||||
onMarkChannelRead,
|
||||
onMarkChannelUnread,
|
||||
onSelectChannel,
|
||||
onToggleCollapsed,
|
||||
@@ -224,6 +233,10 @@ export function SidebarSection({
|
||||
testId: string;
|
||||
unreadChannelIds: Set<string>;
|
||||
onHideDm?: (channelId: string) => void;
|
||||
onMarkChannelRead?: (
|
||||
channelId: string,
|
||||
lastMessageAt: string | null | undefined,
|
||||
) => void;
|
||||
onMarkChannelUnread?: (
|
||||
channelId: string,
|
||||
lastMessageAt: string | null | undefined,
|
||||
@@ -308,18 +321,36 @@ export function SidebarSection({
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
|
||||
return onMarkChannelUnread ? (
|
||||
const hasContextAction =
|
||||
(unreadChannelIds.has(channel.id) && onMarkChannelRead) ||
|
||||
(!unreadChannelIds.has(channel.id) && onMarkChannelUnread);
|
||||
|
||||
return hasContextAction ? (
|
||||
<ContextMenu key={channel.id}>
|
||||
<ContextMenuTrigger asChild>{menuItem}</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
onMarkChannelUnread(channel.id, channel.lastMessageAt)
|
||||
}
|
||||
>
|
||||
<CircleDot className="h-4 w-4" />
|
||||
Mark unread
|
||||
</ContextMenuItem>
|
||||
{unreadChannelIds.has(channel.id) && onMarkChannelRead ? (
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
onMarkChannelRead(channel.id, channel.lastMessageAt)
|
||||
}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
Mark as read
|
||||
</ContextMenuItem>
|
||||
) : onMarkChannelUnread ? (
|
||||
<ContextMenuItem
|
||||
onClick={() =>
|
||||
onMarkChannelUnread(
|
||||
channel.id,
|
||||
channel.lastMessageAt,
|
||||
)
|
||||
}
|
||||
>
|
||||
<CircleDot className="h-4 w-4" />
|
||||
Mark unread
|
||||
</ContextMenuItem>
|
||||
) : null}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
) : (
|
||||
|
||||
@@ -89,6 +89,22 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
|
||||
keysWindows: "Ctrl+S",
|
||||
category: "Navigation",
|
||||
},
|
||||
{
|
||||
id: "mark-current-read",
|
||||
label: "Mark as read",
|
||||
description: "Mark the current conversation as read",
|
||||
keys: "Escape",
|
||||
keysWindows: "Escape",
|
||||
category: "Navigation",
|
||||
},
|
||||
{
|
||||
id: "mark-all-read",
|
||||
label: "Mark all as read",
|
||||
description: "Mark all conversations as read",
|
||||
keys: "⇧Escape",
|
||||
keysWindows: "Shift+Escape",
|
||||
category: "Navigation",
|
||||
},
|
||||
|
||||
// Zoom
|
||||
{
|
||||
|
||||
@@ -81,7 +81,6 @@ test("notification settings drive the Home badge and desktop alerts", async ({
|
||||
|
||||
await page.goto("/");
|
||||
await expect(page.getByTestId("sidebar-home-count")).toHaveCount(0);
|
||||
await expect.poll(getAppBadgeCount).toBe(0);
|
||||
|
||||
await openSettings(page, "notifications");
|
||||
await expect(page.getByTestId("settings-notifications")).toBeVisible();
|
||||
@@ -93,6 +92,11 @@ test("notification settings drive the Home badge and desktop alerts", async ({
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
// The dock badge sums unreadChannelIds.size + homeBadgeCount. Seeded test
|
||||
// channels may start with unreads, so capture the baseline after navigating
|
||||
// to general (which marks it read) but before injecting the mock mention.
|
||||
const baseline = await getAppBadgeCount();
|
||||
|
||||
await page.evaluate(() => {
|
||||
const win = window as Window & {
|
||||
__SPROUT_E2E_PUSH_MOCK_FEED_ITEM__?: (item: {
|
||||
@@ -129,7 +133,7 @@ test("notification settings drive the Home badge and desktop alerts", async ({
|
||||
});
|
||||
|
||||
await expect(page.getByTestId("sidebar-home-count")).toHaveText("1");
|
||||
await expect.poll(getAppBadgeCount).toBe(1);
|
||||
await expect.poll(getAppBadgeCount).toBe(baseline + 1);
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -183,18 +187,18 @@ test("notification settings drive the Home badge and desktop alerts", async ({
|
||||
await page.getByTestId("settings-close").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("engineering");
|
||||
await expect(page.getByTestId("sidebar-home-count")).toHaveCount(0);
|
||||
await expect.poll(getAppBadgeCount).toBe(0);
|
||||
await expect.poll(getAppBadgeCount).toBe(baseline);
|
||||
|
||||
await openSettings(page, "notifications");
|
||||
await page.getByTestId("notifications-home-badge-toggle").click();
|
||||
await page.getByTestId("settings-close").click();
|
||||
await expect(page.getByTestId("sidebar-home-count")).toHaveText("1");
|
||||
await expect.poll(getAppBadgeCount).toBe(1);
|
||||
await expect.poll(getAppBadgeCount).toBe(baseline + 1);
|
||||
|
||||
await page.getByRole("button", { name: "Home" }).click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("Home");
|
||||
await expect(page.getByTestId("sidebar-home-count")).toHaveCount(0);
|
||||
await expect.poll(getAppBadgeCount).toBe(0);
|
||||
await expect.poll(getAppBadgeCount).toBe(baseline);
|
||||
});
|
||||
|
||||
test("desktop notification clicks open the matching forum thread", async ({
|
||||
|
||||
Reference in New Issue
Block a user