Add button to start thread on reply (#2320)
authorFilipe Medeiros <hello@filipesm.eu>
Wed, 23 Jul 2025 15:17:17 +0000 (16:17 +0100)
committerGitHub <noreply@github.com>
Wed, 23 Jul 2025 15:17:17 +0000 (20:47 +0530)
* add simple button to start a thread on reply

* force build

* remove useless actions

* add actions back

* change icon to ThreadPlus

* add button to context menu

* fix capital T

---------

Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
src/app/features/room/RoomTimeline.tsx
src/app/features/room/message/Message.tsx

index 773e115badd81ae8f40a25305b323fbdf9a8e748..f2218b04fbaca1b18a0efe858f85bb87d312bd4b 100644 (file)
@@ -933,7 +933,7 @@ export function RoomTimeline({
   );
 
   const handleReplyClick: MouseEventHandler<HTMLButtonElement> = useCallback(
-    (evt) => {
+    (evt, startThread = false) => {
       const replyId = evt.currentTarget.getAttribute('data-event-id');
       if (!replyId) {
         console.warn('Button should have "data-event-id" attribute!');
@@ -944,7 +944,9 @@ export function RoomTimeline({
       const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet());
       const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent();
       const { body, formatted_body: formattedBody } = content;
-      const { 'm.relates_to': relation } = replyEvt.getWireContent();
+      const { 'm.relates_to': relation } = startThread
+        ? { 'm.relates_to': { rel_type: 'm.thread', event_id: replyId } }
+        : replyEvt.getWireContent();
       const senderId = replyEvt.getSender();
       if (senderId && typeof body === 'string') {
         setReplyDraft({
index b85605d5dd10f2be042084a18b0d27e323fa8a7d..c5de9ea189bd0a5665f2111478bcbbde3fe77082 100644 (file)
@@ -669,7 +669,10 @@ export type MessageProps = {
   messageSpacing: MessageSpacing;
   onUserClick: MouseEventHandler<HTMLButtonElement>;
   onUsernameClick: MouseEventHandler<HTMLButtonElement>;
-  onReplyClick: MouseEventHandler<HTMLButtonElement>;
+  onReplyClick: (
+    ev: Parameters<MouseEventHandler<HTMLButtonElement>>[0],
+    startThread?: boolean
+  ) => void;
   onEditId?: (eventId?: string) => void;
   onReactionToggle: (targetEventId: string, key: string, shortcode?: string) => void;
   reply?: ReactNode;
@@ -859,6 +862,8 @@ export const Message = as<'div', MessageProps>(
       }, 100);
     };
 
+    const isThreadedMessage = mEvent.threadRootId !== undefined;
+
     return (
       <MessageBase
         className={classNames(css.MessageBase, className)}
@@ -921,6 +926,17 @@ export const Message = as<'div', MessageProps>(
                 >
                   <Icon src={Icons.ReplyArrow} size="100" />
                 </IconButton>
+                {!isThreadedMessage && (
+                  <IconButton
+                    onClick={(ev) => onReplyClick(ev, true)}
+                    data-event-id={mEvent.getId()}
+                    variant="SurfaceVariant"
+                    size="300"
+                    radii="300"
+                  >
+                    <Icon src={Icons.ThreadPlus} size="100" />
+                  </IconButton>
+                )}
                 {canEditEvent(mx, mEvent) && onEditId && (
                   <IconButton
                     onClick={() => onEditId(mEvent.getId())}
@@ -1000,6 +1016,27 @@ export const Message = as<'div', MessageProps>(
                               Reply
                             </Text>
                           </MenuItem>
+                          {!isThreadedMessage && (
+                            <MenuItem
+                              size="300"
+                              after={<Icon src={Icons.ThreadPlus} size="100" />}
+                              radii="300"
+                              data-event-id={mEvent.getId()}
+                              onClick={(evt: any) => {
+                                onReplyClick(evt, true);
+                                closeMenu();
+                              }}
+                            >
+                              <Text
+                                className={css.MessageMenuItemText}
+                                as="span"
+                                size="T300"
+                                truncate
+                              >
+                                Reply in Thread
+                              </Text>
+                            </MenuItem>
+                          )}
                           {canEditEvent(mx, mEvent) && onEditId && (
                             <MenuItem
                               size="300"