1- import { MouseEventHandler , useCallback , useMemo } from 'react' ;
1+ import { MouseEventHandler , useCallback , useEffect , useMemo , useState } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
33import { useAtomValue } from 'jotai' ;
44import {
5+ IThreadBundledRelationship ,
56 MatrixClient ,
67 MatrixEvent ,
8+ NotificationCountType ,
79 Room ,
10+ RoomEvent ,
11+ ThreadEvent ,
812 PushProcessor ,
913 EventTimelineSet ,
1014 IContent ,
@@ -47,6 +51,7 @@ import {
4751} from '$utils/room' ;
4852import { getLinkedTimelines , getLiveTimeline } from '$utils/timeline' ;
4953import * as customHtmlCss from '$styles/CustomHtml.css' ;
54+ import { UnreadBadge , UnreadBadgeCenter } from '$components/unread-badge' ;
5055import {
5156 EncryptedContent ,
5257 Event ,
@@ -105,20 +110,51 @@ function ThreadReplyChip({
105110 const useAuthentication = useMediaAuthentication ( ) ;
106111 const nicknames = useAtomValue ( nicknamesAtom ) ;
107112
113+ const [ counter , forceUpdate ] = useState ( 0 ) ;
114+
108115 const thread = room . getThread ( mEventId ) ;
109116
117+ useEffect ( ( ) => {
118+ if ( ! thread ) return ( ) => { } ;
119+ const onUpdate = ( ) => forceUpdate ( ( n ) => n + 1 ) ;
120+ thread . on ( ThreadEvent . NewReply as any , onUpdate ) ;
121+ thread . on ( ThreadEvent . Update as any , onUpdate ) ;
122+ room . on ( RoomEvent . Redaction as any , onUpdate ) ;
123+ room . on ( RoomEvent . UnreadNotifications as any , onUpdate ) ;
124+ return ( ) => {
125+ thread . off ( ThreadEvent . NewReply as any , onUpdate ) ;
126+ thread . off ( ThreadEvent . Update as any , onUpdate ) ;
127+ room . off ( RoomEvent . Redaction as any , onUpdate ) ;
128+ room . off ( RoomEvent . UnreadNotifications as any , onUpdate ) ;
129+ } ;
130+ } , [ room , thread ] ) ;
131+
110132 const replyEvents = useMemo ( ( ) => {
133+ // With threadSupport:true, reply events live in thread.timelineSet not the main room timeline.
134+ // Prefer thread.events when available so avatars and preview text are populated.
135+ if ( thread ) {
136+ const fromThread = thread . events . filter (
137+ ( ev ) => ev . getId ( ) !== mEventId && ! reactionOrEditEvent ( ev )
138+ ) ;
139+ if ( fromThread . length > 0 ) return fromThread ;
140+ }
111141 const linkedTimelines = getLinkedTimelines ( getLiveTimeline ( room ) ) ;
112142 return linkedTimelines
113143 . flatMap ( ( tl ) => tl . getEvents ( ) )
114144 . filter (
115145 ( ev ) => ev . threadRootId === mEventId && ev . getId ( ) !== mEventId && ! reactionOrEditEvent ( ev )
116146 ) ;
117- } , [ room , mEventId ] ) ;
147+ // eslint-disable-next-line react-hooks/exhaustive-deps -- counter is a cache-busting key, not used directly in body
148+ } , [ room , mEventId , thread , counter ] ) ;
118149
119150 if ( ! thread ) return null ;
120151
121- const replyCount = thread . length ?? 0 ;
152+ // Prefer the server-authoritative bundled count. thread.length only reflects
153+ // events fetched into the local timeline, which can be much lower than the
154+ // true total before the thread drawer is first opened and paginated.
155+ const bundledCount =
156+ thread . rootEvent ?. getServerAggregatedRelation < IThreadBundledRelationship > ( 'm.thread' ) ?. count ;
157+ const replyCount = bundledCount ?? thread . length ?? 0 ;
122158 if ( replyCount === 0 ) return null ;
123159
124160 const uniqueSenders : string [ ] = [ ] ;
@@ -146,6 +182,12 @@ function ThreadReplyChip({
146182
147183 const isOpen = openThreadId === mEventId ;
148184
185+ const unreadTotal = room . getThreadUnreadNotificationCount ( mEventId , NotificationCountType . Total ) ;
186+ const unreadHighlight = room . getThreadUnreadNotificationCount (
187+ mEventId ,
188+ NotificationCountType . Highlight
189+ ) ;
190+
149191 return (
150192 < Chip
151193 size = "400"
@@ -201,6 +243,11 @@ function ThreadReplyChip({
201243 · { latestSenderName } : { latestBody . slice ( 0 , 60 ) }
202244 </ Text >
203245 ) }
246+ { unreadTotal > 0 && (
247+ < UnreadBadgeCenter >
248+ < UnreadBadge highlight = { unreadHighlight > 0 } count = { unreadTotal } />
249+ </ UnreadBadgeCenter >
250+ ) }
204251 </ Chip >
205252 ) ;
206253}
@@ -226,6 +273,7 @@ export interface TimelineEventRendererOptions {
226273 hideMembershipEvents : boolean ;
227274 hideNickAvatarEvents : boolean ;
228275 showHiddenEvents : boolean ;
276+ hideThreadChip ?: boolean ;
229277 } ;
230278 state : {
231279 focusItem ?: { index : number ; highlight : boolean ; scrollTo : boolean } ;
@@ -280,6 +328,7 @@ export function useTimelineEventRenderer({
280328 hideMembershipEvents,
281329 hideNickAvatarEvents,
282330 showHiddenEvents,
331+ hideThreadChip,
283332 } ,
284333 state : { focusItem, editId, activeReplyId, openThreadId } ,
285334 permissions : { canRedact, canDeleteOwn, canSendReaction, canPinEvent } ,
@@ -309,9 +358,16 @@ export function useTimelineEventRenderer({
309358 isRedacted,
310359 getUnsigned,
311360 getTs,
312- replyEventId,
361+ getWireContent,
362+ replyEventId : rawReplyEventId ,
313363 threadRootId,
314364 } = mEvent ;
365+ // In the thread drawer (hideThreadChip=true), suppress reply headers for events
366+ // that only have m.in_reply_to as a non-thread-client fallback (is_falling_back: true).
367+ const replyEventId =
368+ hideThreadChip && getWireContent . call ( mEvent ) ?. [ 'm.relates_to' ] ?. is_falling_back
369+ ? undefined
370+ : rawReplyEventId ;
315371
316372 const reactionRelations = getEventReactions ( timelineSet , mEventId ) ;
317373 const reactions = reactionRelations ?. getSortedAnnotationsByKey ( ) ;
@@ -400,15 +456,15 @@ export function useTimelineEventRenderer({
400456 room = { room }
401457 timelineSet = { timelineSet }
402458 replyEventId = { replyEventId }
403- threadRootId = { threadRootId }
459+ threadRootId = { hideThreadChip ? undefined : threadRootId }
404460 mentions = { baseContent [ 'm.mentions' ] }
405461 onClick = { handleOpenReply }
406462 />
407463 )
408464 }
409465 reactions = { ( ( ) => {
410466 const threadChip =
411- room . getThread ( mEventId ) || threadRootId ? (
467+ ! hideThreadChip && ( room . getThread ( mEventId ) || threadRootId ) ? (
412468 < ThreadReplyChip
413469 room = { room }
414470 mEventId = { mEventId }
@@ -469,9 +525,14 @@ export function useTimelineEventRenderer({
469525 getContent : getEventContent ,
470526 getOriginalContent,
471527 getTs,
472- replyEventId,
528+ getWireContent,
529+ replyEventId : rawReplyEventId ,
473530 threadRootId,
474531 } = mEvent ;
532+ const replyEventId =
533+ hideThreadChip && getWireContent . call ( mEvent ) ?. [ 'm.relates_to' ] ?. is_falling_back
534+ ? undefined
535+ : rawReplyEventId ;
475536
476537 const reactionRelations = getEventReactions ( timelineSet , mEventId ) ;
477538 const reactions = reactionRelations ?. getSortedAnnotationsByKey ( ) ;
@@ -522,14 +583,14 @@ export function useTimelineEventRenderer({
522583 room = { room }
523584 timelineSet = { timelineSet }
524585 replyEventId = { replyEventId }
525- threadRootId = { threadRootId }
586+ threadRootId = { hideThreadChip ? undefined : threadRootId }
526587 onClick = { handleOpenReply }
527588 />
528589 )
529590 }
530591 reactions = { ( ( ) => {
531592 const threadChip =
532- room . getThread ( mEventId ) || threadRootId ? (
593+ ! hideThreadChip && ( room . getThread ( mEventId ) || threadRootId ) ? (
533594 < ThreadReplyChip
534595 room = { room }
535596 mEventId = { mEventId }
@@ -638,9 +699,14 @@ export function useTimelineEventRenderer({
638699 isRedacted,
639700 getUnsigned,
640701 getContent : getEventContent ,
641- replyEventId,
702+ getWireContent,
703+ replyEventId : rawReplyEventId ,
642704 threadRootId,
643705 } = mEvent ;
706+ const replyEventId =
707+ hideThreadChip && getWireContent . call ( mEvent ) ?. [ 'm.relates_to' ] ?. is_falling_back
708+ ? undefined
709+ : rawReplyEventId ;
644710
645711 const reactionRelations = getEventReactions ( timelineSet , mEventId ) ;
646712 const reactions = reactionRelations ?. getSortedAnnotationsByKey ( ) ;
@@ -683,15 +749,15 @@ export function useTimelineEventRenderer({
683749 room = { room }
684750 timelineSet = { timelineSet }
685751 replyEventId = { replyEventId }
686- threadRootId = { threadRootId }
752+ threadRootId = { hideThreadChip ? undefined : threadRootId }
687753 mentions = { content [ 'm.mentions' ] }
688754 onClick = { handleOpenReply }
689755 />
690756 )
691757 }
692758 reactions = { ( ( ) => {
693759 const threadChip =
694- room . getThread ( mEventId ) || threadRootId ? (
760+ ! hideThreadChip && ( room . getThread ( mEventId ) || threadRootId ) ? (
695761 < ThreadReplyChip
696762 room = { room }
697763 mEventId = { mEventId }
0 commit comments