@@ -40,6 +40,7 @@ import { useFocusDrawerPresence } from "@/features/channels/ui/useFocusDrawerPre
4040import { useCardMintJobs } from "@/features/agents/cardMintStore" ;
4141import { BotActivityComposerAction } from "@/features/channels/ui/BotActivityBar" ;
4242import { ChannelComposerActivityRow } from "@/features/channels/ui/ChannelComposerActivityRow" ;
43+ import { useThreadComposerActivity } from "@/features/channels/ui/useThreadComposerActivity" ;
4344import { ComposerActivityAccessory } from "@/features/messages/ui/ComposerActivityAccessory" ;
4445import { TypingIndicatorRow } from "@/features/messages/ui/TypingIndicatorRow" ;
4546import {
@@ -286,6 +287,7 @@ export const ChannelPane = React.memo(function ChannelPane({
286287 onEdit ( target ) ;
287288 return true ;
288289 } , [ findLastOwnEditable , messages , onEdit ] ) ;
290+
289291 const handleEditLastOwnThreadMessage = React . useCallback ( ( ) : boolean => {
290292 if ( ! onEdit ) return false ;
291293 const scope : TimelineMessage [ ] = [ ] ;
@@ -296,7 +298,9 @@ export const ChannelPane = React.memo(function ChannelPane({
296298 onEdit ( target ) ;
297299 return true ;
298300 } , [ findLastOwnEditable , onEdit , threadHeadMessage , threadMessages ] ) ;
301+
299302 const timeoutState = useTimeoutState ( ) ;
303+
300304 // A moderation DM (1:1 with the relay identity) is read-only for the member;
301305 // only DMs pay for the NIP-11 `self` lookup. Fails open: no `relaySelf` →
302306 // ordinary DM, composer enabled.
@@ -306,6 +310,7 @@ export const ChannelPane = React.memo(function ChannelPane({
306310 currentPubkey ,
307311 relaySelfQuery . data ,
308312 ) ;
313+
309314 const isComposerDisabled =
310315 ! activeChannel ?. isMember ||
311316 activeChannel . archivedAt !== null ||
@@ -315,6 +320,7 @@ export const ChannelPane = React.memo(function ChannelPane({
315320 isSending ;
316321 const knownAgentPubkeys = React . useMemo ( ( ) => {
317322 const pubkeys = new Set < string > ( ) ;
323+
318324 for ( const pubkey of agentPubkeys ?? [ ] ) {
319325 pubkeys . add ( pubkey . toLowerCase ( ) ) ;
320326 }
@@ -324,12 +330,14 @@ export const ChannelPane = React.memo(function ChannelPane({
324330 for ( const agent of activityAgents ) {
325331 pubkeys . add ( agent . pubkey . toLowerCase ( ) ) ;
326332 }
333+
327334 return pubkeys ;
328335 } , [ activityAgents , agentPubkeys , agentSessionAgents ] ) ;
329336 const completeWelcomeComposerBanner = React . useCallback ( ( ) => {
330337 if ( ! activeChannelId || ! isActiveWelcomeChannel ) {
331338 return ;
332339 }
340+
333341 clearWelcomeComposerDismissTimer ( ) ;
334342 completedWelcomeBannerChannelIdsRef . current . add ( activeChannelId ) ;
335343 setWelcomeComposerBannerState ( "complete" ) ;
@@ -361,8 +369,10 @@ export const ChannelPane = React.memo(function ChannelPane({
361369 isActiveWelcomeChannel &&
362370 ( containsWelcomePersonaMention ( content ) ||
363371 mentionsKnownAgent ( mentionPubkeys , knownAgentPubkeys ) ) ;
372+
364373 messageTimelineRef . current ?. scrollToBottomOnNextUpdate ( ) ;
365374 await onSendMessage ( content , mentionPubkeys , mediaTags , channelId ) ;
375+
366376 if (
367377 channelId &&
368378 channelId !== activeChannelId &&
@@ -371,6 +381,7 @@ export const ChannelPane = React.memo(function ChannelPane({
371381 ) {
372382 await goChannel ( channelId , { replace : true } ) ;
373383 }
384+
374385 if ( shouldCompleteWelcomeBanner ) {
375386 completeWelcomeComposerBanner ( ) ;
376387 }
@@ -389,32 +400,24 @@ export const ChannelPane = React.memo(function ChannelPane({
389400 ! isComposerDisabled &&
390401 ! isMainDeferredEditPending &&
391402 ! isSinglePanelView ;
392- // Working set for the composer bar (observer turns + bot-typing fallback,
393- // folded by agentWorkingSignal); gates the dock's reserved bottom rail.
394403 const composerWorkingBotPubkeys = useChannelWorkingAgentPubkeys (
395404 activeChannel ?. id ?? null ,
396405 ) ;
397- // Background card mints surface in the same rail ("Minting card…" chip),
398- // so they must also reserve the activity row.
399406 const hasCardMintActivity = useCardMintJobs ( ) . length > 0 ;
400407 const hasComposerBottomActivity =
401408 composerWorkingBotPubkeys . length > 0 ||
402409 typingPubkeys . length > 0 ||
403410 hasCardMintActivity ;
404- const threadComposerBotTypingPubkeys = React . useMemo ( ( ) => {
405- if ( ! openThreadHeadId ) return [ ] ;
406- return botTypingEntries
407- . filter ( ( entry ) => entry . threadHeadId === openThreadHeadId )
408- . map ( ( entry ) => entry . pubkey )
409- . filter (
410- ( pubkey , index , all ) =>
411- all . findIndex (
412- ( candidate ) => candidate . toLowerCase ( ) === pubkey . toLowerCase ( ) ,
413- ) === index ,
414- ) ;
415- } , [ botTypingEntries , openThreadHeadId ] ) ;
416- const hasThreadComposerBotActivity =
417- threadComposerBotTypingPubkeys . length > 0 ;
411+ const {
412+ combinedTypingPubkeys : combinedThreadTypingPubkeys ,
413+ hasActivity : hasThreadComposerActivity ,
414+ pillBotPubkeys : threadPillBotPubkeys ,
415+ } = useThreadComposerActivity ( {
416+ botTypingEntries,
417+ channelId : activeChannel ?. id ?? null ,
418+ threadHeadId : openThreadHeadId ,
419+ typingPubkeys : threadTypingPubkeys ,
420+ } ) ;
418421 const directMessageIntro = React . useMemo (
419422 ( ) =>
420423 buildDirectMessageIntro ( {
@@ -424,6 +427,7 @@ export const ChannelPane = React.memo(function ChannelPane({
424427 } ) ,
425428 [ activeChannel , currentPubkey , profiles ] ,
426429 ) ;
430+
427431 const handleWelcomeAddAgent = React . useCallback ( ( ) => {
428432 onAddAgent ?.( {
429433 beforeSend : ( ) =>
@@ -465,6 +469,7 @@ export const ChannelPane = React.memo(function ChannelPane({
465469 for ( const message of threadAllMessages ) {
466470 messagesById . set ( message . id , message ) ;
467471 }
472+
468473 return buildVideoReviewContextsByMessageId ( {
469474 channelId : activeChannel ?. id ?? null ,
470475 channelName : activeChannel ?. name ,
@@ -485,6 +490,7 @@ export const ChannelPane = React.memo(function ChannelPane({
485490 threadAllMessages ,
486491 threadHeadMessage ,
487492 ] ) ;
493+
488494 const isOverlay = useIsThreadPanelOverlay ( ) ;
489495 const useSplitAuxiliaryPane = ! isSinglePanelView && ! isOverlay ;
490496 const threadViewMode = useThreadViewMode ( ) ;
@@ -577,6 +583,7 @@ export const ChannelPane = React.memo(function ChannelPane({
577583 data-testid = "channel-shared-header-backdrop"
578584 />
579585 ) : null }
586+
580587 { ! isSinglePanelView ? (
581588 < section
582589 aria-label = "Channel messages and composer"
@@ -781,9 +788,6 @@ export const ChannelPane = React.memo(function ChannelPane({
781788 }
782789 showTopBorder = { false }
783790 />
784- { /* The accessory is anchored in the dock's reserved bottom
785- rail, so fading it cannot change the observed overlay
786- height or move the conversation. */ }
787791 < ComposerActivityAccessory visible = { hasComposerBottomActivity } >
788792 < ChannelComposerActivityRow
789793 agents = { activityAgents }
@@ -802,6 +806,7 @@ export const ChannelPane = React.memo(function ChannelPane({
802806 ) : null }
803807 </ section >
804808 ) : null }
809+
805810 { /*
806811 * `AnimatePresence` keeps the focus thread drawer mounted through its exit
807812 * animation — without it the drawer's own existence condition
@@ -878,33 +883,27 @@ export const ChannelPane = React.memo(function ChannelPane({
878883 threadHeadMessage . id ,
879884 ) }
880885 threadReplyUnreadCounts = { threadReplyUnreadCounts }
881- activityAccessoryVisible = {
882- hasThreadComposerBotActivity || threadTypingPubkeys . length > 0
883- }
886+ activityAccessoryVisible = { hasThreadComposerActivity }
884887 activityAccessoryContent = {
885- hasThreadComposerBotActivity ||
886- threadTypingPubkeys . length > 0 ? (
888+ hasThreadComposerActivity ? (
887889 < BotActivityComposerAction
888890 agents = { activityAgents }
889891 channelId = { activeChannel ?. id ?? null }
890892 onOpenAgentSession = { onOpenAgentSession }
891893 profiles = { profiles }
894+ typingBotPubkeys = { threadPillBotPubkeys }
892895 typingIndicator = {
893- threadTypingPubkeys . length > 0 ? (
896+ combinedThreadTypingPubkeys . length > 0 ? (
894897 < TypingIndicatorRow
895898 channel = { activeChannel }
896- // The strip's slot owns spacing and the
897- // typing-only inset; zero the base paddings and
898- // let the row shrink so the lone-item slot can
899- // ellipsize the label.
900899 className = "min-w-0 shrink px-0 py-0 sm:px-0"
901900 currentPubkey = { currentPubkey }
902901 profiles = { profiles }
903- typingPubkeys = { threadTypingPubkeys }
902+ typingPubkeys = { combinedThreadTypingPubkeys }
904903 />
905904 ) : null
906905 }
907- workingBotPubkeys = { threadComposerBotTypingPubkeys }
906+ workingBotPubkeys = { threadPillBotPubkeys }
908907 />
909908 ) : null
910909 }
0 commit comments