Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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", 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
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
31 changes: 25 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,10 @@ export function AppShell() {
const refetchHomeFeedOnLiveMention = React.useEffectEvent(() => {
void homeFeedQuery.refetch();
});
const handleChannelNotification = React.useEffectEvent(() => {
if (!notificationSettings.settings.desktopEnabled) return;
void requestDockBounce();
});
Comment thread
wpfleger96 marked this conversation as resolved.

const handleDmNotification = React.useEffectEvent(
(event: RelayEvent, channel: Channel) => {
Expand Down Expand Up @@ -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();
});
},
);
Expand All @@ -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.
Expand All @@ -280,6 +287,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 +447,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 +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) {
Expand Down Expand Up @@ -581,6 +597,7 @@ export function AppShell() {
<ChannelNavigationProvider channels={channels}>
<AppShellProvider
value={{
markAllChannelsRead,
markChannelRead,
markChannelUnread,
openChannelManagement: () => {
Expand Down Expand Up @@ -693,6 +710,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