fix(desktop): address markdown-viewer review findings

- Extract pane-open handlers (manage channel + markdown doc) into
  useChannelPaneOpeners, bringing ChannelScreen back under the
  1000-line file-size ratchet (1016 -> 984).
- Include the markdown doc panel in hasAuxiliaryPanel so narrow
  layouts enter single-panel view (and Escape-to-close engages)
  the same way sibling panes do, instead of squeezing the timeline.
- Key MarkdownDocPanel by URL so opening a different document resets
  the Preview/Code toggle.
- Pre-gate the open-in-viewer affordance on the advertised imeta size
  so a >2MiB .md is not fetched twice just to be refused (decoder
  still re-checks — imeta size is untrusted).
- Document the 150-line syntax-highlight cap inherited from
  SyntaxHighlightedCode in the Code view.

Signed-off-by: Riley Crane <rileycrane@squareup.com>
Co-authored-by: Fizz <d4ec90109b43092b0ec94cf136850915a3732a29ab3089a1dec43d2ed43dd6eb@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Riley Crane
2026-08-21 17:03:28 -07:00
parent 332c54ea89
commit 520dff6bfc
5 changed files with 121 additions and 49 deletions

View File

@@ -1015,7 +1015,10 @@ export const ChannelPane = React.memo(function ChannelPane({
// reappears when it closes. Opening a document clears competitors
// in the screen-level handler, so it is never dead on arrival.
const panel = (
// Keyed by URL so opening a different document resets the
// Preview/Code toggle instead of inheriting the previous doc's.
<MarkdownDocPanel
key={openMarkdownDoc.url}
filename={openMarkdownDoc.filename}
isSinglePanelView={
useSplitAuxiliaryPane ? false : isSinglePanelView

View File

@@ -54,10 +54,8 @@ import { useHuddleReadMarker } from "@/features/channels/ui/useHuddleReadMarker"
import { useHuddleThreadIsolation } from "@/features/channels/ui/useHuddleThreadIsolation";
import { AgentSessionProvider } from "@/shared/context/AgentSessionContext";
import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext";
import {
type MarkdownDocTarget,
MarkdownDocViewerProvider,
} from "@/shared/ui/markdown/markdownDocViewerContext";
import { useChannelPaneOpeners } from "@/features/channels/ui/useChannelPaneOpeners";
import { MarkdownDocViewerProvider } from "@/shared/ui/markdown/markdownDocViewerContext";
import { useMainInsetRef } from "@/shared/layout/MainInsetContext";
import { channelContentTopPaddingMeasurement } from "@/shared/layout/chromeLayout";
import { useMeasuredCssVariable } from "@/shared/layout/useMeasuredCssVariable";
@@ -690,7 +688,8 @@ export function ChannelScreen({
effectiveOpenThreadHeadId ||
openAgentSessionPubkey ||
profilePanelPubkey ||
channelManagementOpen,
channelManagementOpen ||
(markdownDocUrl && markdownDocName),
);
const displayedThreadHeadMessage = threadPanelData.threadHead;
const displayedThreadAllMessages = threadPanelData.messages;
@@ -719,59 +718,24 @@ export function ChannelScreen({
resetKey: activeChannelId,
enabled: !isSinglePanelView,
});
const handleManageChannel = React.useCallback(() => {
if (!requireThreadEditResolution()) return;
if (activeChannel?.channelType === "forum") {
openGlobalChannelManagement();
return;
}
if (channelManagementOpen) {
setChannelManagementOpen(false);
return;
}
setOpenThreadHeadId(null);
setExpandedThreadReplyIds(new Set());
setThreadScrollTargetId(null);
setThreadReplyTargetId(null);
handleCloseAgentSession();
setProfilePanelPubkey(null);
setChannelManagementOpen(true);
}, [
activeChannel?.channelType,
const { handleManageChannel, handleOpenMarkdownDoc } = useChannelPaneOpeners({
channelType: activeChannel?.channelType,
channelManagementOpen,
closeAgentSession: handleCloseAgentSession,
openGlobalChannelManagement,
openMarkdownDoc,
requireThreadEditResolution,
setChannelManagementOpen,
setExpandedThreadReplyIds,
setOpenThreadHeadId,
handleCloseAgentSession,
setProfilePanelPubkey,
]);
setThreadReplyTargetId,
setThreadScrollTargetId,
});
const handleToggleMembers = React.useCallback(
() => setIsMembersSidebarOpen((prev) => !prev),
[],
);
// Opening a document clears competing panes (mirrors handleManageChannel /
// useChannelProfilePanel) so the doc panel — lowest in ChannelPane's pane
// priority chain — is never opened dead behind another pane.
const handleOpenMarkdownDoc = React.useCallback(
(doc: MarkdownDocTarget) => {
setOpenThreadHeadId(null);
setExpandedThreadReplyIds(new Set());
setThreadScrollTargetId(null);
setThreadReplyTargetId(null);
handleCloseAgentSession();
setChannelManagementOpen(false);
setProfilePanelPubkey(null);
openMarkdownDoc(doc.url, doc.filename);
},
[
handleCloseAgentSession,
openMarkdownDoc,
setChannelManagementOpen,
setOpenThreadHeadId,
setProfilePanelPubkey,
],
);
const channelHeader = React.useMemo(
() => (
<ChannelScreenHeader

View File

@@ -165,6 +165,8 @@ export function MarkdownDocPanel({
className="overflow-x-auto pt-3 text-xs leading-relaxed"
data-testid="markdown-doc-code"
>
{/* Shiki's synchronous-tokenization guard caps highlighting at
150 lines; longer documents render as plain text here. */}
<SyntaxHighlightedCode code={decoded.text} language="markdown" />
</pre>
)

View File

@@ -0,0 +1,97 @@
import * as React from "react";
import type { MarkdownDocTarget } from "@/shared/ui/markdown/markdownDocViewerContext";
type UseChannelPaneOpenersOptions = {
channelType: string | undefined;
channelManagementOpen: boolean;
closeAgentSession: () => void;
openGlobalChannelManagement: () => void;
openMarkdownDoc: (url: string, filename: string) => void;
/** Prompts for an unresolved in-progress thread edit; false blocks the open. */
requireThreadEditResolution: () => boolean;
setChannelManagementOpen: (open: boolean) => void;
setExpandedThreadReplyIds: React.Dispatch<React.SetStateAction<Set<string>>>;
setOpenThreadHeadId: (id: string | null) => void;
setProfilePanelPubkey: (pubkey: string | null) => void;
setThreadReplyTargetId: (id: string | null) => void;
setThreadScrollTargetId: (id: string | null) => void;
};
/**
* Open handlers for ChannelPane's mutually exclusive auxiliary panes.
*
* Each opener clears every competing pane before opening its own (mirroring
* useChannelProfilePanel) so a newly opened pane is never dead behind a
* higher-priority sibling in ChannelPane's pane priority chain.
*/
export function useChannelPaneOpeners({
channelType,
channelManagementOpen,
closeAgentSession,
openGlobalChannelManagement,
openMarkdownDoc,
requireThreadEditResolution,
setChannelManagementOpen,
setExpandedThreadReplyIds,
setOpenThreadHeadId,
setProfilePanelPubkey,
setThreadReplyTargetId,
setThreadScrollTargetId,
}: UseChannelPaneOpenersOptions) {
const clearCompetingPanes = React.useCallback(() => {
setOpenThreadHeadId(null);
setExpandedThreadReplyIds(new Set());
setThreadScrollTargetId(null);
setThreadReplyTargetId(null);
closeAgentSession();
setProfilePanelPubkey(null);
}, [
closeAgentSession,
setExpandedThreadReplyIds,
setOpenThreadHeadId,
setProfilePanelPubkey,
setThreadReplyTargetId,
setThreadScrollTargetId,
]);
const handleManageChannel = React.useCallback(() => {
if (!requireThreadEditResolution()) return;
if (channelType === "forum") {
openGlobalChannelManagement();
return;
}
if (channelManagementOpen) {
setChannelManagementOpen(false);
return;
}
clearCompetingPanes();
setChannelManagementOpen(true);
}, [
channelType,
channelManagementOpen,
clearCompetingPanes,
openGlobalChannelManagement,
requireThreadEditResolution,
setChannelManagementOpen,
]);
const handleOpenMarkdownDoc = React.useCallback(
(doc: MarkdownDocTarget) => {
// Opening a doc closes the thread pane, so an in-progress thread edit
// must be resolved first — same contract as the sibling pane openers.
if (!requireThreadEditResolution()) return;
clearCompetingPanes();
setChannelManagementOpen(false);
openMarkdownDoc(doc.url, doc.filename);
},
[
clearCompetingPanes,
openMarkdownDoc,
requireThreadEditResolution,
setChannelManagementOpen,
],
);
return { handleManageChannel, handleOpenMarkdownDoc };
}

View File

@@ -6,7 +6,10 @@ import { invokeTauri } from "@/shared/api/tauri";
import { useSmoothCorners } from "@/shared/ui/smoothCorners";
import { isRelayDownloadable } from "./mediaEntry";
import { isMarkdownDocFilename } from "./markdownDocFile";
import {
isMarkdownDocFilename,
MAX_MARKDOWN_DOC_BYTES,
} from "./markdownDocFile";
import { useMarkdownDocViewer } from "./markdownDocViewerContext";
import { useMarkdownRuntime } from "./runtimeContext";
@@ -56,6 +59,9 @@ export function FileCard({
const opensInViewer =
openMarkdownDoc !== null &&
isMarkdownDocFilename(filename) &&
// Advertised-size pre-gate: skip fetching a doc the viewer would refuse
// anyway. The imeta size is untrusted, so the decoder re-checks post-fetch.
(size == null || size <= MAX_MARKDOWN_DOC_BYTES) &&
isRelayDownloadable(href, relayOrigin ?? undefined);
return (