mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
97
desktop/src/features/channels/ui/useChannelPaneOpeners.ts
Normal file
97
desktop/src/features/channels/ui/useChannelPaneOpeners.ts
Normal 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 };
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user