diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 41ea3a515..5578f9f9e 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -35,7 +35,8 @@ const overrides = new Map([ ["src/app/AppShell.tsx", 860], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + memory-leak safeguards ["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation ["src/features/channels/ui/ChannelManagementSheet.tsx", 800], - ["src/features/channels/ui/ChannelScreen.tsx", 530], // profile panel state + mutual exclusion wiring + ProfilePanelProvider context + ["src/features/channels/ui/ChannelPane.tsx", 520], // composer/timeline/sidebar orchestration + anchored agent activity footers + ["src/features/channels/ui/ChannelScreen.tsx", 550], // profile panel state + mutual exclusion wiring + ProfilePanelProvider context + agent typing classification ["src/features/messages/hooks.ts", 500], // message query/mutation hooks + optimistic updates ["src/features/messages/ui/MessageComposer.tsx", 700], // media upload handlers (paste, drop, dialog) + channelId reset effect + edit mode (pre-fill, save, cancel, escape) ["src/features/settings/ui/SettingsView.tsx", 600], diff --git a/desktop/src/features/agents/ui/PersonaCatalogDetailsSheet.tsx b/desktop/src/features/agents/ui/PersonaCatalogDetailsSheet.tsx index 6dee50974..0df75a779 100644 --- a/desktop/src/features/agents/ui/PersonaCatalogDetailsSheet.tsx +++ b/desktop/src/features/agents/ui/PersonaCatalogDetailsSheet.tsx @@ -1,7 +1,6 @@ import { isCatalogPersonaSelected } from "@/features/agents/lib/catalog"; import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import type { AgentPersona } from "@/shared/api/types"; -import { Card } from "@/shared/ui/card"; import { cn } from "@/shared/lib/cn"; import { promptPreview } from "@/shared/lib/promptPreview"; import { @@ -19,6 +18,8 @@ import { } from "./personaLibraryCopy"; type PersonaCatalogDetailsSheetProps = { + feedbackErrorMessage: string | null; + feedbackNoticeMessage: string | null; isPending: boolean; onOpenChange: (open: boolean) => void; onTogglePersona: (persona: AgentPersona) => void; @@ -27,6 +28,8 @@ type PersonaCatalogDetailsSheetProps = { }; export function PersonaCatalogDetailsSheet({ + feedbackErrorMessage, + feedbackNoticeMessage, isPending, onOpenChange, onTogglePersona, @@ -40,12 +43,12 @@ export function PersonaCatalogDetailsSheet({ return ( {persona ? ( -
- + <> +
+
+ + + {feedbackNoticeMessage ? ( +

+ {feedbackNoticeMessage} +

+ ) : null} + + {feedbackErrorMessage ? ( +

+ {feedbackErrorMessage} +

+ ) : null} - -
- -

- Type -

-

Built-in persona

-
- +

- Preferred model + System prompt

-

- {persona.model ?? "Use app default"} -

- - -

- Preferred provider -

-

- {persona.provider ?? "Use app default"} -

-
+
+                  {persona.systemPrompt}
+                
+
- - -

- System prompt -

-
-                {persona.systemPrompt}
-              
-
-
+ ) : null} diff --git a/desktop/src/features/agents/ui/PersonaCatalogSurface.tsx b/desktop/src/features/agents/ui/PersonaCatalogSurface.tsx index 23fd5b885..159025f7b 100644 --- a/desktop/src/features/agents/ui/PersonaCatalogSurface.tsx +++ b/desktop/src/features/agents/ui/PersonaCatalogSurface.tsx @@ -62,6 +62,8 @@ export function PersonaCatalogSurface({ /> { if (!open) { diff --git a/desktop/src/features/channels/ui/BotActivityBar.tsx b/desktop/src/features/channels/ui/BotActivityBar.tsx index c0179d0c7..8d023dd96 100644 --- a/desktop/src/features/channels/ui/BotActivityBar.tsx +++ b/desktop/src/features/channels/ui/BotActivityBar.tsx @@ -1,157 +1,166 @@ -import { Bot, Loader2 } from "lucide-react"; +import * as React from "react"; +import { Loader2 } from "lucide-react"; +import type { UserProfileLookup } from "@/features/profile/lib/identity"; import type { ManagedAgent } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/shared/ui/dropdown-menu"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; +import { UserAvatar } from "@/shared/ui/UserAvatar"; + +export type BotActivityAgent = Pick; type BotActivityBarProps = { - agents: ManagedAgent[]; + agents: BotActivityAgent[]; onOpenAgentSession: (pubkey: string) => void; openAgentSessionPubkey: string | null; + profiles?: UserProfileLookup; typingBotPubkeys: string[]; }; -const COMPACT_THRESHOLD = 4; -const OVERFLOW_THRESHOLD = 6; -const MAX_VISIBLE_WITH_OVERFLOW = 5; +const HOVER_OPEN_DELAY_MS = 150; +const HOVER_CLOSE_DELAY_MS = 180; -/** - * Compact right-aligned row of clickable bot pills. - * Only renders pills for bots that are currently typing (actively working). - */ -export function BotActivityBar({ +export function BotActivityComposerAction({ agents, onOpenAgentSession, openAgentSessionPubkey, + profiles, typingBotPubkeys, }: BotActivityBarProps) { - if (typingBotPubkeys.length === 0) { - return null; - } - - const typingSet = new Set( - typingBotPubkeys.map((pubkey) => pubkey.toLowerCase()), + const [open, setOpen] = React.useState(false); + const hoverTimerRef = React.useRef | null>( + null, ); - const typingAgents = agents.filter((agent) => - typingSet.has(agent.pubkey.toLowerCase()), - ); + const typingAgents = React.useMemo(() => { + const typingSet = new Set( + typingBotPubkeys.map((pubkey) => pubkey.toLowerCase()), + ); + + return agents.filter((agent) => typingSet.has(agent.pubkey.toLowerCase())); + }, [agents, typingBotPubkeys]); + + const clearHoverTimer = React.useCallback(() => { + if (hoverTimerRef.current !== null) { + clearTimeout(hoverTimerRef.current); + hoverTimerRef.current = null; + } + }, []); + + const openWithDelay = React.useCallback(() => { + clearHoverTimer(); + hoverTimerRef.current = setTimeout(() => { + setOpen(true); + }, HOVER_OPEN_DELAY_MS); + }, [clearHoverTimer]); + + const closeWithDelay = React.useCallback(() => { + clearHoverTimer(); + hoverTimerRef.current = setTimeout(() => { + setOpen(false); + }, HOVER_CLOSE_DELAY_MS); + }, [clearHoverTimer]); + + const keepOpen = React.useCallback(() => { + clearHoverTimer(); + }, [clearHoverTimer]); + + React.useEffect(() => { + return () => clearHoverTimer(); + }, [clearHoverTimer]); if (typingAgents.length === 0) { return null; } - const { hiddenAgents, visibleAgents } = splitVisibleAgents( - typingAgents, - openAgentSessionPubkey, - ); - const isCompact = typingAgents.length >= COMPACT_THRESHOLD; + const agentAvatarUrl = (agent: BotActivityAgent) => + profiles?.[agent.pubkey.toLowerCase()]?.avatarUrl ?? null; + const selectedPubkey = openAgentSessionPubkey?.toLowerCase() ?? null; + const triggerLabel = + typingAgents.length === 1 + ? `${typingAgents[0]?.name ?? "Agent"} is working` + : `${typingAgents.length} agents working`; return ( -
- {visibleAgents.map((agent) => { - const isSelected = - openAgentSessionPubkey?.toLowerCase() === agent.pubkey.toLowerCase(); - return ( - - + + + + + event.preventDefault()} + side="top" + sideOffset={8} + > +
+ Agents working +
+
+ {typingAgents.map((agent) => { + const isSelected = selectedPubkey === agent.pubkey.toLowerCase(); + + return ( - - - {agent.name} is working — click to view activity - - - ); - })} - - {hiddenAgents.length > 0 ? ( - - - - - - - More agents working - - {hiddenAgents.map((agent) => ( - onOpenAgentSession(agent.pubkey)} + onClick={() => { + clearHoverTimer(); + setOpen(false); + onOpenAgentSession(agent.pubkey); + }} + type="button" > - + {agent.name} - - - ))} - - - ) : null} -
- ); -} - -function splitVisibleAgents( - typingAgents: ManagedAgent[], - openAgentSessionPubkey: string | null, -): { visibleAgents: ManagedAgent[]; hiddenAgents: ManagedAgent[] } { - if (typingAgents.length < OVERFLOW_THRESHOLD) { - return { visibleAgents: typingAgents, hiddenAgents: [] }; - } - - const selectedAgent = openAgentSessionPubkey - ? typingAgents.find( - (agent) => - agent.pubkey.toLowerCase() === openAgentSessionPubkey.toLowerCase(), - ) - : null; - - const visibleAgents = typingAgents.slice(0, MAX_VISIBLE_WITH_OVERFLOW); - - if ( - selectedAgent && - !visibleAgents.some((agent) => agent.pubkey === selectedAgent.pubkey) - ) { - visibleAgents[visibleAgents.length - 1] = selectedAgent; - } - - const visibleSet = new Set(visibleAgents.map((agent) => agent.pubkey)); - const hiddenAgents = typingAgents.filter( - (agent) => !visibleSet.has(agent.pubkey), + + + ); + })} +
+ + ); - - return { visibleAgents, hiddenAgents }; } diff --git a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx index 8ec47403f..6685fb580 100644 --- a/desktop/src/features/channels/ui/ChannelManagementSheet.tsx +++ b/desktop/src/features/channels/ui/ChannelManagementSheet.tsx @@ -38,7 +38,6 @@ import { } from "@/shared/ui/alert-dialog"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; -import { Separator } from "@/shared/ui/separator"; import { Sheet, SheetContent, @@ -214,7 +213,7 @@ export function ChannelManagementSheet({ data-testid="channel-management-sheet" side="right" > - +
{channel.name} @@ -260,63 +259,59 @@ export function ChannelManagementSheet({ ) : null} {showAccessSection ? ( - <> -
-
- {canJoin ? ( - - ) : null} - - {canLeave ? ( - - ) : null} -
- {joinChannelMutation.error instanceof Error ? ( -

- {joinChannelMutation.error.message} -

- ) : null} - {leaveChannelMutation.error instanceof Error ? ( -

- {leaveChannelMutation.error.message} -

+
+
+ {canJoin ? ( + ) : null} -
- - + {canLeave ? ( + + ) : null} +
+ {joinChannelMutation.error instanceof Error ? ( +

+ {joinChannelMutation.error.message} +

+ ) : null} + {leaveChannelMutation.error instanceof Error ? ( +

+ {leaveChannelMutation.error.message} +

+ ) : null} + ) : null}
- -
- -
{resolvedChannel.channelType !== "dm" ? ( - <> - - -
-
- {isArchived ? ( - - ) : ( - - )} -
- {archiveChannelMutation.error instanceof Error ? ( -

- {archiveChannelMutation.error.message} -

- ) : null} - {unarchiveChannelMutation.error instanceof Error ? ( -

- {unarchiveChannelMutation.error.message} -

- ) : null} -
- +
+
+ {isArchived ? ( + + ) : ( + + )} +
+ {archiveChannelMutation.error instanceof Error ? ( +

+ {archiveChannelMutation.error.message} +

+ ) : null} + {unarchiveChannelMutation.error instanceof Error ? ( +

+ {unarchiveChannelMutation.error.message} +

+ ) : null} +
) : null} {isOwner && resolvedChannel.channelType !== "dm" ? ( - <> - - -
+ - - - - - - - Delete channel? - - Delete {resolvedChannel.name} from the workspace list. - This action cannot be undone. - - - {deleteChannelMutation.error instanceof Error ? ( -

- {deleteChannelMutation.error.message} -

- ) : null} - - - - - - - - -
-
-
- + + + + + + Delete channel? + + Delete {resolvedChannel.name} from the workspace list. + This action cannot be undone. + + + {deleteChannelMutation.error instanceof Error ? ( +

+ {deleteChannelMutation.error.message} +

+ ) : null} + + + + + + + + +
+ +
) : null}
diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index a82b78513..7f8b50f2b 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -5,10 +5,14 @@ import { MessageComposer } from "@/features/messages/ui/MessageComposer"; import { MessageThreadPanel } from "@/features/messages/ui/MessageThreadPanel"; import { MessageTimeline } from "@/features/messages/ui/MessageTimeline"; import { TypingIndicatorRow } from "@/features/messages/ui/TypingIndicatorRow"; +import type { TypingIndicatorEntry } from "@/features/messages/useChannelTyping"; import { UserProfilePanel } from "@/features/profile/ui/UserProfilePanel"; import { ChannelFindBar } from "@/features/search/ui/ChannelFindBar"; import { AgentSessionThreadPanel } from "@/features/channels/ui/AgentSessionThreadPanel"; -import { BotActivityBar } from "@/features/channels/ui/BotActivityBar"; +import { + BotActivityComposerAction, + type BotActivityAgent, +} from "@/features/channels/ui/BotActivityBar"; import { Button } from "@/shared/ui/button"; import type { useChannelFind } from "@/features/search/useChannelFind"; import type { MainTimelineEntry } from "@/features/messages/lib/threadPanel"; @@ -52,8 +56,9 @@ function getInitialThreadPanelWidth(): number { type ChannelPaneProps = { activeChannel: Channel | null; + activityAgents?: BotActivityAgent[]; agentSessionAgents: ManagedAgent[]; - botTypingPubkeys: string[]; + botTypingEntries: TypingIndicatorEntry[]; channelFind: ReturnType; currentPubkey?: string; editTarget?: { @@ -118,7 +123,8 @@ type ChannelPaneProps = { export const ChannelPane = React.memo(function ChannelPane({ activeChannel, agentSessionAgents, - botTypingPubkeys, + activityAgents = agentSessionAgents, + botTypingEntries, channelFind, currentPubkey, editTarget = null, @@ -229,6 +235,41 @@ export const ChannelPane = React.memo(function ChannelPane({ activeChannel.archivedAt !== null || activeChannel.channelType === "forum" || isSending; + const hasTypingActivity = typingPubkeys.length > 0; + const composerBotTypingPubkeys = React.useMemo(() => { + const pubkeys: string[] = []; + for (const entry of botTypingEntries) { + if ( + !pubkeys.some( + (pubkey) => pubkey.toLowerCase() === entry.pubkey.toLowerCase(), + ) + ) { + pubkeys.push(entry.pubkey); + } + } + return pubkeys; + }, [botTypingEntries]); + const threadComposerBotTypingPubkeys = React.useMemo(() => { + if (!openThreadHeadId) { + return []; + } + + const pubkeys: string[] = []; + for (const entry of botTypingEntries) { + if (entry.threadHeadId !== openThreadHeadId) { + continue; + } + + if ( + !pubkeys.some( + (pubkey) => pubkey.toLowerCase() === entry.pubkey.toLowerCase(), + ) + ) { + pubkeys.push(entry.pubkey); + } + } + return pubkeys; + }, [botTypingEntries, openThreadHeadId]); const selectedAgent = React.useMemo( () => @@ -241,8 +282,8 @@ export const ChannelPane = React.memo(function ChannelPane({ ); return ( -
-
+
+
{channelFind.isOpen ? ( + {hasTypingActivity ? ( +
+ +
+ ) : null} {isNonMemberView ? (
) : ( - - )} -
- -
- + + } + placeholder={ + activeChannel?.archivedAt + ? "Archived channels are read-only." + : activeChannel?.channelType === "forum" + ? "Forum posting is not wired in this pass." + : activeChannel + ? `Message #${activeChannel.name}` + : "Select a channel" + } + showTopBorder={false} />
-
+ )}
{threadHeadMessage ? ( @@ -384,15 +431,24 @@ export const ChannelPane = React.memo(function ChannelPane({ widthPx={threadPanelWidthPx} threadReplies={threadMessages} threadTypingPubkeys={threadTypingPubkeys} + toolbarExtraActions={ + + } /> ) : activeChannel && selectedAgent ? ( - pubkey.toLowerCase() === selectedAgent.pubkey.toLowerCase(), + isWorking={botTypingEntries.some( + (entry) => + entry.pubkey.toLowerCase() === selectedAgent.pubkey.toLowerCase(), )} onClose={onCloseAgentSession} onResetWidth={handleThreadPanelWidthReset} diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index de35cf856..b85a826ce 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -34,7 +34,10 @@ import { import { buildThreadPanelData } from "@/features/messages/lib/threadPanel"; import { useFetchOlderMessages } from "@/features/messages/useFetchOlderMessages"; import { useLoadMissingAncestors } from "@/features/messages/useLoadMissingAncestors"; -import { useChannelTyping } from "@/features/messages/useChannelTyping"; +import { + type TypingIndicatorEntry, + useChannelTyping, +} from "@/features/messages/useChannelTyping"; import { useUsersBatchQuery } from "@/features/profile/hooks"; import { mergeCurrentProfileIntoLookup } from "@/features/profile/lib/identity"; import type { @@ -142,13 +145,6 @@ export function ChannelScreen({ currentPubkey, latestMessageEvent, ); - const mainTypingPubkeys = React.useMemo( - () => - typingEntries - .filter((entry) => entry.threadHeadId === null) - .map((entry) => entry.pubkey), - [typingEntries], - ); const threadTypingPubkeys = React.useMemo( () => typingEntries @@ -170,21 +166,28 @@ export function ChannelScreen({ }); const managedAgentsQuery = useManagedAgentsQuery(); useManagedAgentObserverBridge(managedAgentsQuery.data ?? []); - const { humanTypingPubkeys, botTypingPubkeys } = React.useMemo(() => { + const { botTypingEntries, humanTypingPubkeys } = React.useMemo<{ + botTypingEntries: TypingIndicatorEntry[]; + humanTypingPubkeys: string[]; + }>(() => { const localAgentSet = new Set( (managedAgentsQuery.data ?? []) .filter((agent) => agent.backend.type === "local") .map((agent) => agent.pubkey.toLowerCase()), ); + const channelTypingEntries = typingEntries.filter( + (entry) => entry.threadHeadId === null, + ); + const agentTypingEntries = typingEntries.filter((entry) => + localAgentSet.has(entry.pubkey.toLowerCase()), + ); return { - humanTypingPubkeys: mainTypingPubkeys.filter( - (pk) => !localAgentSet.has(pk.toLowerCase()), - ), - botTypingPubkeys: mainTypingPubkeys.filter((pk) => - localAgentSet.has(pk.toLowerCase()), - ), + botTypingEntries: agentTypingEntries, + humanTypingPubkeys: channelTypingEntries + .filter((entry) => !localAgentSet.has(entry.pubkey.toLowerCase())) + .map((entry) => entry.pubkey), }; - }, [mainTypingPubkeys, managedAgentsQuery.data]); + }, [managedAgentsQuery.data, typingEntries]); const messageProfiles = React.useMemo(() => { const base = mergeCurrentProfileIntoLookup( @@ -270,6 +273,23 @@ export function ChannelScreen({ (messageId: string) => directReplyIdsByParentId.get(messageId)?.[0] ?? null, [directReplyIdsByParentId], ); + const getReplyDescendantIdsForMessage = React.useCallback( + (messageId: string) => { + const descendantIds: string[] = []; + const pendingIds = [...(directReplyIdsByParentId.get(messageId) ?? [])]; + + while (pendingIds.length > 0) { + const currentId = pendingIds.pop(); + if (!currentId) continue; + + descendantIds.push(currentId); + pendingIds.push(...(directReplyIdsByParentId.get(currentId) ?? [])); + } + + return descendantIds; + }, + [directReplyIdsByParentId], + ); const threadPanelData = React.useMemo( () => buildThreadPanelData( @@ -314,6 +334,7 @@ export function ChannelScreen({ editTargetId, expandedThreadReplyIds, getFirstReplyIdForMessage, + getReplyDescendantIdsForMessage, openThreadHeadId, sendMessageMutation, setExpandedThreadReplyIds, @@ -450,7 +471,7 @@ export function ChannelScreen({ - {isHeader ? {label} : null} ); } diff --git a/desktop/src/features/channels/ui/MembersSidebar.tsx b/desktop/src/features/channels/ui/MembersSidebar.tsx index 6156462dc..7f69c8533 100644 --- a/desktop/src/features/channels/ui/MembersSidebar.tsx +++ b/desktop/src/features/channels/ui/MembersSidebar.tsx @@ -196,7 +196,7 @@ export function MembersSidebar({ data-testid="members-sidebar" side="right" > - + Members People and bots in {channel.name}. diff --git a/desktop/src/features/channels/useChannelPaneHandlers.ts b/desktop/src/features/channels/useChannelPaneHandlers.ts index 98751c636..f1adf9eef 100644 --- a/desktop/src/features/channels/useChannelPaneHandlers.ts +++ b/desktop/src/features/channels/useChannelPaneHandlers.ts @@ -21,6 +21,7 @@ export function useChannelPaneHandlers({ editTargetId, expandedThreadReplyIds, getFirstReplyIdForMessage, + getReplyDescendantIdsForMessage, openThreadHeadId, sendMessageMutation, setExpandedThreadReplyIds, @@ -36,6 +37,7 @@ export function useChannelPaneHandlers({ editTargetId: string | null; expandedThreadReplyIds: ReadonlySet; getFirstReplyIdForMessage: (messageId: string) => string | null; + getReplyDescendantIdsForMessage: (messageId: string) => string[]; openThreadHeadId: string | null; sendMessageMutation: ReturnType; setExpandedThreadReplyIds: React.Dispatch>>; @@ -158,21 +160,33 @@ export function useChannelPaneHandlers({ const handleExpandThreadReplies = React.useCallback( (message: { id: string }) => { - const firstReplyId = getFirstReplyIdForMessage(message.id); - if (!expandedThreadReplyIdsRef.current.has(message.id)) { + if (expandedThreadReplyIdsRef.current.has(message.id)) { + const descendantIds = getReplyDescendantIdsForMessage(message.id); setExpandedThreadReplyIds((current) => { const next = new Set(current); - next.add(message.id); + next.delete(message.id); + for (const descendantId of descendantIds) { + next.delete(descendantId); + } return next; }); + return; } + const firstReplyId = getFirstReplyIdForMessage(message.id); + setExpandedThreadReplyIds((current) => { + const next = new Set(current); + next.add(message.id); + return next; + }); + if (firstReplyId) { setThreadScrollTargetId(firstReplyId); } }, [ getFirstReplyIdForMessage, + getReplyDescendantIdsForMessage, setExpandedThreadReplyIds, setThreadScrollTargetId, ], diff --git a/desktop/src/features/chat/ui/ChatHeader.tsx b/desktop/src/features/chat/ui/ChatHeader.tsx index 1e1eb20a3..102569732 100644 --- a/desktop/src/features/chat/ui/ChatHeader.tsx +++ b/desktop/src/features/chat/ui/ChatHeader.tsx @@ -71,9 +71,11 @@ export function ChatHeader({ mode = "channel", statusBadge, }: ChatHeaderProps) { + const trimmedDescription = description.trim(); + return (
@@ -87,6 +89,7 @@ export function ChatHeader({

{title}

@@ -96,12 +99,6 @@ export function ChatHeader({
) : null}
-

- {description} -

{actions ?
{actions}
: null} diff --git a/desktop/src/features/messages/ui/DayDivider.tsx b/desktop/src/features/messages/ui/DayDivider.tsx index 93a03cf21..f7926801d 100644 --- a/desktop/src/features/messages/ui/DayDivider.tsx +++ b/desktop/src/features/messages/ui/DayDivider.tsx @@ -1,18 +1,14 @@ -import { Separator } from "@/shared/ui/separator"; - export function DayDivider({ label }: { label: string }) { return (
- -

+

{label}

-
); } diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx index a635c556c..2f7e78421 100644 --- a/desktop/src/features/messages/ui/MessageComposer.tsx +++ b/desktop/src/features/messages/ui/MessageComposer.tsx @@ -20,6 +20,7 @@ import { } from "@/features/messages/lib/normalizeMentionClipboard"; import { useRichTextEditor } from "@/features/messages/lib/useRichTextEditor"; import { useTypingBroadcast } from "@/features/messages/useTypingBroadcast"; +import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { ChannelAutocomplete } from "./ChannelAutocomplete"; import { ComposerAttachments } from "./ComposerAttachments"; @@ -56,6 +57,8 @@ type MessageComposerProps = { body: string; id: string; } | null; + showTopBorder?: boolean; + toolbarExtraActions?: React.ReactNode; typingParentEventId?: string | null; typingRootEventId?: string | null; }; @@ -74,6 +77,8 @@ export function MessageComposer({ placeholder, profiles, replyTarget = null, + showTopBorder = false, + toolbarExtraActions, typingParentEventId = null, typingRootEventId = null, }: MessageComposerProps) { @@ -537,10 +542,15 @@ export function MessageComposer({ // ── Render ────────────────────────────────────────────────────────── return ( -