From 4158d363586fd1f0f27a959268ec28e4e6e30adb Mon Sep 17 00:00:00 2001 From: greentore <117551249+greentore@users.noreply.github.com> Date: Mon, 24 Jul 2023 00:40:09 +0200 Subject: [PATCH 01/10] Add basic `m.thread` support --- src/app/molecules/message/Message.jsx | 2 +- src/app/organisms/room/RoomInput.tsx | 10 ++++++++-- src/app/state/roomInputDrafts.ts | 4 +++- src/client/action/navigation.js | 3 ++- src/client/state/navigation.js | 1 + 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/app/molecules/message/Message.jsx b/src/app/molecules/message/Message.jsx index 26a5b29da..5e7496d06 100644 --- a/src/app/molecules/message/Message.jsx +++ b/src/app/molecules/message/Message.jsx @@ -739,7 +739,7 @@ function Message({ setEdit(eventId); }, []); const reply = useCallback(() => { - replyTo(senderId, mEvent.getId(), body, customHTML); + replyTo(senderId, mEvent.getId(), body, customHTML, content["m.relates_to"] ); }, [body, customHTML]); if (msgType === 'm.emote') className.push('message--type-emote'); diff --git a/src/app/organisms/room/RoomInput.tsx b/src/app/organisms/room/RoomInput.tsx index a50c80047..2aa57c046 100644 --- a/src/app/organisms/room/RoomInput.tsx +++ b/src/app/organisms/room/RoomInput.tsx @@ -10,7 +10,7 @@ import React, { } from 'react'; import { useAtom } from 'jotai'; import isHotkey from 'is-hotkey'; -import { EventType, IContent, MsgType, Room } from 'matrix-js-sdk'; +import { EventType, IContent, IEventRelation, MsgType, Room } from 'matrix-js-sdk'; import { ReactEditor } from 'slate-react'; import { Transforms, Range, Editor, Element } from 'slate'; import { @@ -185,13 +185,15 @@ export const RoomInput = forwardRef( userId: string, eventId: string, body: string, - formattedBody: string + formattedBody: string | null, + relatesTo: IEventRelation | undefined, ) => { setReplyDraft({ userId, eventId, body, formattedBody, + relatesTo, }); ReactEditor.focus(editor); }; @@ -284,6 +286,10 @@ export const RoomInput = forwardRef( event_id: replyDraft.eventId, }, }; + if (replyDraft.relatesTo?.rel_type === "m.thread") { + content['m.relates_to'].event_id = replyDraft.relatesTo.event_id; + content['m.relates_to'].rel_type = "m.thread"; + } } mx.sendMessage(roomId, content); resetEditor(editor); diff --git a/src/app/state/roomInputDrafts.ts b/src/app/state/roomInputDrafts.ts index 2708b8bdf..c55343069 100644 --- a/src/app/state/roomInputDrafts.ts +++ b/src/app/state/roomInputDrafts.ts @@ -2,6 +2,7 @@ import { atom } from 'jotai'; import { atomFamily } from 'jotai/utils'; import { Descendant } from 'slate'; import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; +import { IEventRelation } from 'matrix-js-sdk'; import { TListAtom, createListAtom } from './list'; import { createUploadAtomFamily } from './upload'; import { TUploadContent } from '../utils/matrix'; @@ -39,7 +40,8 @@ export type IReplyDraft = { userId: string; eventId: string; body: string; - formattedBody?: string; + formattedBody: string | null; + relatesTo: IEventRelation | undefined; }; const createReplyDraftAtom = () => atom(undefined); export type TReplyDraftAtom = ReturnType; diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js index 4ee78a638..1a0217b87 100644 --- a/src/client/action/navigation.js +++ b/src/client/action/navigation.js @@ -139,13 +139,14 @@ export function openViewSource(event) { }); } -export function replyTo(userId, eventId, body, formattedBody) { +export function replyTo(userId, eventId, body, formattedBody, relatesTo) { appDispatcher.dispatch({ type: cons.actions.navigation.CLICK_REPLY_TO, userId, eventId, body, formattedBody, + relatesTo, }); } diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index 07231cd4b..dae9ec9a0 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -376,6 +376,7 @@ class Navigation extends EventEmitter { action.eventId, action.body, action.formattedBody, + action.relatesTo, ); }, [cons.actions.navigation.OPEN_SEARCH]: () => { From 93ba3b47532528c93220c83a344c193316783f1c Mon Sep 17 00:00:00 2001 From: greentore <117551249+greentore@users.noreply.github.com> Date: Wed, 2 Aug 2023 21:25:51 +0200 Subject: [PATCH 02/10] Fix types --- src/app/organisms/room/RoomInput.tsx | 4 ++-- src/app/state/roomInputDrafts.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/organisms/room/RoomInput.tsx b/src/app/organisms/room/RoomInput.tsx index 2aa57c046..62cefedc5 100644 --- a/src/app/organisms/room/RoomInput.tsx +++ b/src/app/organisms/room/RoomInput.tsx @@ -185,8 +185,8 @@ export const RoomInput = forwardRef( userId: string, eventId: string, body: string, - formattedBody: string | null, - relatesTo: IEventRelation | undefined, + formattedBody?: string | null, + relatesTo?: IEventRelation | undefined, ) => { setReplyDraft({ userId, diff --git a/src/app/state/roomInputDrafts.ts b/src/app/state/roomInputDrafts.ts index c55343069..ab7c4636c 100644 --- a/src/app/state/roomInputDrafts.ts +++ b/src/app/state/roomInputDrafts.ts @@ -40,8 +40,8 @@ export type IReplyDraft = { userId: string; eventId: string; body: string; - formattedBody: string | null; - relatesTo: IEventRelation | undefined; + formattedBody?: string | null; + relatesTo?: IEventRelation | undefined; }; const createReplyDraftAtom = () => atom(undefined); export type TReplyDraftAtom = ReturnType; From 7d97821d532f8a6dba61383b85d3fcacffab2fdd Mon Sep 17 00:00:00 2001 From: greentore <117551249+greentore@users.noreply.github.com> Date: Sat, 3 Aug 2024 20:02:48 +0200 Subject: [PATCH 03/10] Update to v4 --- src/app/features/room/RoomInput.tsx | 5 +++++ src/app/features/room/RoomTimeline.tsx | 7 +++++-- src/app/state/room/roomInputDrafts.ts | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 2728a54c8..97aea70a8 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -310,6 +310,11 @@ export const RoomInput = forwardRef( event_id: replyDraft.eventId, }, }; + if (replyDraft.relatesTo?.rel_type === 'm.thread') { + content['m.relates_to'].event_id = replyDraft.relatesTo.event_id; + content['m.relates_to'].rel_type = 'm.thread'; + content['m.relates_to'].is_falling_back = false; + } } mx.sendMessage(roomId, content); resetEditor(editor); diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 6e5037038..d7fc8602b 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -16,6 +16,7 @@ import { EventTimeline, EventTimelineSet, EventTimelineSetHandlerMap, + IContent, IEncryptedFile, MatrixClient, MatrixEvent, @@ -908,8 +909,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const replyEvt = room.findEventById(replyId); if (!replyEvt) return; const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet()); - const { body, formatted_body: formattedBody }: Record = - editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent(); + // TODO: replace with `RoomMessageEventContent` once matrix-js-sdk is updated. + const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent(); + const { body, formatted_body: formattedBody, 'm.relates_to': relatesTo } = content; const senderId = replyEvt.getSender(); if (senderId && typeof body === 'string') { setReplyDraft({ @@ -917,6 +919,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli eventId: replyId, body, formattedBody, + relatesTo, }); setTimeout(() => ReactEditor.focus(editor), 100); } diff --git a/src/app/state/room/roomInputDrafts.ts b/src/app/state/room/roomInputDrafts.ts index 60b42fdb7..79eb801db 100644 --- a/src/app/state/room/roomInputDrafts.ts +++ b/src/app/state/room/roomInputDrafts.ts @@ -2,6 +2,7 @@ import { atom } from 'jotai'; import { atomFamily } from 'jotai/utils'; import { Descendant } from 'slate'; import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; +import { IEventRelation } from 'matrix-js-sdk'; import { TListAtom, createListAtom } from '../list'; import { createUploadAtomFamily } from '../upload'; import { TUploadContent } from '../../utils/matrix'; @@ -39,7 +40,8 @@ export type IReplyDraft = { userId: string; eventId: string; body: string; - formattedBody?: string; + formattedBody?: string | undefined; + relatesTo?: IEventRelation | undefined; }; const createReplyDraftAtom = () => atom(undefined); export type TReplyDraftAtom = ReturnType; From 6c27a3a9afcd0ee7204b06148f792948f213e4ac Mon Sep 17 00:00:00 2001 From: greentore <117551249+greentore@users.noreply.github.com> Date: Sat, 3 Aug 2024 20:45:58 +0200 Subject: [PATCH 04/10] Fix auto formatting mess --- src/client/action/navigation.js | 7 ++++--- src/client/state/navigation.js | 32 ++++++++------------------------ 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js index 22eb715be..1967a463b 100644 --- a/src/client/action/navigation.js +++ b/src/client/action/navigation.js @@ -1,5 +1,5 @@ -import appDispatcher from "../dispatcher"; -import cons from "../state/cons"; +import appDispatcher from '../dispatcher'; +import cons from '../state/cons'; export function openSpaceSettings(roomId, tabText) { appDispatcher.dispatch({ @@ -21,10 +21,11 @@ export function toggleRoomSettings(roomId, tabText) { appDispatcher.dispatch({ type: cons.actions.navigation.TOGGLE_ROOM_SETTINGS, roomId, - tabText, + tabText }); } + export function openCreateRoom(isSpace = false, parentId = null) { appDispatcher.dispatch({ type: cons.actions.navigation.OPEN_CREATE_ROOM, diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index c30a812da..5f28f232f 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -1,6 +1,6 @@ -import EventEmitter from "events"; -import appDispatcher from "../dispatcher"; -import cons from "./cons"; +import EventEmitter from 'events'; +import appDispatcher from '../dispatcher'; +import cons from './cons'; class Navigation extends EventEmitter { constructor() { @@ -20,24 +20,16 @@ class Navigation extends EventEmitter { navigate(action) { const actions = { [cons.actions.navigation.OPEN_SPACE_SETTINGS]: () => { - this.emit( - cons.events.navigation.SPACE_SETTINGS_OPENED, - action.roomId, - action.tabText, - ); + this.emit(cons.events.navigation.SPACE_SETTINGS_OPENED, action.roomId, action.tabText); }, [cons.actions.navigation.OPEN_SPACE_ADDEXISTING]: () => { - this.emit( - cons.events.navigation.SPACE_ADDEXISTING_OPENED, - action.roomId, - action.spaces, - ); + this.emit(cons.events.navigation.SPACE_ADDEXISTING_OPENED, action.roomId, action.spaces); }, [cons.actions.navigation.TOGGLE_ROOM_SETTINGS]: () => { this.emit( cons.events.navigation.ROOM_SETTINGS_TOGGLED, action.roomId, - action.tabText, + action.tabText ); }, [cons.actions.navigation.OPEN_CREATE_ROOM]: () => { @@ -54,18 +46,10 @@ class Navigation extends EventEmitter { ); }, [cons.actions.navigation.OPEN_INVITE_USER]: () => { - this.emit( - cons.events.navigation.INVITE_USER_OPENED, - action.roomId, - action.searchTerm, - ); + this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm); }, [cons.actions.navigation.OPEN_PROFILE_VIEWER]: () => { - this.emit( - cons.events.navigation.PROFILE_VIEWER_OPENED, - action.userId, - action.roomId, - ); + this.emit(cons.events.navigation.PROFILE_VIEWER_OPENED, action.userId, action.roomId); }, [cons.actions.navigation.OPEN_SETTINGS]: () => { this.emit(cons.events.navigation.SETTINGS_OPENED, action.tabText); From 6f2122f560b9c7c83d0f5f54a9f9390bd9a7a2bd Mon Sep 17 00:00:00 2001 From: greentore <117551249+greentore@users.noreply.github.com> Date: Tue, 6 Aug 2024 06:38:58 +0200 Subject: [PATCH 05/10] Add threaded reply indicators --- src/app/components/message/Reply.css.ts | 17 ++++ src/app/components/message/Reply.tsx | 82 ++++++++++++------- .../message-search/SearchResultGroup.tsx | 16 ++-- src/app/features/room/RoomInput.tsx | 8 +- src/app/features/room/RoomTimeline.tsx | 30 ++++--- src/app/pages/client/inbox/Notifications.tsx | 13 +-- src/app/state/room/roomInputDrafts.ts | 2 +- src/app/utils/room.ts | 19 +++-- 8 files changed, 115 insertions(+), 72 deletions(-) diff --git a/src/app/components/message/Reply.css.ts b/src/app/components/message/Reply.css.ts index 014a2840a..b76c8d051 100644 --- a/src/app/components/message/Reply.css.ts +++ b/src/app/components/message/Reply.css.ts @@ -5,6 +5,23 @@ export const ReplyBend = style({ flexShrink: 0, }); +export const ThreadIndicator = style({ + opacity: config.opacity.P300, + gap: '0.125rem', + cursor: 'pointer', + + selectors: { + ':hover&': { + opacity: config.opacity.P500, + }, + }, +}); + +export const ThreadIndicatorIcon = style({ + width: '0.875rem', + height: '0.875rem', +}); + export const Reply = style({ marginBottom: toRem(1), minWidth: 0, diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 85383cdb5..84754ec62 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -1,7 +1,7 @@ import { Box, Icon, Icons, Text, as, color, toRem } from 'folds'; import { EventTimelineSet, MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk'; import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; -import React, { ReactNode, useEffect, useMemo, useState } from 'react'; +import React, { MouseEventHandler, ReactNode, useEffect, useMemo, useState } from 'react'; import to from 'await-to-js'; import classNames from 'classnames'; import colorMXID from '../../../util/colorMXID'; @@ -22,6 +22,7 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>( ( type ReplyProps = { mx: MatrixClient; room: Room; - timelineSet?: EventTimelineSet; - eventId: string; + timelineSet?: EventTimelineSet | undefined; + replyEventId: string; + threadRootId?: string | undefined; + onClick?: MouseEventHandler | undefined; }; -export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ...props }, ref) => { +export const Reply = as<'div', ReplyProps>((_, ref) => { + const { mx, room, timelineSet, replyEventId, threadRootId, onClick, ...props } = _; const [replyEvent, setReplyEvent] = useState( - timelineSet?.findEventById(eventId) + timelineSet?.findEventById(replyEventId) ); const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []); @@ -62,7 +66,7 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, .. useEffect(() => { let disposed = false; const loadEvent = async () => { - const [err, evt] = await to(mx.fetchRoomEvent(room.roomId, eventId)); + const [err, evt] = await to(mx.fetchRoomEvent(room.roomId, replyEventId)); const mEvent = new MatrixEvent(evt); if (disposed) return; if (err) { @@ -78,37 +82,53 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, .. return () => { disposed = true; }; - }, [replyEvent, mx, room, eventId]); + }, [replyEvent, mx, room, replyEventId]); const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted'; const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody; return ( - + {threadRootId && ( + + + Threaded reply + + )} + + {getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)} + + ) + } + data-event-id={replyEventId} + onClick={onClick} + > + {replyEvent !== undefined ? ( - {getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)} + {badEncryption ? : bodyJSX} - ) - } - {...props} - ref={ref} - > - {replyEvent !== undefined ? ( - - {badEncryption ? : bodyJSX} - - ) : ( - - )} - + ) : ( + + )} + + ); }); diff --git a/src/app/features/message-search/SearchResultGroup.tsx b/src/app/features/message-search/SearchResultGroup.tsx index 2b2a816a5..84ba3a763 100644 --- a/src/app/features/message-search/SearchResultGroup.tsx +++ b/src/app/features/message-search/SearchResultGroup.tsx @@ -148,7 +148,7 @@ export function SearchResultGroup({ } ); - const handleOpenClick: MouseEventHandler = (evt) => { + const handleOpenClick: MouseEventHandler = (evt) => { const eventId = evt.currentTarget.getAttribute('data-event-id'); if (!eventId) return; onOpen(room.roomId, eventId); @@ -183,15 +183,16 @@ export function SearchResultGroup({ event.sender; const senderAvatarMxc = getMemberAvatarMxc(room, event.sender); + const relation = event.content['m.relates_to']; const mainEventId = - event.content['m.relates_to']?.rel_type === RelationType.Replace - ? event.content['m.relates_to'].event_id - : event.event_id; + relation?.rel_type === RelationType.Replace ? relation.event_id : event.event_id; const getContent = (() => event.content['m.new_content'] ?? event.content) as GetContentCallback; - const replyEventId = event.content['m.relates_to']?.['m.in_reply_to']?.event_id; + const replyEventId = relation?.['m.in_reply_to']?.event_id; + const threadRootId = + relation?.rel_type === RelationType.Thread ? relation.event_id : undefined; return ( {replyEventId && ( )} diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 97aea70a8..4375a69b3 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -10,7 +10,7 @@ import React, { } from 'react'; import { useAtom, useAtomValue } from 'jotai'; import { isKeyHotkey } from 'is-hotkey'; -import { EventType, IContent, MsgType, Room } from 'matrix-js-sdk'; +import { EventType, IContent, MsgType, RelationType, Room } from 'matrix-js-sdk'; import { ReactEditor } from 'slate-react'; import { Transforms, Editor } from 'slate'; import { @@ -310,9 +310,9 @@ export const RoomInput = forwardRef( event_id: replyDraft.eventId, }, }; - if (replyDraft.relatesTo?.rel_type === 'm.thread') { - content['m.relates_to'].event_id = replyDraft.relatesTo.event_id; - content['m.relates_to'].rel_type = 'm.thread'; + 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; } } diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index d7fc8602b..8ecb6db51 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -837,13 +837,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli markAsRead(mx, room.roomId); }; - const handleOpenReply: MouseEventHandler = useCallback( + const handleOpenReply: MouseEventHandler = useCallback( async (evt) => { - const replyId = evt.currentTarget.getAttribute('data-reply-id'); - if (typeof replyId !== 'string') return; - const replyTimeline = getEventTimeline(room, replyId); + const targetId = evt.currentTarget.getAttribute('data-event-id'); + if (!targetId) return; + const replyTimeline = getEventTimeline(room, targetId); const absoluteIndex = - replyTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, replyTimeline, replyId); + replyTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, replyTimeline, targetId); if (typeof absoluteIndex === 'number') { scrollToItem(absoluteIndex, { @@ -858,7 +858,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli }); } else { setTimeline(getEmptyTimeline()); - loadEventTimeline(replyId); + loadEventTimeline(targetId); } }, [room, timeline, scrollToItem, loadEventTimeline] @@ -911,7 +911,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet()); // TODO: replace with `RoomMessageEventContent` once matrix-js-sdk is updated. const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent(); - const { body, formatted_body: formattedBody, 'm.relates_to': relatesTo } = content; + const { body, formatted_body: formattedBody, 'm.relates_to': relation } = content; const senderId = replyEvt.getSender(); if (senderId && typeof body === 'string') { setReplyDraft({ @@ -919,7 +919,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli eventId: replyId, body, formattedBody, - relatesTo, + relation, }); setTimeout(() => ReactEditor.focus(editor), 100); } @@ -970,7 +970,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 } = mEvent; + const { replyEventId, threadRootId } = mEvent; const highlighted = focusItem?.index === item && focusItem.highlight; const editedEvent = getEditedEvent(mEventId, mEvent, timelineSet); @@ -1005,12 +1005,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli reply={ replyEventId && ( ) @@ -1051,7 +1050,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 } = mEvent; + const { replyEventId, threadRootId } = mEvent; const highlighted = focusItem?.index === item && focusItem.highlight; return ( @@ -1078,12 +1077,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli reply={ replyEventId && ( ) diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx index 6a8160d86..aa8782161 100644 --- a/src/app/pages/client/inbox/Notifications.tsx +++ b/src/app/pages/client/inbox/Notifications.tsx @@ -20,6 +20,7 @@ import { IRoomEvent, JoinRule, Method, + RelationType, Room, } from 'matrix-js-sdk'; import { useVirtualizer } from '@tanstack/react-virtual'; @@ -352,7 +353,7 @@ function RoomNotificationsGroupComp({ } ); - const handleOpenClick: MouseEventHandler = (evt) => { + const handleOpenClick: MouseEventHandler = (evt) => { const eventId = evt.currentTarget.getAttribute('data-event-id'); if (!eventId) return; onOpen(room.roomId, eventId); @@ -403,7 +404,10 @@ function RoomNotificationsGroupComp({ const senderAvatarMxc = getMemberAvatarMxc(room, event.sender); const getContent = (() => event.content) as GetContentCallback; - const replyEventId = event.content['m.relates_to']?.['m.in_reply_to']?.event_id; + const relation = event.content['m.relates_to']; + const replyEventId = relation?.['m.in_reply_to']?.event_id; + const threadRootId = + relation?.rel_type === RelationType.Thread ? relation.event_id : undefined; return ( {replyEventId && ( )} diff --git a/src/app/state/room/roomInputDrafts.ts b/src/app/state/room/roomInputDrafts.ts index 79eb801db..33bd06076 100644 --- a/src/app/state/room/roomInputDrafts.ts +++ b/src/app/state/room/roomInputDrafts.ts @@ -41,7 +41,7 @@ export type IReplyDraft = { eventId: string; body: string; formattedBody?: string | undefined; - relatesTo?: IEventRelation | undefined; + relation?: IEventRelation | undefined; }; const createReplyDraftAtom = () => atom(undefined); export type TReplyDraftAtom = ReturnType; diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index 750dd6ca7..7bceb56d6 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -389,13 +389,18 @@ export const getEditedEvent = ( return edits && getLatestEdit(mEvent, edits.getRelations()); }; -export const canEditEvent = (mx: MatrixClient, mEvent: MatrixEvent) => - mEvent.getSender() === mx.getUserId() && - !mEvent.isRelation() && - mEvent.getType() === MessageEvent.RoomMessage && - (mEvent.getContent().msgtype === MsgType.Text || - mEvent.getContent().msgtype === MsgType.Emote || - mEvent.getContent().msgtype === MsgType.Notice); +export const canEditEvent = (mx: MatrixClient, mEvent: MatrixEvent) => { + const content = mEvent.getWireContent(); + const relationType = content['m.relates_to']?.rel_type; + return ( + mEvent.getSender() === mx.getUserId() && + (!relationType || relationType === RelationType.Thread) && + mEvent.getType() === MessageEvent.RoomMessage && + (content.msgtype === MsgType.Text || + content.msgtype === MsgType.Emote || + content.msgtype === MsgType.Notice) + ); +}; export const getLatestEditableEvt = ( timeline: EventTimeline, From 3e8f692d60d5cadc649c13a3f69138644b9ebf1a Mon Sep 17 00:00:00 2001 From: greentore <117551249+greentore@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:16:21 +0200 Subject: [PATCH 06/10] Fix reply overflow --- src/app/components/message/Reply.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 84754ec62..2a6301474 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -88,7 +88,7 @@ export const Reply = as<'div', ReplyProps>((_, ref) => { const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody; return ( - + {threadRootId && ( Date: Thu, 8 Aug 2024 04:13:46 +0200 Subject: [PATCH 07/10] Fix replying to edited threaded replies --- src/app/features/room/RoomTimeline.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 8ecb6db51..a2d3ccd9c 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -909,9 +909,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const replyEvt = room.findEventById(replyId); if (!replyEvt) return; const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet()); - // TODO: replace with `RoomMessageEventContent` once matrix-js-sdk is updated. const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent(); - const { body, formatted_body: formattedBody, 'm.relates_to': relation } = content; + const { body, formatted_body: formattedBody } = content; + const { 'm.relates_to': relation } = replyEvt.getOriginalContent(); const senderId = replyEvt.getSender(); if (senderId && typeof body === 'string') { setReplyDraft({ From eac682fdfab5f03cb3a511ef5981be95f2673557 Mon Sep 17 00:00:00 2001 From: greentore <117551249+greentore@users.noreply.github.com> Date: Thu, 8 Aug 2024 04:15:06 +0200 Subject: [PATCH 08/10] Add thread indicator to room input --- src/app/components/message/Reply.css.ts | 4 ++- src/app/components/message/Reply.tsx | 19 ++++++-------- src/app/features/room/RoomInput.tsx | 33 ++++++++++++++----------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/app/components/message/Reply.css.ts b/src/app/components/message/Reply.css.ts index b76c8d051..96b49274c 100644 --- a/src/app/components/message/Reply.css.ts +++ b/src/app/components/message/Reply.css.ts @@ -8,9 +8,11 @@ export const ReplyBend = style({ export const ThreadIndicator = style({ opacity: config.opacity.P300, gap: '0.125rem', - cursor: 'pointer', selectors: { + 'button&': { + cursor: 'pointer', + }, ':hover&': { opacity: config.opacity.P500, }, diff --git a/src/app/components/message/Reply.tsx b/src/app/components/message/Reply.tsx index 2a6301474..82a9d9198 100644 --- a/src/app/components/message/Reply.tsx +++ b/src/app/components/message/Reply.tsx @@ -38,6 +38,13 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>( ) ); +export const ThreadIndicator = as<'div'>(({ ...props }, ref) => ( + + + Threaded reply + +)); + type ReplyProps = { mx: MatrixClient; room: Room; @@ -90,17 +97,7 @@ export const Reply = as<'div', ReplyProps>((_, ref) => { return ( {threadRootId && ( - - - Threaded reply - + )} ( > - + {replyDraft.relation?.rel_type === RelationType.Thread && } + + + {getMemberDisplayName(room, replyDraft.userId) ?? + getMxIdLocalPart(replyDraft.userId) ?? + replyDraft.userId} + + + } + > - - {getMemberDisplayName(room, replyDraft.userId) ?? - getMxIdLocalPart(replyDraft.userId) ?? - replyDraft.userId} - + {trimReplyFromBody(replyDraft.body)} - } - > - - {trimReplyFromBody(replyDraft.body)} - - + + ) From e9a35d09db1f094456501ddb3dccafcf24aaf998 Mon Sep 17 00:00:00 2001 From: greentore <117551249+greentore@users.noreply.github.com> Date: Thu, 8 Aug 2024 04:19:00 +0200 Subject: [PATCH 09/10] Fix editing encrypted events --- src/app/utils/room.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index 7bceb56d6..8cf33a8ff 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -390,7 +390,7 @@ export const getEditedEvent = ( }; export const canEditEvent = (mx: MatrixClient, mEvent: MatrixEvent) => { - const content = mEvent.getWireContent(); + const content = mEvent.getContent(); const relationType = content['m.relates_to']?.rel_type; return ( mEvent.getSender() === mx.getUserId() && From f8b1107513b222994125ae91228bd1e8b0617db1 Mon Sep 17 00:00:00 2001 From: greentore <117551249+greentore@users.noreply.github.com> Date: Wed, 14 Aug 2024 19:36:20 +0200 Subject: [PATCH 10/10] Use `toRem` function for converting units --- src/app/components/message/Reply.css.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/message/Reply.css.ts b/src/app/components/message/Reply.css.ts index 96b49274c..067993914 100644 --- a/src/app/components/message/Reply.css.ts +++ b/src/app/components/message/Reply.css.ts @@ -7,7 +7,7 @@ export const ReplyBend = style({ export const ThreadIndicator = style({ opacity: config.opacity.P300, - gap: '0.125rem', + gap: toRem(2), selectors: { 'button&': { @@ -20,8 +20,8 @@ export const ThreadIndicator = style({ }); export const ThreadIndicatorIcon = style({ - width: '0.875rem', - height: '0.875rem', + width: toRem(14), + height: toRem(14), }); export const Reply = style({