Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
016d246
refactor(desktop): align channel management panel with profile
thomaspblock Jun 16, 2026
f4efcb6
Merge remote-tracking branch 'origin/main' into channel-management-clean
thomaspblock Jun 16, 2026
767614f
chore(desktop): normalize channel panel copy punctuation
thomaspblock Jun 16, 2026
3a1bebf
test(desktop): align channel management e2e with edit modal
thomaspblock Jun 16, 2026
6a2f578
Merge remote-tracking branch 'origin/main' into channel-management-clean
thomaspblock Jun 16, 2026
3473efb
Keep channel management in home inbox
thomaspblock Jun 16, 2026
7dc6b7d
Align channel management side panel behavior
thomaspblock Jun 16, 2026
b5d8a8f
Polish channel management hero spacing
thomaspblock Jun 17, 2026
3c13398
Restore channel panel Escape close
thomaspblock Jun 17, 2026
2d7e879
Preserve docked channel panel close behavior
thomaspblock Jun 17, 2026
0d33fc8
Merge main into channel management panel branch
thomaspblock Jun 17, 2026
4f1280e
Merge origin/main into channel management panel branch
thomaspblock Jun 19, 2026
239b6de
Merge origin/main into channel management panel branch
thomaspblock Jun 22, 2026
775af7c
Fix docked channel management interactions
thomaspblock Jun 22, 2026
229aaeb
Merge origin/main into channel management panel branch
thomaspblock Jun 22, 2026
8f7c320
Fix default channel TTL after main merge
thomaspblock Jun 22, 2026
d6bc5f3
Merge origin/main into channel management panel branch.
thomaspblock Jun 23, 2026
f90dc49
Merge origin/main into channel management panel branch.
thomaspblock Jun 23, 2026
e3e49d3
Merge origin/main into channel management panel branch.
thomaspblock Jun 24, 2026
7f96225
chore(merge): merge origin/main into channel-management-clean
Jun 24, 2026
9571882
fix(channels): align split panel header with shared AuxiliaryPanelHeader
Jun 24, 2026
70e21a1
fix(channels): make channel management a history-backed route pane
Jun 25, 2026
d0658b0
fix(channels): toggle channel management pane on repeat button click
Jun 25, 2026
781403b
chore(merge): merge origin/main into channel-management-clean
Jun 25, 2026
c1ae92e
refactor(desktop): inline window-drag handling back into AppShell
Jun 25, 2026
8808d1f
revert(desktop): keep useTauriWindowDrag hook
Jun 25, 2026
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
70 changes: 29 additions & 41 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import * as React from "react";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { useQueryClient } from "@tanstack/react-query";
import { Outlet, useLocation } from "@tanstack/react-router";

