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
59 changes: 26 additions & 33 deletions desktop/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useQueryClient } from "@tanstack/react-query";

import { ChannelPane } from "@/app/ChannelPane";
import { useActiveChannelHeader } from "@/app/useActiveChannelHeader";
import { useChannelPaneHandlers } from "@/app/useChannelPaneHandlers";
import { AgentsView } from "@/features/agents/ui/AgentsView";
import { ForumView } from "@/features/forum/ui/ForumView";
import { ChatHeader } from "@/features/chat/ui/ChatHeader";
Expand Down Expand Up @@ -203,6 +204,26 @@ export function AppShell() {
[replyTargetId, timelineMessages],
);

const { handleCancelReply, handleReply, handleSend, handleToggleReaction } =
useChannelPaneHandlers({
replyTargetId,
sendMessageMutation,
setReplyTargetId,
toggleReactionMutation,
});

const handleTargetReached = React.useCallback((messageId: string) => {
setSearchAnchor((current) =>
current?.eventId === messageId ? null : current,
);
}, []);

const canReact = activeChannel !== null && activeChannel.archivedAt === null;
const effectiveToggleReaction = React.useMemo(
() => (canReact ? handleToggleReaction : undefined),
[canReact, handleToggleReaction],
);

const channelDescription = activeChannel
? [
activeChannel.archivedAt ? "Archived." : null,
Expand Down Expand Up @@ -664,39 +685,11 @@ export function AppShell() {
isSending={sendMessageMutation.isPending}
isTimelineLoading={isTimelineLoading}
messages={timelineMessages}
onCancelReply={() => {
setReplyTargetId(null);
}}
onReply={(message) => {
setReplyTargetId((current) =>
current === message.id ? null : message.id,
);
}}
onSend={async (content, mentionPubkeys, mediaTags) => {
await sendMessageMutation.mutateAsync({
content,
mentionPubkeys,
parentEventId: replyTargetId,
mediaTags,
});
setReplyTargetId(null);
}}
onTargetReached={(messageId) => {
setSearchAnchor((current) =>
current?.eventId === messageId ? null : current,
);
}}
onToggleReaction={
activeChannel && activeChannel.archivedAt === null
? async (message, emoji, remove) => {
await toggleReactionMutation.mutateAsync({
emoji,
eventId: message.id,
remove,
});
}
: undefined
}
onCancelReply={handleCancelReply}
onReply={handleReply}
onSend={handleSend}
onTargetReached={handleTargetReached}
onToggleReaction={effectiveToggleReaction}
profiles={messageProfiles}
replyTargetId={replyTargetId}
replyTargetMessage={replyTargetMessage}
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/app/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type ChannelPaneProps = {
typingPubkeys: string[];
};

export function ChannelPane({
export const ChannelPane = React.memo(function ChannelPane({
activeChannel,
currentPubkey,
isSending,
Expand Down Expand Up @@ -118,4 +118,4 @@ export function ChannelPane({
/>
</React.Fragment>
);
}
});
82 changes: 82 additions & 0 deletions desktop/src/app/useChannelPaneHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from "react";

import type { useSendMessageMutation } from "@/features/messages/hooks";
import type { useToggleReactionMutation } from "@/features/messages/hooks";

/**
* Stable callback references for ChannelPane so that keystroke-driven
* re-renders of AppShell don't cascade into the timeline and composer.
*
* Mutation objects from TanStack Query v5 are new references on every render
* (especially when `isPending` flips), so we stash `.mutateAsync` in a ref
* rather than listing the whole mutation as a dependency.
*/
export function useChannelPaneHandlers({
replyTargetId,
sendMessageMutation,
setReplyTargetId,
toggleReactionMutation,
}: {
replyTargetId: string | null;
sendMessageMutation: ReturnType<typeof useSendMessageMutation>;
setReplyTargetId: React.Dispatch<React.SetStateAction<string | null>>;
toggleReactionMutation: ReturnType<typeof useToggleReactionMutation>;
}) {
// Keep mutable values in refs so callbacks never need to list them as deps.
const replyTargetIdRef = React.useRef(replyTargetId);
replyTargetIdRef.current = replyTargetId;

const sendMutateRef = React.useRef(sendMessageMutation.mutateAsync);
sendMutateRef.current = sendMessageMutation.mutateAsync;

const toggleMutateRef = React.useRef(toggleReactionMutation.mutateAsync);
toggleMutateRef.current = toggleReactionMutation.mutateAsync;

const handleCancelReply = React.useCallback(() => {
setReplyTargetId(null);
}, [setReplyTargetId]);

const handleReply = React.useCallback(
(message: { id: string }) => {
setReplyTargetId((current) =>
current === message.id ? null : message.id,
);
},
[setReplyTargetId],
);

const handleSend = React.useCallback(
async (
content: string,
mentionPubkeys: string[],
mediaTags?: string[][],
) => {
await sendMutateRef.current({
content,
mentionPubkeys,
parentEventId: replyTargetIdRef.current,
mediaTags,
});
setReplyTargetId(null);
},
[setReplyTargetId],
);

const handleToggleReaction = React.useCallback(
async (message: { id: string }, emoji: string, remove: boolean) => {
await toggleMutateRef.current({
emoji,
eventId: message.id,
remove,
});
},
[],
);

return {
handleCancelReply,
handleReply,
handleSend,
handleToggleReaction,
};
}
4 changes: 2 additions & 2 deletions desktop/src/features/messages/ui/ChannelAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type ChannelAutocompleteProps = {
onSelect: (suggestion: ChannelSuggestion) => void;
};

export function ChannelAutocomplete({
export const ChannelAutocomplete = React.memo(function ChannelAutocomplete({
suggestions,
selectedIndex,
onSelect,
Expand Down Expand Up @@ -58,4 +58,4 @@ export function ChannelAutocomplete({
</div>
</div>
);
}
});
5 changes: 3 additions & 2 deletions desktop/src/features/messages/ui/ComposerEmojiPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from "react";
import Picker from "@emoji-mart/react";
import data from "@emoji-mart/data";
import { SmilePlus } from "lucide-react";
Expand All @@ -13,7 +14,7 @@ type ComposerEmojiPickerProps = {
open: boolean;
};

export function ComposerEmojiPicker({
export const ComposerEmojiPicker = React.memo(function ComposerEmojiPicker({
disabled = false,
onEmojiSelect,
onOpenChange,
Expand Down Expand Up @@ -55,4 +56,4 @@ export function ComposerEmojiPicker({
</PopoverContent>
</Popover>
);
}
});
4 changes: 2 additions & 2 deletions desktop/src/features/messages/ui/MentionAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type MentionAutocompleteProps = {
onSelect: (suggestion: MentionSuggestion) => void;
};

export function MentionAutocomplete({
export const MentionAutocomplete = React.memo(function MentionAutocomplete({
suggestions,
selectedIndex,
onSelect,
Expand Down Expand Up @@ -67,4 +67,4 @@ export function MentionAutocomplete({
</div>
</div>
);
}
});
Loading
Loading