From 486ca4556b12ae886dbffccd26a66d4c3b5cdca1 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Fri, 27 Feb 2026 08:34:46 +0200 Subject: [PATCH 1/8] basic image with reply --- src/app/features/room/RoomInput.tsx | 2 +- src/app/features/room/msgContent.ts | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index ae46d2d098..215dea1cec 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -281,7 +281,7 @@ export const RoomInput = forwardRef( if (!fileItem) throw new Error('Broken upload'); if (fileItem.file.type.startsWith('image')) { - return getImageMsgContent(mx, fileItem, upload.mxc); + return getImageMsgContent(mx, fileItem, upload.mxc, replyDraft); } if (fileItem.file.type.startsWith('video')) { return getVideoMsgContent(mx, fileItem, upload.mxc); diff --git a/src/app/features/room/msgContent.ts b/src/app/features/room/msgContent.ts index 5b7cd14527..da9d696f93 100644 --- a/src/app/features/room/msgContent.ts +++ b/src/app/features/room/msgContent.ts @@ -1,4 +1,4 @@ -import { IContent, MatrixClient, MsgType } from 'matrix-js-sdk'; +import { IContent, MatrixClient, RelationType, MsgType } from 'matrix-js-sdk'; import to from 'await-to-js'; import { IThumbnailContent, @@ -46,7 +46,8 @@ const generateThumbnailContent = async ( export const getImageMsgContent = async ( mx: MatrixClient, item: TUploadItem, - mxc: string + mxc: string, + replyDraft?: any ): Promise => { const { file, originalFile, encInfo, metadata } = item; const [imgError, imgEl] = await to(loadImageElement(getImageFileUrl(originalFile))); @@ -58,6 +59,19 @@ export const getImageMsgContent = async ( body: file.name, [MATRIX_SPOILER_PROPERTY_NAME]: metadata.markedAsSpoiler, }; + + if (replyDraft) { + content['m.relates_to'] = { + 'm.in_reply_to': { + event_id: replyDraft.eventId, + }, + }; + if (replyDraft.relation?.rel_type === RelationType.Thread) { + content['m.relates_to'].event_id = replyDraft.relation.event_id; + content['m.relates_to'].rel_type = RelationType.Thread; + content['m.relates_to'].is_falling_back = false; + } + } if (imgEl) { const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height)); From 9fb07fbf5865758cf32be090852b54cd9e3d8a82 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Fri, 27 Feb 2026 09:20:33 +0200 Subject: [PATCH 2/8] separated the reply code into a function and assigned it to all file types --- src/app/features/room/RoomInput.tsx | 6 ++--- src/app/features/room/msgContent.ts | 39 ++++++++++++++++++----------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 215dea1cec..2db6020f5e 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -284,12 +284,12 @@ export const RoomInput = forwardRef( return getImageMsgContent(mx, fileItem, upload.mxc, replyDraft); } if (fileItem.file.type.startsWith('video')) { - return getVideoMsgContent(mx, fileItem, upload.mxc); + return getVideoMsgContent(mx, fileItem, upload.mxc, replyDraft); } if (fileItem.file.type.startsWith('audio')) { - return getAudioMsgContent(fileItem, upload.mxc); + return getAudioMsgContent(fileItem, upload.mxc, replyDraft); } - return getFileMsgContent(fileItem, upload.mxc); + return getFileMsgContent(fileItem, upload.mxc, replyDraft); }); handleCancelUpload(uploads); const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises)); diff --git a/src/app/features/room/msgContent.ts b/src/app/features/room/msgContent.ts index da9d696f93..ff30a1c147 100644 --- a/src/app/features/room/msgContent.ts +++ b/src/app/features/room/msgContent.ts @@ -43,6 +43,21 @@ const generateThumbnailContent = async ( return thumbnailContent; }; +export const getReplyEvent = (replyDraft: any): any => { + const relatesTo: IContent = {}; + + relatesTo['m.in_reply_to'] = { + event_id: replyDraft.eventId, + }; + + if (replyDraft.relation?.rel_type === RelationType.Thread) { + relatesTo.event_id = replyDraft.relation.event_id; + relatesTo.rel_type = RelationType.Thread; + relatesTo.is_falling_back = false; + } + return relatesTo; +}; + export const getImageMsgContent = async ( mx: MatrixClient, item: TUploadItem, @@ -60,18 +75,8 @@ export const getImageMsgContent = async ( [MATRIX_SPOILER_PROPERTY_NAME]: metadata.markedAsSpoiler, }; - if (replyDraft) { - content['m.relates_to'] = { - 'm.in_reply_to': { - event_id: replyDraft.eventId, - }, - }; - if (replyDraft.relation?.rel_type === RelationType.Thread) { - content['m.relates_to'].event_id = replyDraft.relation.event_id; - content['m.relates_to'].rel_type = RelationType.Thread; - content['m.relates_to'].is_falling_back = false; - } - } + if (replyDraft) content['m.relates_to'] = getReplyEvent(replyDraft); + if (imgEl) { const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height)); @@ -94,7 +99,8 @@ export const getImageMsgContent = async ( export const getVideoMsgContent = async ( mx: MatrixClient, item: TUploadItem, - mxc: string + mxc: string, + replyDraft?: any ): Promise => { const { file, originalFile, encInfo, metadata } = item; @@ -107,6 +113,7 @@ export const getVideoMsgContent = async ( body: file.name, [MATRIX_SPOILER_PROPERTY_NAME]: metadata.markedAsSpoiler, }; + if (replyDraft) content['m.relates_to'] = getReplyEvent(replyDraft); if (videoEl) { const [thumbError, thumbContent] = await to( generateThumbnailContent( @@ -140,7 +147,7 @@ export const getVideoMsgContent = async ( return content; }; -export const getAudioMsgContent = (item: TUploadItem, mxc: string): IContent => { +export const getAudioMsgContent = (item: TUploadItem, mxc: string, replyDraft?: any): IContent => { const { file, encInfo } = item; const content: IContent = { msgtype: MsgType.Audio, @@ -151,6 +158,7 @@ export const getAudioMsgContent = (item: TUploadItem, mxc: string): IContent => size: file.size, }, }; + if (replyDraft) content['m.relates_to'] = getReplyEvent(replyDraft); if (encInfo) { content.file = { ...encInfo, @@ -162,7 +170,7 @@ export const getAudioMsgContent = (item: TUploadItem, mxc: string): IContent => return content; }; -export const getFileMsgContent = (item: TUploadItem, mxc: string): IContent => { +export const getFileMsgContent = (item: TUploadItem, mxc: string, replyDraft?: any): IContent => { const { file, encInfo } = item; const content: IContent = { msgtype: MsgType.File, @@ -173,6 +181,7 @@ export const getFileMsgContent = (item: TUploadItem, mxc: string): IContent => { size: file.size, }, }; + if (replyDraft) content['m.relates_to'] = getReplyEvent(replyDraft); if (encInfo) { content.file = { ...encInfo, From d291d67e754587b884d866504b76816f09f9c8c2 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Fri, 27 Feb 2026 09:44:54 +0200 Subject: [PATCH 3/8] cleared the reply flag after sending a reply file so it doesnt prompt you to reply to it again through text --- src/app/features/room/RoomInput.tsx | 10 ++++++---- src/app/features/room/msgContent.ts | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 2db6020f5e..f279ba6231 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -277,19 +277,21 @@ export const RoomInput = forwardRef( const handleSendUpload = async (uploads: UploadSuccess[]) => { const contentsPromises = uploads.map(async (upload) => { + const replyDraftCont = replyDraft; + setReplyDraft(undefined); const fileItem = selectedFiles.find((f) => f.file === upload.file); if (!fileItem) throw new Error('Broken upload'); if (fileItem.file.type.startsWith('image')) { - return getImageMsgContent(mx, fileItem, upload.mxc, replyDraft); + return getImageMsgContent(mx, fileItem, upload.mxc, replyDraftCont); } if (fileItem.file.type.startsWith('video')) { - return getVideoMsgContent(mx, fileItem, upload.mxc, replyDraft); + return getVideoMsgContent(mx, fileItem, upload.mxc, replyDraftCont); } if (fileItem.file.type.startsWith('audio')) { - return getAudioMsgContent(fileItem, upload.mxc, replyDraft); + return getAudioMsgContent(fileItem, upload.mxc, replyDraftCont); } - return getFileMsgContent(fileItem, upload.mxc, replyDraft); + return getFileMsgContent(fileItem, upload.mxc, replyDraftCont); }); handleCancelUpload(uploads); const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises)); diff --git a/src/app/features/room/msgContent.ts b/src/app/features/room/msgContent.ts index ff30a1c147..44cb32ce98 100644 --- a/src/app/features/room/msgContent.ts +++ b/src/app/features/room/msgContent.ts @@ -43,7 +43,7 @@ const generateThumbnailContent = async ( return thumbnailContent; }; -export const getReplyEvent = (replyDraft: any): any => { +export const getReplyEvent = (replyDraft: IContent): IEventRelation => { const relatesTo: IContent = {}; relatesTo['m.in_reply_to'] = { @@ -62,7 +62,7 @@ export const getImageMsgContent = async ( mx: MatrixClient, item: TUploadItem, mxc: string, - replyDraft?: any + replyDraft?: IContent ): Promise => { const { file, originalFile, encInfo, metadata } = item; const [imgError, imgEl] = await to(loadImageElement(getImageFileUrl(originalFile))); @@ -100,7 +100,7 @@ export const getVideoMsgContent = async ( mx: MatrixClient, item: TUploadItem, mxc: string, - replyDraft?: any + replyDraft?: IContent ): Promise => { const { file, originalFile, encInfo, metadata } = item; @@ -147,7 +147,11 @@ export const getVideoMsgContent = async ( return content; }; -export const getAudioMsgContent = (item: TUploadItem, mxc: string, replyDraft?: any): IContent => { +export const getAudioMsgContent = ( + item: TUploadItem, + mxc: string, + replyDraft?: IContent +): IContent => { const { file, encInfo } = item; const content: IContent = { msgtype: MsgType.Audio, @@ -170,7 +174,11 @@ export const getAudioMsgContent = (item: TUploadItem, mxc: string, replyDraft?: return content; }; -export const getFileMsgContent = (item: TUploadItem, mxc: string, replyDraft?: any): IContent => { +export const getFileMsgContent = ( + item: TUploadItem, + mxc: string, + replyDraft?: IContent +): IContent => { const { file, encInfo } = item; const content: IContent = { msgtype: MsgType.File, From 0f5de225f9a824122fe7d328e05a7e546ba24bee Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Sat, 28 Feb 2026 10:05:48 +0200 Subject: [PATCH 4/8] prioritized text over images for keeping the reply tag and made it unique to the first item sent --- src/app/features/room/RoomInput.tsx | 21 +++++++++++++-------- src/app/features/room/msgContent.ts | 6 ++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index f279ba6231..fb41cbae55 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -9,7 +9,7 @@ import React, { } from 'react'; import { useAtom, useAtomValue } from 'jotai'; import { isKeyHotkey } from 'is-hotkey'; -import { EventType, IContent, MsgType, RelationType, Room } from 'matrix-js-sdk'; +import { EventType, IContent, IEventRelation, MsgType, RelationType, Room } from 'matrix-js-sdk'; import { ReactEditor } from 'slate-react'; import { Transforms, Editor } from 'slate'; import { @@ -68,6 +68,7 @@ import { useFilePicker } from '../../hooks/useFilePicker'; import { useFilePasteHandler } from '../../hooks/useFilePasteHandler'; import { useFileDropZone } from '../../hooks/useFileDrop'; import { + IReplyDraft, TUploadItem, TUploadMetadata, roomIdToMsgDraftAtomFamily, @@ -276,22 +277,26 @@ export const RoomInput = forwardRef( }; const handleSendUpload = async (uploads: UploadSuccess[]) => { - const contentsPromises = uploads.map(async (upload) => { - const replyDraftCont = replyDraft; - setReplyDraft(undefined); + const plaintext = toPlainText(editor.children, isMarkdown).trim(); + const replyDraftBase = plaintext.length === 0 ? replyDraft : undefined; + setReplyDraft(undefined); + + const contentsPromises = uploads.map(async (upload, index) => { + const replyDraftContent = index === 0 ? replyDraftBase : undefined; + const fileItem = selectedFiles.find((f) => f.file === upload.file); if (!fileItem) throw new Error('Broken upload'); if (fileItem.file.type.startsWith('image')) { - return getImageMsgContent(mx, fileItem, upload.mxc, replyDraftCont); + return getImageMsgContent(mx, fileItem, upload.mxc, replyDraftContent); } if (fileItem.file.type.startsWith('video')) { - return getVideoMsgContent(mx, fileItem, upload.mxc, replyDraftCont); + return getVideoMsgContent(mx, fileItem, upload.mxc, replyDraftContent); } if (fileItem.file.type.startsWith('audio')) { - return getAudioMsgContent(fileItem, upload.mxc, replyDraftCont); + return getAudioMsgContent(fileItem, upload.mxc, replyDraftContent); } - return getFileMsgContent(fileItem, upload.mxc, replyDraftCont); + return getFileMsgContent(fileItem, upload.mxc, replyDraftContent); }); handleCancelUpload(uploads); const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises)); diff --git a/src/app/features/room/msgContent.ts b/src/app/features/room/msgContent.ts index 44cb32ce98..df951e821b 100644 --- a/src/app/features/room/msgContent.ts +++ b/src/app/features/room/msgContent.ts @@ -1,4 +1,4 @@ -import { IContent, MatrixClient, RelationType, MsgType } from 'matrix-js-sdk'; +import { IContent, MatrixClient, RelationType, MsgType, IEventRelation } from 'matrix-js-sdk'; import to from 'await-to-js'; import { IThumbnailContent, @@ -44,7 +44,9 @@ const generateThumbnailContent = async ( }; export const getReplyEvent = (replyDraft: IContent): IEventRelation => { - const relatesTo: IContent = {}; + if (!replyDraft) return {}; + + const relatesTo: IEventRelation = {}; relatesTo['m.in_reply_to'] = { event_id: replyDraft.eventId, From f86585404505216e96f9cc96b8c31450ef76da92 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Sat, 28 Feb 2026 11:24:07 +0200 Subject: [PATCH 5/8] refactored the reply flow to remove duplicated effort --- src/app/features/room/RoomInput.tsx | 54 +++++++++++++++++------------ src/app/features/room/msgContent.ts | 42 +++------------------- 2 files changed, 36 insertions(+), 60 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index fb41cbae55..cec8e45910 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -119,6 +119,23 @@ import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag'; import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; import { useComposingCheck } from '../../hooks/useComposingCheck'; +const getReplyContent = (replyDraft: IReplyDraft | undefined): IEventRelation => { + if (!replyDraft) return {}; + + const relatesTo: IEventRelation = {}; + + relatesTo['m.in_reply_to'] = { + event_id: replyDraft.eventId, + }; + + if (replyDraft.relation?.rel_type === RelationType.Thread) { + relatesTo.event_id = replyDraft.relation.event_id; + relatesTo.rel_type = RelationType.Thread; + relatesTo.is_falling_back = false; + } + return relatesTo; +}; + interface RoomInputProps { editor: Editor; fileDropContainerRef: RefObject; @@ -277,29 +294,31 @@ export const RoomInput = forwardRef( }; const handleSendUpload = async (uploads: UploadSuccess[]) => { - const plaintext = toPlainText(editor.children, isMarkdown).trim(); - const replyDraftBase = plaintext.length === 0 ? replyDraft : undefined; - setReplyDraft(undefined); - - const contentsPromises = uploads.map(async (upload, index) => { - const replyDraftContent = index === 0 ? replyDraftBase : undefined; - + const contentsPromises = uploads.map(async (upload) => { const fileItem = selectedFiles.find((f) => f.file === upload.file); if (!fileItem) throw new Error('Broken upload'); if (fileItem.file.type.startsWith('image')) { - return getImageMsgContent(mx, fileItem, upload.mxc, replyDraftContent); + return getImageMsgContent(mx, fileItem, upload.mxc); } if (fileItem.file.type.startsWith('video')) { - return getVideoMsgContent(mx, fileItem, upload.mxc, replyDraftContent); + return getVideoMsgContent(mx, fileItem, upload.mxc); } if (fileItem.file.type.startsWith('audio')) { - return getAudioMsgContent(fileItem, upload.mxc, replyDraftContent); + return getAudioMsgContent(fileItem, upload.mxc); } - return getFileMsgContent(fileItem, upload.mxc, replyDraftContent); + return getFileMsgContent(fileItem, upload.mxc); }); handleCancelUpload(uploads); const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises)); + + if (contents.length > 0) { + const plaintext = toPlainText(editor.children, isMarkdown).trim(); + const replyContent = plaintext.length === 0 ? getReplyContent(replyDraft) : undefined; + if (replyContent) contents[0]['m.relates_to'] = replyContent; + setReplyDraft(undefined); + } + contents.forEach((content) => mx.sendMessage(roomId, content as any)); }; @@ -367,18 +386,7 @@ export const RoomInput = forwardRef( content.format = 'org.matrix.custom.html'; content.formatted_body = formattedBody; } - if (replyDraft) { - content['m.relates_to'] = { - 'm.in_reply_to': { - event_id: replyDraft.eventId, - }, - }; - if (replyDraft.relation?.rel_type === RelationType.Thread) { - content['m.relates_to'].event_id = replyDraft.relation.event_id; - content['m.relates_to'].rel_type = RelationType.Thread; - content['m.relates_to'].is_falling_back = false; - } - } + if (replyDraft) content['m.relates_to'] = getReplyContent(replyDraft); mx.sendMessage(roomId, content as any); resetEditor(editor); resetEditorHistory(editor); diff --git a/src/app/features/room/msgContent.ts b/src/app/features/room/msgContent.ts index df951e821b..d70ecc3005 100644 --- a/src/app/features/room/msgContent.ts +++ b/src/app/features/room/msgContent.ts @@ -1,4 +1,4 @@ -import { IContent, MatrixClient, RelationType, MsgType, IEventRelation } from 'matrix-js-sdk'; +import { IContent, MatrixClient, MsgType } from 'matrix-js-sdk'; import to from 'await-to-js'; import { IThumbnailContent, @@ -43,28 +43,10 @@ const generateThumbnailContent = async ( return thumbnailContent; }; -export const getReplyEvent = (replyDraft: IContent): IEventRelation => { - if (!replyDraft) return {}; - - const relatesTo: IEventRelation = {}; - - relatesTo['m.in_reply_to'] = { - event_id: replyDraft.eventId, - }; - - if (replyDraft.relation?.rel_type === RelationType.Thread) { - relatesTo.event_id = replyDraft.relation.event_id; - relatesTo.rel_type = RelationType.Thread; - relatesTo.is_falling_back = false; - } - return relatesTo; -}; - export const getImageMsgContent = async ( mx: MatrixClient, item: TUploadItem, - mxc: string, - replyDraft?: IContent + mxc: string ): Promise => { const { file, originalFile, encInfo, metadata } = item; const [imgError, imgEl] = await to(loadImageElement(getImageFileUrl(originalFile))); @@ -77,8 +59,6 @@ export const getImageMsgContent = async ( [MATRIX_SPOILER_PROPERTY_NAME]: metadata.markedAsSpoiler, }; - if (replyDraft) content['m.relates_to'] = getReplyEvent(replyDraft); - if (imgEl) { const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height)); @@ -101,8 +81,7 @@ export const getImageMsgContent = async ( export const getVideoMsgContent = async ( mx: MatrixClient, item: TUploadItem, - mxc: string, - replyDraft?: IContent + mxc: string ): Promise => { const { file, originalFile, encInfo, metadata } = item; @@ -115,7 +94,6 @@ export const getVideoMsgContent = async ( body: file.name, [MATRIX_SPOILER_PROPERTY_NAME]: metadata.markedAsSpoiler, }; - if (replyDraft) content['m.relates_to'] = getReplyEvent(replyDraft); if (videoEl) { const [thumbError, thumbContent] = await to( generateThumbnailContent( @@ -149,11 +127,7 @@ export const getVideoMsgContent = async ( return content; }; -export const getAudioMsgContent = ( - item: TUploadItem, - mxc: string, - replyDraft?: IContent -): IContent => { +export const getAudioMsgContent = (item: TUploadItem, mxc: string): IContent => { const { file, encInfo } = item; const content: IContent = { msgtype: MsgType.Audio, @@ -164,7 +138,6 @@ export const getAudioMsgContent = ( size: file.size, }, }; - if (replyDraft) content['m.relates_to'] = getReplyEvent(replyDraft); if (encInfo) { content.file = { ...encInfo, @@ -176,11 +149,7 @@ export const getAudioMsgContent = ( return content; }; -export const getFileMsgContent = ( - item: TUploadItem, - mxc: string, - replyDraft?: IContent -): IContent => { +export const getFileMsgContent = (item: TUploadItem, mxc: string): IContent => { const { file, encInfo } = item; const content: IContent = { msgtype: MsgType.File, @@ -191,7 +160,6 @@ export const getFileMsgContent = ( size: file.size, }, }; - if (replyDraft) content['m.relates_to'] = getReplyEvent(replyDraft); if (encInfo) { content.file = { ...encInfo, From 193e77e779e08f8469680c212d75480344c2fc7b Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Sat, 28 Feb 2026 12:53:09 +0200 Subject: [PATCH 6/8] added sticker replies and sticker reply display logic --- src/app/features/room/RoomInput.tsx | 10 ++++++++-- src/app/features/room/RoomTimeline.tsx | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index cec8e45910..186ed830cc 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -118,6 +118,7 @@ import { useTheme } from '../../hooks/useTheme'; import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag'; import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; import { useComposingCheck } from '../../hooks/useComposingCheck'; +import { StickerEventContent } from 'matrix-js-sdk/lib/types'; const getReplyContent = (replyDraft: IReplyDraft | undefined): IEventRelation => { if (!replyDraft) return {}; @@ -454,11 +455,16 @@ export const RoomInput = forwardRef( await getImageUrlBlob(stickerUrl) ); - mx.sendEvent(roomId, EventType.Sticker, { + const content: StickerEventContent = { body: label, url: mxc, info, - }); + }; + if (replyDraft) { + content['m.relates_to'] = getReplyContent(replyDraft); + setReplyDraft(undefined); + } + mx.sendEvent(roomId, EventType.Sticker, content); }; return ( diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index d1678b65b3..fa5f22c5c3 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -1235,6 +1235,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const reactionRelations = getEventReactions(timelineSet, mEventId); const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey(); const hasReactions = reactions && reactions.length > 0; + const { replyEventId, threadRootId } = mEvent; const highlighted = focusItem?.index === item && focusItem.highlight; return ( @@ -1257,6 +1258,20 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli onUsernameClick={handleUsernameClick} onReplyClick={handleReplyClick} onReactionToggle={handleReactionToggle} + reply={ + replyEventId && ( + + ) + } reactions={ reactionRelations && ( Date: Sat, 28 Feb 2026 16:33:16 +0200 Subject: [PATCH 7/8] fixed all new warnings caused by my code --- src/app/features/room/RoomInput.tsx | 7 +++++-- src/app/features/room/msgContent.ts | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 186ed830cc..8214340f80 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -29,6 +29,7 @@ import { toRem, } from 'folds'; +import { StickerEventContent } from 'matrix-js-sdk/lib/types'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { CustomEditor, @@ -118,7 +119,6 @@ import { useTheme } from '../../hooks/useTheme'; import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag'; import { usePowerLevelTags } from '../../hooks/usePowerLevelTags'; import { useComposingCheck } from '../../hooks/useComposingCheck'; -import { StickerEventContent } from 'matrix-js-sdk/lib/types'; const getReplyContent = (replyDraft: IReplyDraft | undefined): IEventRelation => { if (!replyDraft) return {}; @@ -136,6 +136,9 @@ const getReplyContent = (replyDraft: IReplyDraft | undefined): IEventRelation => } return relatesTo; }; +interface ReplyEventContent { + 'm.relates_to'?: IEventRelation; +} interface RoomInputProps { editor: Editor; @@ -455,7 +458,7 @@ export const RoomInput = forwardRef( await getImageUrlBlob(stickerUrl) ); - const content: StickerEventContent = { + const content: StickerEventContent & ReplyEventContent = { body: label, url: mxc, info, diff --git a/src/app/features/room/msgContent.ts b/src/app/features/room/msgContent.ts index d70ecc3005..5b7cd14527 100644 --- a/src/app/features/room/msgContent.ts +++ b/src/app/features/room/msgContent.ts @@ -58,7 +58,6 @@ export const getImageMsgContent = async ( body: file.name, [MATRIX_SPOILER_PROPERTY_NAME]: metadata.markedAsSpoiler, }; - if (imgEl) { const blurHash = encodeBlurHash(imgEl, 512, scaleYDimension(imgEl.width, 512, imgEl.height)); From a475415ce8c789a66c1f4f5324855c13d03f4e95 Mon Sep 17 00:00:00 2001 From: Shea Duma Date: Thu, 26 Mar 2026 00:42:53 +0200 Subject: [PATCH 8/8] small change to beat race condition --- src/app/features/room/RoomInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 8214340f80..698ab6d847 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -298,6 +298,7 @@ export const RoomInput = forwardRef( }; const handleSendUpload = async (uploads: UploadSuccess[]) => { + const plaintext = toPlainText(editor.children, isMarkdown).trim(); const contentsPromises = uploads.map(async (upload) => { const fileItem = selectedFiles.find((f) => f.file === upload.file); if (!fileItem) throw new Error('Broken upload'); @@ -317,7 +318,6 @@ export const RoomInput = forwardRef( const contents = fulfilledPromiseSettledResult(await Promise.allSettled(contentsPromises)); if (contents.length > 0) { - const plaintext = toPlainText(editor.children, isMarkdown).trim(); const replyContent = plaintext.length === 0 ? getReplyContent(replyDraft) : undefined; if (replyContent) contents[0]['m.relates_to'] = replyContent; setReplyDraft(undefined);