import {
deriveShellRoute,
isWindowDragHandleEvent,
} from "@/app/AppShell.helpers";
import { deriveShellRoute } from "@/app/AppShell.helpers";
import { AppShellProvider } from "@/app/AppShellContext";
import {
AppShellOverlays,
Expand All @@ -20,6 +16,7 @@ import { useMarkAsReadShortcuts } from "@/app/useMarkAsReadShortcuts";
import { useSettingsShortcuts } from "@/app/useSettingsShortcuts";
import { useAppShellDesktopNotifications } from "@/app/useAppShellDesktopNotifications";
import { useThreadActivityFeedItems } from "@/app/useThreadActivityFeedItems";
import { useTauriWindowDrag } from "@/app/useTauriWindowDrag";
import { useWebviewZoomShortcuts } from "@/app/useWebviewZoomShortcuts";
import {
channelsQueryKey,
Expand Down Expand Up @@ -88,10 +85,15 @@ const LazySettingsScreen = React.lazy(async () => {

export function AppShell() {
useWebviewZoomShortcuts();
useTauriWindowDrag();

const workspacesHook = useWorkspaces();
const [isAddWorkspaceOpen, setIsAddWorkspaceOpen] = React.useState(false);
const [isChannelManagementOpen, setIsChannelManagementOpen] =
React.useState(false);
const [managedChannelId, setManagedChannelId] = React.useState<string | null>(
null,
);
const [searchFocusRequest, setSearchFocusRequest] = React.useState(0);
const [browseDialogType, setBrowseDialogType] =
React.useState<BrowseDialogType>(null);
Expand All @@ -118,9 +120,7 @@ export function AppShell() {
() => deriveShellRoute(location.pathname),
[location.pathname],
);
// Settings lives in the history stack: /settings?section=… opens it, back
// (or "Back to app") returns to the previous entry — panels and all — and
// reloads restore the open section from the URL.
// Settings lives in history so back returns to the previous app entry.
const settingsOpen = location.pathname === "/settings";
const locationSearchSection = (location.search as { section?: unknown })
.section;
Expand Down Expand Up @@ -187,6 +187,12 @@ export function AppShell() {
: null,
[channels, selectedChannelId],
);
const managedChannel = React.useMemo(() => {
const targetChannelId = managedChannelId ?? selectedChannelId;
return targetChannelId
? (channels.find((channel) => channel.id === targetChannelId) ?? null)
: null;
}, [channels, managedChannelId, selectedChannelId]);

const {
handleChannelNotification,
Expand Down Expand Up @@ -289,6 +295,7 @@ export function AppShell() {
channels,
);

// Badge count consumes the shared NIP-RS read-state from useUnreadChannels.
const { homeBadgeCount, homeBadgeCountExcludingHighPriority } =
useHomeFeedNotificationState(
homeFeedQuery.data,
Expand Down Expand Up @@ -554,36 +561,6 @@ export function AppShell() {
selectedView,
});

React.useEffect(() => {
function handlePointerDown(event: PointerEvent) {
if (event.button !== 0 || event.detail > 1) {
return;
}

if (!isWindowDragHandleEvent(event)) {
return;
}

void getCurrentWindow().startDragging();
}

function handleDoubleClick(event: MouseEvent) {
if (event.button !== 0 || !isWindowDragHandleEvent(event)) {
return;
}

event.preventDefault();
void getCurrentWindow().toggleMaximize();
}

window.addEventListener("pointerdown", handlePointerDown, true);
window.addEventListener("dblclick", handleDoubleClick, true);
return () => {
window.removeEventListener("pointerdown", handlePointerDown, true);
window.removeEventListener("dblclick", handleDoubleClick, true);
};
}, []);

return (
<PreventSleepProvider>
<ChannelNavigationProvider channels={channels}>
Expand All @@ -593,7 +570,12 @@ export function AppShell() {
markChannelRead,
markChannelUnread,
openCreateChannel: handleOpenCreateChannel,
openChannelManagement: () => setIsChannelManagementOpen(true),
openChannelManagement: (channelId?: string) => {
setManagedChannelId(
typeof channelId === "string" ? channelId : null,
);
setIsChannelManagementOpen(true);
},
getChannelReadAt,
getThreadReadAt,
markThreadRead,
Expand Down Expand Up @@ -827,16 +809,22 @@ export function AppShell() {
</div>
)}
<AppShellOverlays
activeChannel={activeChannel}
activeChannel={managedChannel}
browseDialogType={browseDialogType}
channels={channels}
currentPubkey={identityQuery.data?.pubkey}
isChannelManagementOpen={isChannelManagementOpen}
onBrowseChannelJoin={handleBrowseChannelJoin}
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
onChannelManagementOpenChange={setIsChannelManagementOpen}
onChannelManagementOpenChange={(open) => {
setIsChannelManagementOpen(open);
if (!open) {
setManagedChannelId(null);
}
}}
onDeleteActiveChannel={() => {
setIsChannelManagementOpen(false);
setManagedChannelId(null);
void goHome({ replace: true });
}}
onSelectChannel={(channelId) => {
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/app/AppShellContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type AppShellContextValue = {
) => void;
markChannelUnread: (channelId: string) => void;
openCreateChannel: () => void;
openChannelManagement: () => void;
openChannelManagement: (channelId?: string) => void;
// NIP-RS read marker for a channel as a unix-seconds timestamp, or null
// when unknown. Backed by the single AppShell-mounted ReadStateManager so
// every surface (sidebar, home, badges) projects from the same source.
Expand Down
36 changes: 36 additions & 0 deletions desktop/src/app/useTauriWindowDrag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react";
import { getCurrentWindow } from "@tauri-apps/api/window";

import { isWindowDragHandleEvent } from "@/app/AppShell.helpers";

export function useTauriWindowDrag() {
React.useEffect(() => {
function handlePointerDown(event: PointerEvent) {
if (
event.button !== 0 ||
event.detail > 1 ||
!isWindowDragHandleEvent(event)
) {
return;
}

void getCurrentWindow().startDragging();
}

function handleDoubleClick(event: MouseEvent) {
if (event.button !== 0 || !isWindowDragHandleEvent(event)) {
return;
}

event.preventDefault();
void getCurrentWindow().toggleMaximize();
}

window.addEventListener("pointerdown", handlePointerDown, true);
window.addEventListener("dblclick", handleDoubleClick, true);
return () => {
window.removeEventListener("pointerdown", handlePointerDown, true);
window.removeEventListener("dblclick", handleDoubleClick, true);
};
}, []);
}
6 changes: 3 additions & 3 deletions desktop/src/features/channels/ui/ChannelCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function ChannelCanvas({
}

if (canvasQuery.isLoading) {
return <p className="text-sm text-muted-foreground">Loading canvas</p>;
return <p className="text-sm text-muted-foreground">Loading canvas...</p>;
}

if (canvasQuery.error instanceof Error) {
Expand All @@ -78,7 +78,7 @@ export function ChannelCanvas({
data-testid="channel-canvas-editor"
disabled={setCanvasMutation.isPending}
onChange={(event) => setDraft(event.target.value)}
placeholder="Write your canvas content in Markdown"
placeholder="Write your canvas content in Markdown..."
value={draft}
/>
<div className="flex gap-2">
Expand All @@ -94,7 +94,7 @@ export function ChannelCanvas({
type="button"
>
<Save className="h-4 w-4" />
{setCanvasMutation.isPending ? "Saving" : "Save canvas"}
{setCanvasMutation.isPending ? "Saving..." : "Save canvas"}
</Button>
<Button
data-testid="channel-canvas-cancel"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type * as React from "react";

import type { Channel } from "@/shared/api/types";
import { ChannelManagementSheet } from "@/features/channels/ui/ChannelManagementSheet";
import { RightAuxiliaryPane } from "@/features/channels/ui/RightAuxiliaryPane";

type ChannelManagementAuxiliaryPanelProps = {
activeChannel: Channel;
canResetThreadPanelWidth: boolean;
currentPubkey?: string;
isSinglePanelView: boolean;
onChannelManagementDeleted?: () => void;
onCloseChannelManagement?: () => void;
onResetThreadPanelWidth: () => void;
onThreadPanelResizeStart: (
event: React.PointerEvent<HTMLButtonElement>,
) => void;
threadPanelWidthPx: number;
useSplitAuxiliaryPane: boolean;
};

export function ChannelManagementAuxiliaryPanel({
activeChannel,
canResetThreadPanelWidth,
currentPubkey,
isSinglePanelView,
onChannelManagementDeleted,
onCloseChannelManagement,
onResetThreadPanelWidth,
onThreadPanelResizeStart,
threadPanelWidthPx,
useSplitAuxiliaryPane,
}: ChannelManagementAuxiliaryPanelProps) {
const panel = (
<ChannelManagementSheet
channel={activeChannel}
currentPubkey={currentPubkey}
layout={useSplitAuxiliaryPane || isSinglePanelView ? "split" : "overlay"}
onDeleted={onChannelManagementDeleted}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
onCloseChannelManagement?.();
}
}}
open={true}
/>
);

if (!useSplitAuxiliaryPane) {
return panel;
}

return (
<RightAuxiliaryPane
canResetWidth={canResetThreadPanelWidth}
onResetWidth={onResetThreadPanelWidth}
onResizeStart={onThreadPanelResizeStart}
testId="channel-management-auxiliary-pane"
widthPx={threadPanelWidthPx}
>
{panel}
</RightAuxiliaryPane>
);
}
Loading
Loading