Add basic m.thread support (#1349)

* Add basic `m.thread` support

* Fix types

* Update to v4

* Fix auto formatting mess

* Add threaded reply indicators

* Fix reply overflow

* Fix replying to edited threaded replies

* Add thread indicator to room input

* Fix editing encrypted events

* Use `toRem` function for converting units

---------

Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
This commit is contained in:
greentore 2024-08-15 16:52:32 +02:00 committed by GitHub
parent 7e7bee8f48
commit 830d05e217
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 140 additions and 85 deletions

View file

@ -148,7 +148,7 @@ export function SearchResultGroup({
}
);
const handleOpenClick: MouseEventHandler<HTMLButtonElement> = (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 (
<SequenceCard
@ -240,11 +241,10 @@ export function SearchResultGroup({
</Box>
{replyEventId && (
<Reply
as="button"
mx={mx}
room={room}
eventId={replyEventId}
data-event-id={replyEventId}
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenClick}
/>
)}

View file

@ -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 {
@ -106,7 +106,7 @@ import { CommandAutocomplete } from './CommandAutocomplete';
import { Command, SHRUG, useCommands } from '../../hooks/useCommands';
import { mobileOrTablet } from '../../utils/user-agent';
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
import { ReplyLayout } from '../../components/message';
import { ReplyLayout, ThreadIndicator } from '../../components/message';
import { roomToParentsAtom } from '../../state/room/roomToParents';
interface RoomInputProps {
@ -310,6 +310,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
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;
}
}
mx.sendMessage(roomId, content);
resetEditor(editor);
@ -489,22 +494,25 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
>
<Icon src={Icons.Cross} size="50" />
</IconButton>
<ReplyLayout
userColor={colorMXID(replyDraft.userId)}
username={
<Box direction="Column">
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
<ReplyLayout
userColor={colorMXID(replyDraft.userId)}
username={
<Text size="T300" truncate>
<b>
{getMemberDisplayName(room, replyDraft.userId) ??
getMxIdLocalPart(replyDraft.userId) ??
replyDraft.userId}
</b>
</Text>
}
>
<Text size="T300" truncate>
<b>
{getMemberDisplayName(room, replyDraft.userId) ??
getMxIdLocalPart(replyDraft.userId) ??
replyDraft.userId}
</b>
{trimReplyFromBody(replyDraft.body)}
</Text>
}
>
<Text size="T300" truncate>
{trimReplyFromBody(replyDraft.body)}
</Text>
</ReplyLayout>
</ReplyLayout>
</Box>
</Box>
</div>
)

View file

@ -16,6 +16,7 @@ import {
EventTimeline,
EventTimelineSet,
EventTimelineSetHandlerMap,
IContent,
IEncryptedFile,
MatrixClient,
MatrixEvent,
@ -837,13 +838,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
markAsRead(mx, room.roomId);
};
const handleOpenReply: MouseEventHandler<HTMLButtonElement> = 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 +859,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
});
} else {
setTimeline(getEmptyTimeline());
loadEventTimeline(replyId);
loadEventTimeline(targetId);
}
},
[room, timeline, scrollToItem, loadEventTimeline]
@ -909,8 +910,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<string, string> =
editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent();
const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent();
const { body, formatted_body: formattedBody } = content;
const { 'm.relates_to': relation } = replyEvt.getOriginalContent();
const senderId = replyEvt.getSender();
if (senderId && typeof body === 'string') {
setReplyDraft({
@ -918,6 +920,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
eventId: replyId,
body,
formattedBody,
relation,
});
setTimeout(() => ReactEditor.focus(editor), 100);
}
@ -969,7 +972,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);
@ -1004,12 +1007,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
reply={
replyEventId && (
<Reply
as="button"
mx={mx}
room={room}
timelineSet={timelineSet}
eventId={replyEventId}
data-reply-id={replyEventId}
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenReply}
/>
)
@ -1050,7 +1052,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 (
@ -1077,12 +1079,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
reply={
replyEventId && (
<Reply
as="button"
mx={mx}
room={room}
timelineSet={timelineSet}
eventId={replyEventId}
data-reply-id={replyEventId}
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenReply}
/>
)