@@ -14,6 +14,7 @@ import { isKeyHotkey } from 'is-hotkey';
1414import {
1515 EventType ,
1616 IContent ,
17+ MatrixEvent ,
1718 MsgType ,
1819 RelationType ,
1920 Room ,
@@ -114,6 +115,7 @@ import { settingsAtom } from '$state/settings';
114115import {
115116 getMemberDisplayName ,
116117 getMentionContent ,
118+ reactionOrEditEvent ,
117119 trimReplyFromBody ,
118120 trimReplyFromFormattedBody ,
119121} from '$utils/room' ;
@@ -160,7 +162,33 @@ import {
160162import { CommandAutocomplete } from './CommandAutocomplete' ;
161163import { AudioMessageRecorder } from './AudioMessageRecorder' ;
162164
163- const getReplyContent = ( replyDraft : IReplyDraft | undefined ) : IEventRelation => {
165+ // Returns the event ID of the most recent non-reaction/non-edit event in a thread,
166+ // falling back to the thread root if no replies exist yet.
167+ const getLatestThreadEventId = ( room : Room , threadRootId : string ) : string => {
168+ const thread = room . getThread ( threadRootId ) ;
169+ const threadEvents : MatrixEvent [ ] = thread ?. events ?? [ ] ;
170+ const filtered = threadEvents . filter (
171+ ( ev ) => ev . getId ( ) !== threadRootId && ! reactionOrEditEvent ( ev )
172+ ) ;
173+ if ( filtered . length > 0 ) {
174+ return filtered [ filtered . length - 1 ] . getId ( ) ?? threadRootId ;
175+ }
176+ // Fall back to the live timeline if the Thread object hasn't been registered yet
177+ const liveEvents = room
178+ . getUnfilteredTimelineSet ( )
179+ . getLiveTimeline ( )
180+ . getEvents ( )
181+ . filter (
182+ ( ev ) =>
183+ ev . threadRootId === threadRootId && ev . getId ( ) !== threadRootId && ! reactionOrEditEvent ( ev )
184+ ) ;
185+ if ( liveEvents . length > 0 ) {
186+ return liveEvents [ liveEvents . length - 1 ] . getId ( ) ?? threadRootId ;
187+ }
188+ return threadRootId ;
189+ } ;
190+
191+ const getReplyContent = ( replyDraft : IReplyDraft | undefined , room ?: Room ) : IEventRelation => {
164192 if ( ! replyDraft ) return { } ;
165193
166194 const relatesTo : IEventRelation = { } ;
@@ -173,13 +201,19 @@ const getReplyContent = (replyDraft: IReplyDraft | undefined): IEventRelation =>
173201 // Check if this is a reply to a specific message in the thread
174202 // (replyDraft.body being empty means it's just a seeded thread draft)
175203 if ( replyDraft . body && replyDraft . eventId !== replyDraft . relation . event_id ) {
176- // This is a reply to a message within the thread
204+ // Explicit reply to a specific message — per spec, is_falling_back must be false
177205 relatesTo [ 'm.in_reply_to' ] = {
178206 event_id : replyDraft . eventId ,
179207 } ;
180208 relatesTo . is_falling_back = false ;
181209 } else {
182- // This is just a regular thread message
210+ // Regular thread message — per spec, include fallback m.in_reply_to pointing to the
211+ // most recent thread message so unthreaded clients can display it as a reply chain
212+ const threadRootId = replyDraft . relation . event_id ?? replyDraft . eventId ;
213+ const latestEventId = room ? getLatestThreadEventId ( room , threadRootId ) : threadRootId ;
214+ relatesTo [ 'm.in_reply_to' ] = {
215+ event_id : latestEventId ,
216+ } ;
183217 relatesTo . is_falling_back = true ;
184218 }
185219 } else {
@@ -461,7 +495,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
461495 const contents = fulfilledPromiseSettledResult ( await Promise . allSettled ( contentsPromises ) ) ;
462496
463497 if ( contents . length > 0 ) {
464- const replyContent = plainText ?. length === 0 ? getReplyContent ( replyDraft ) : undefined ;
498+ const replyContent =
499+ plainText ?. length === 0 ? getReplyContent ( replyDraft , room ) : undefined ;
465500 if ( replyContent ) contents [ 0 ] [ 'm.relates_to' ] = replyContent ;
466501 if ( threadRootId ) {
467502 setReplyDraft ( {
@@ -605,7 +640,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
605640 content . formatted_body = formattedBody ;
606641 }
607642 if ( replyDraft ) {
608- content [ 'm.relates_to' ] = getReplyContent ( replyDraft ) ;
643+ content [ 'm.relates_to' ] = getReplyContent ( replyDraft , room ) ;
609644 }
610645 const invalidate = ( ) =>
611646 queryClient . invalidateQueries ( { queryKey : [ 'delayedEvents' , roomId ] } ) ;
@@ -792,7 +827,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
792827 info,
793828 } ;
794829 if ( replyDraft ) {
795- content [ 'm.relates_to' ] = getReplyContent ( replyDraft ) ;
830+ content [ 'm.relates_to' ] = getReplyContent ( replyDraft , room ) ;
796831 if ( threadRootId ) {
797832 setReplyDraft ( {
798833 userId : mx . getUserId ( ) ?? '' ,
0 commit comments