Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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", 830], // 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
["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
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 24 additions & 6 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +27,7 @@ import {
} from "@/features/notifications/hooks";
import {
listenForDesktopNotificationActions,
requestDockBounce,
revealDesktopAppWindow,
sendDesktopNotification,
setDesktopAppBadgeCount,
Expand Down Expand Up @@ -209,6 +211,9 @@ export function AppShell() {
const refetchHomeFeedOnLiveMention = React.useEffectEvent(() => {
void homeFeedQuery.refetch();
});
const handleChannelNotification = React.useEffectEvent(() => {
void requestDockBounce();
});
Comment thread
wpfleger96 marked this conversation as resolved.
Resolved

const handleDmNotification = React.useEffectEvent(
(event: RelayEvent, channel: Channel) => {
Expand Down Expand Up @@ -238,9 +243,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();
});
},
);
Expand All @@ -265,13 +270,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.
Expand All @@ -280,6 +286,7 @@ export function AppShell() {
pubkey: identityQuery.data?.pubkey,
relayClient,
currentPubkey: identityQuery.data?.pubkey,
onChannelMessage: handleChannelNotification,
onDmMessage: handleDmNotification,
onLiveMention: refetchHomeFeedOnLiveMention,
},
Expand Down Expand Up @@ -439,8 +446,8 @@ export function AppShell() {
}, []);

React.useEffect(() => {
void setDesktopAppBadgeCount(homeBadgeCount);
}, [homeBadgeCount]);
void setDesktopAppBadgeCount(unreadChannelIds.size + homeBadgeCount);
}, [homeBadgeCount, unreadChannelIds.size]);

React.useEffect(() => {
let isCancelled = false;
Expand Down Expand Up @@ -546,6 +553,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) {
Expand Down Expand Up @@ -581,6 +596,7 @@ export function AppShell() {
<ChannelNavigationProvider channels={channels}>
<AppShellProvider
value={{
markAllChannelsRead,
markChannelRead,
markChannelUnread,
openChannelManagement: () => {
Expand Down Expand Up @@ -693,6 +709,8 @@ export function AppShell() {
void applyAgents(templateId, createdForum.id);
}}
onHideDm={handleHideDm}
onMarkAllChannelsRead={markAllChannelsRead}
onMarkChannelRead={markChannelRead}
onMarkChannelUnread={markChannelUnread}
onOpenBrowseChannels={handleOpenBrowseChannels}
onOpenBrowseForums={handleOpenBrowseForums}
Expand Down
2 changes: 2 additions & 0 deletions desktop/src/app/AppShellContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from "react";

type AppShellContextValue = {
markAllChannelsRead: () => void;
markChannelRead: (
channelId: string,
readAt: string | null | undefined,
Expand All @@ -20,6 +21,7 @@ type AppShellContextValue = {
};

const AppShellContext = React.createContext<AppShellContextValue>({
markAllChannelsRead: () => {},
markChannelRead: () => {},
markChannelUnread: () => {},
openChannelManagement: () => {},
Expand Down
50 changes: 50 additions & 0 deletions desktop/src/app/useMarkAsReadShortcuts.ts
Original file line number Diff line number Diff line change
@@ -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,
]);
}
1 change: 1 addition & 0 deletions desktop/src/features/channels/useLiveChannelUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
) {
Expand Down
18 changes: 18 additions & 0 deletions desktop/src/features/channels/useUnreadChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion desktop/src/features/notifications/lib/desktop.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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") {
Expand Down
Loading