mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
refactor(desktop): keep channel panes within size ratchet
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import { LogIn } from "lucide-react";
|
||||
import { AnimatePresence } from "motion/react";
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
@@ -25,7 +24,6 @@ import {
|
||||
hasOtherDmParticipant,
|
||||
} from "@/features/channels/lib/dmHuddleMembers";
|
||||
import { buildVideoReviewPresentationByMessageId } from "@/features/messages/lib/videoReviewContext";
|
||||
import { isThreadReply } from "@/features/messages/lib/threading";
|
||||
import { useComposerHeightPadding } from "@/features/messages/ui/useComposerHeightPadding";
|
||||
import { UserProfilePanel } from "@/features/profile/ui/UserProfilePanel";
|
||||
import { AgentSessionThreadPanel } from "@/features/channels/ui/AgentSessionThreadPanel";
|
||||
@@ -59,11 +57,10 @@ import type { ChannelPaneProps } from "@/features/channels/ui/ChannelPane.types"
|
||||
import * as agentSessionSelection from "@/features/channels/ui/agentSessionSelection";
|
||||
import { usePrepareDmSendChannel } from "@/features/channels/ui/usePrepareDmSendChannel";
|
||||
import { useChannelPaneMessages } from "@/features/channels/ui/useChannelPaneMessages";
|
||||
import { useRoutedMessageEdit } from "@/features/channels/ui/useRoutedMessageEdit";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { useRenderScopedReactionHydration } from "@/features/messages/lib/useRenderScopedReactionHydration";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import { isWelcomeExperienceChannel as isWelcomeExperience } from "@/features/onboarding/welcome";
|
||||
import { KIND_SYSTEM_MESSAGE } from "@/shared/constants/kinds";
|
||||
import { useIsThreadPanelOverlay } from "@/shared/hooks/use-mobile";
|
||||
import { channelChrome } from "@/shared/layout/chromeLayout";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
@@ -239,26 +236,6 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
const isEditInThread = editTarget?.isThreadReply === true;
|
||||
const mainEditTarget = editTarget && !isEditInThread ? editTarget : null;
|
||||
const threadEditTarget = editTarget && isEditInThread ? editTarget : null;
|
||||
const findLastOwnEditable = React.useCallback(
|
||||
(candidates: TimelineMessage[]): TimelineMessage | null => {
|
||||
if (!onEdit || !currentPubkey) return null;
|
||||
let best: TimelineMessage | null = null;
|
||||
for (const message of candidates) {
|
||||
if (
|
||||
message.kind === KIND_SYSTEM_MESSAGE ||
|
||||
message.pubkey !== currentPubkey ||
|
||||
message.pending
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!best || message.createdAt >= best.createdAt) {
|
||||
best = message;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
},
|
||||
[onEdit, currentPubkey],
|
||||
);
|
||||
|
||||
const timeoutState = useTimeoutState();
|
||||
// A moderation DM (1:1 with the relay identity) is read-only for the member;
|
||||
@@ -486,81 +463,23 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
onExternalTargetResolved: onThreadScrollTargetResolved,
|
||||
onModeChange: markExitComplete,
|
||||
});
|
||||
const pendingMainEditRef = React.useRef<TimelineMessage | null>(null);
|
||||
const editTargetRef = React.useRef(editTarget);
|
||||
editTargetRef.current = editTarget;
|
||||
const pendingMainEditContextRef = React.useRef({
|
||||
channelId: activeChannel?.id ?? null,
|
||||
threadId: threadHeadMessage?.id ?? null,
|
||||
});
|
||||
const pendingMainEditContext = {
|
||||
channelId: activeChannel?.id ?? null,
|
||||
threadId: threadHeadMessage?.id ?? null,
|
||||
};
|
||||
const previousPendingContext = pendingMainEditContextRef.current;
|
||||
if (
|
||||
previousPendingContext.channelId !== pendingMainEditContext.channelId ||
|
||||
(previousPendingContext.threadId !== null &&
|
||||
pendingMainEditContext.threadId !== null &&
|
||||
previousPendingContext.threadId !== pendingMainEditContext.threadId)
|
||||
) {
|
||||
pendingMainEditRef.current = null;
|
||||
}
|
||||
pendingMainEditContextRef.current = pendingMainEditContext;
|
||||
const handleRoutedEdit = React.useCallback(
|
||||
(message: TimelineMessage): boolean => {
|
||||
const currentEditTarget = editTargetRef.current;
|
||||
if (
|
||||
currentEditTarget &&
|
||||
currentEditTarget.id !== message.id &&
|
||||
currentEditTarget.isThreadReply !== isThreadReply(message.tags ?? [])
|
||||
) {
|
||||
pendingMainEditRef.current = null;
|
||||
toast.info("Finish or cancel your edit first.");
|
||||
return false;
|
||||
}
|
||||
if (currentEditTarget?.id === message.id) {
|
||||
pendingMainEditRef.current = null;
|
||||
onEdit?.(message);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!isThreadReply(message.tags ?? []) &&
|
||||
(isSinglePanelView || useFocusThreadDrawer)
|
||||
) {
|
||||
pendingMainEditRef.current = message;
|
||||
onCloseThread();
|
||||
return true;
|
||||
}
|
||||
onEdit?.(message);
|
||||
return Boolean(onEdit);
|
||||
},
|
||||
[isSinglePanelView, onCloseThread, onEdit, useFocusThreadDrawer],
|
||||
);
|
||||
const handleEditLastOwnMainMessage = React.useCallback((): boolean => {
|
||||
const target = findLastOwnEditable(
|
||||
mainTimelineEntries.map((entry) => entry.message),
|
||||
);
|
||||
return target ? handleRoutedEdit(target) : false;
|
||||
}, [findLastOwnEditable, handleRoutedEdit, mainTimelineEntries]);
|
||||
const handleEditLastOwnThreadMessage = React.useCallback((): boolean => {
|
||||
const scope: TimelineMessage[] = [];
|
||||
if (threadHeadMessage) scope.push(threadHeadMessage);
|
||||
for (const entry of threadMessages) scope.push(entry.message);
|
||||
const target = findLastOwnEditable(scope);
|
||||
return target ? handleRoutedEdit(target) : false;
|
||||
}, [
|
||||
findLastOwnEditable,
|
||||
handleRoutedEdit,
|
||||
const {
|
||||
handleEditLastOwnMainMessage,
|
||||
handleEditLastOwnThreadMessage,
|
||||
routeEdit: handleRoutedEdit,
|
||||
} = useRoutedMessageEdit({
|
||||
activeChannelId,
|
||||
channelIsCovered,
|
||||
currentPubkey,
|
||||
editTarget,
|
||||
isSinglePanelView,
|
||||
mainMessages: mainTimelineEntries.map((entry) => entry.message),
|
||||
onCloseThread,
|
||||
onEdit,
|
||||
threadHeadMessage,
|
||||
threadMessages,
|
||||
]);
|
||||
React.useEffect(() => {
|
||||
const pendingMainEdit = pendingMainEditRef.current;
|
||||
if (!pendingMainEdit || isSinglePanelView || channelIsCovered) return;
|
||||
pendingMainEditRef.current = null;
|
||||
onEdit?.(pendingMainEdit);
|
||||
}, [channelIsCovered, isSinglePanelView, onEdit]);
|
||||
threadMessages: threadMessages.map((entry) => entry.message),
|
||||
useFocusThreadDrawer,
|
||||
});
|
||||
const hasSplitAuxiliaryPane =
|
||||
useSplitAuxiliaryPane &&
|
||||
(channelManagementOpen ||
|
||||
|
||||
@@ -569,14 +569,8 @@ export function ChannelScreen({
|
||||
welcomeAgentCreate.openAddAgent(() => setIsAddBotOpen(true), options),
|
||||
[welcomeAgentCreate],
|
||||
);
|
||||
const handleOpenMembersSidebar = React.useCallback(
|
||||
() => setIsMembersSidebarOpen(true),
|
||||
[],
|
||||
);
|
||||
const handleCloseChannelManagement = React.useCallback(
|
||||
() => setChannelManagementOpen(false),
|
||||
[setChannelManagementOpen],
|
||||
);
|
||||
const handleOpenMembersSidebar = () => setIsMembersSidebarOpen(true);
|
||||
const handleCloseChannelManagement = () => setChannelManagementOpen(false);
|
||||
const handleChannelManagementDeleted = React.useCallback(() => {
|
||||
setChannelManagementOpen(false);
|
||||
void goHome({ replace: true });
|
||||
@@ -637,9 +631,6 @@ export function ChannelScreen({
|
||||
isPlaceholderData: messagesQuery.isPlaceholderData,
|
||||
dataLength: messagesQuery.data?.length ?? null,
|
||||
},
|
||||
// A persisted head only counts as hydrated when it has rows to paint
|
||||
// (channelHeadCache.ts), so this bypass never settles onto an empty
|
||||
// placeholder while the authoritative refresh is still in flight.
|
||||
hasSettledThisChannel ||
|
||||
(activeChannelId !== null &&
|
||||
hasPersistedHydratedChannel(queryClient, activeChannelId)),
|
||||
@@ -769,8 +760,7 @@ export function ChannelScreen({
|
||||
activeDmHeaderParticipants={activeDmHeaderParticipants}
|
||||
activeDmPresenceStatus={activeDmPresenceStatus}
|
||||
chromeWrapperRef={channelHeaderChromeRef}
|
||||
currentPubkey={currentPubkey}
|
||||
headerEndActions={headerEndActions}
|
||||
{...{ currentPubkey, headerEndActions }}
|
||||
isAddBotOpen={isAddBotOpen}
|
||||
isJoining={joinChannelMutation.isPending}
|
||||
onAddBotOpenChange={setIsAddBotOpen}
|
||||
@@ -875,12 +865,11 @@ export function ChannelScreen({
|
||||
canResetThreadPanelWidth={canResetThreadPanelWidth}
|
||||
fetchOlder={fetchOlder}
|
||||
header={channelHeader}
|
||||
idleAuxiliaryPanel={idleAuxiliaryPanel}
|
||||
idleAuxiliaryTitle={idleAuxiliaryTitle}
|
||||
{...{ idleAuxiliaryPanel, idleAuxiliaryTitle }}
|
||||
hasOlderMessages={hasOlderMessages}
|
||||
historyExhausted={historyExhausted}
|
||||
{...{ onAddFiles }}
|
||||
onAddAgent={handleOpenAddBot}
|
||||
onAddFiles={onAddFiles}
|
||||
onBrowseChannels={openBrowseChannels}
|
||||
onCreateChannel={openCreateChannel}
|
||||
onOpenMembers={handleOpenMembersSidebar}
|
||||
@@ -934,8 +923,8 @@ export function ChannelScreen({
|
||||
? handleBackFromAgentSession
|
||||
: undefined
|
||||
}
|
||||
{...{ onCloseIdleAuxiliaryPanel }}
|
||||
onCloseChannelManagement={handleCloseChannelManagement}
|
||||
onCloseIdleAuxiliaryPanel={onCloseIdleAuxiliaryPanel}
|
||||
onCloseThread={handleCloseThread}
|
||||
onDelete={
|
||||
activeChannel?.archivedAt ? undefined : handleDelete
|
||||
|
||||
123
desktop/src/features/channels/ui/useRoutedMessageEdit.ts
Normal file
123
desktop/src/features/channels/ui/useRoutedMessageEdit.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import { isThreadReply } from "@/features/messages/lib/threading";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import { KIND_SYSTEM_MESSAGE } from "@/shared/constants/kinds";
|
||||
|
||||
type Input = {
|
||||
activeChannelId: string | null;
|
||||
channelIsCovered: boolean;
|
||||
currentPubkey?: string | null;
|
||||
editTarget: { id: string; isThreadReply: boolean } | null;
|
||||
isSinglePanelView: boolean;
|
||||
mainMessages: TimelineMessage[];
|
||||
onCloseThread: () => void;
|
||||
onEdit?: (message: TimelineMessage) => void;
|
||||
threadHeadMessage: TimelineMessage | null;
|
||||
threadMessages: TimelineMessage[];
|
||||
useFocusThreadDrawer: boolean;
|
||||
};
|
||||
|
||||
export function useRoutedMessageEdit({
|
||||
activeChannelId,
|
||||
channelIsCovered,
|
||||
currentPubkey,
|
||||
editTarget,
|
||||
isSinglePanelView,
|
||||
mainMessages,
|
||||
onCloseThread,
|
||||
onEdit,
|
||||
threadHeadMessage,
|
||||
threadMessages,
|
||||
useFocusThreadDrawer,
|
||||
}: Input) {
|
||||
const pendingMainEditRef = React.useRef<TimelineMessage | null>(null);
|
||||
const editTargetRef = React.useRef(editTarget);
|
||||
editTargetRef.current = editTarget;
|
||||
const contextRef = React.useRef({
|
||||
channelId: activeChannelId,
|
||||
threadId: threadHeadMessage?.id,
|
||||
});
|
||||
const context = {
|
||||
channelId: activeChannelId,
|
||||
threadId: threadHeadMessage?.id,
|
||||
};
|
||||
if (
|
||||
contextRef.current.channelId !== context.channelId ||
|
||||
(contextRef.current.threadId &&
|
||||
context.threadId &&
|
||||
contextRef.current.threadId !== context.threadId)
|
||||
)
|
||||
pendingMainEditRef.current = null;
|
||||
contextRef.current = context;
|
||||
|
||||
const findLastOwnEditable = React.useCallback(
|
||||
(messages: TimelineMessage[]) => {
|
||||
if (!onEdit || !currentPubkey) return null;
|
||||
return messages.reduce<TimelineMessage | null>(
|
||||
(best, message) =>
|
||||
message.kind === KIND_SYSTEM_MESSAGE ||
|
||||
message.pubkey !== currentPubkey ||
|
||||
message.pending ||
|
||||
(best && message.createdAt < best.createdAt)
|
||||
? best
|
||||
: message,
|
||||
null,
|
||||
);
|
||||
},
|
||||
[currentPubkey, onEdit],
|
||||
);
|
||||
const routeEdit = React.useCallback(
|
||||
(message: TimelineMessage) => {
|
||||
const current = editTargetRef.current;
|
||||
if (
|
||||
current &&
|
||||
current.id !== message.id &&
|
||||
current.isThreadReply !== isThreadReply(message.tags ?? [])
|
||||
) {
|
||||
pendingMainEditRef.current = null;
|
||||
toast.info("Finish or cancel your edit first.");
|
||||
return false;
|
||||
}
|
||||
if (current?.id === message.id) {
|
||||
pendingMainEditRef.current = null;
|
||||
onEdit?.(message);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!isThreadReply(message.tags ?? []) &&
|
||||
(isSinglePanelView || useFocusThreadDrawer)
|
||||
) {
|
||||
pendingMainEditRef.current = message;
|
||||
onCloseThread();
|
||||
return true;
|
||||
}
|
||||
onEdit?.(message);
|
||||
return Boolean(onEdit);
|
||||
},
|
||||
[isSinglePanelView, onCloseThread, onEdit, useFocusThreadDrawer],
|
||||
);
|
||||
const handleEditLastOwnMainMessage = React.useCallback(() => {
|
||||
const target = findLastOwnEditable(mainMessages);
|
||||
return target ? routeEdit(target) : false;
|
||||
}, [findLastOwnEditable, mainMessages, routeEdit]);
|
||||
const handleEditLastOwnThreadMessage = React.useCallback(() => {
|
||||
const target = findLastOwnEditable(
|
||||
threadHeadMessage
|
||||
? [threadHeadMessage, ...threadMessages]
|
||||
: threadMessages,
|
||||
);
|
||||
return target ? routeEdit(target) : false;
|
||||
}, [findLastOwnEditable, routeEdit, threadHeadMessage, threadMessages]);
|
||||
React.useEffect(() => {
|
||||
const pending = pendingMainEditRef.current;
|
||||
if (!pending || isSinglePanelView || channelIsCovered) return;
|
||||
pendingMainEditRef.current = null;
|
||||
onEdit?.(pending);
|
||||
}, [channelIsCovered, isSinglePanelView, onEdit]);
|
||||
return {
|
||||
handleEditLastOwnMainMessage,
|
||||
handleEditLastOwnThreadMessage,
|
||||
routeEdit,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user