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:
parent
7e7bee8f48
commit
830d05e217
8 changed files with 140 additions and 85 deletions
|
@ -5,6 +5,25 @@ export const ReplyBend = style({
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ThreadIndicator = style({
|
||||||
|
opacity: config.opacity.P300,
|
||||||
|
gap: toRem(2),
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'button&': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
':hover&': {
|
||||||
|
opacity: config.opacity.P500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ThreadIndicatorIcon = style({
|
||||||
|
width: toRem(14),
|
||||||
|
height: toRem(14),
|
||||||
|
});
|
||||||
|
|
||||||
export const Reply = style({
|
export const Reply = style({
|
||||||
marginBottom: toRem(1),
|
marginBottom: toRem(1),
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Box, Icon, Icons, Text, as, color, toRem } from 'folds';
|
import { Box, Icon, Icons, Text, as, color, toRem } from 'folds';
|
||||||
import { EventTimelineSet, MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
|
import { EventTimelineSet, MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk';
|
||||||
import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend';
|
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 to from 'await-to-js';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
@ -22,6 +22,7 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>(
|
||||||
<Box
|
<Box
|
||||||
className={classNames(css.Reply, className)}
|
className={classNames(css.Reply, className)}
|
||||||
alignItems="Center"
|
alignItems="Center"
|
||||||
|
alignSelf="Start"
|
||||||
gap="100"
|
gap="100"
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@ -37,16 +38,26 @@ export const ReplyLayout = as<'div', ReplyLayoutProps>(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ThreadIndicator = as<'div'>(({ ...props }, ref) => (
|
||||||
|
<Box className={css.ThreadIndicator} alignItems="Center" alignSelf="Start" {...props} ref={ref}>
|
||||||
|
<Icon className={css.ThreadIndicatorIcon} src={Icons.Message} />
|
||||||
|
<Text size="T200">Threaded reply</Text>
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
|
|
||||||
type ReplyProps = {
|
type ReplyProps = {
|
||||||
mx: MatrixClient;
|
mx: MatrixClient;
|
||||||
room: Room;
|
room: Room;
|
||||||
timelineSet?: EventTimelineSet;
|
timelineSet?: EventTimelineSet | undefined;
|
||||||
eventId: string;
|
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<MatrixEvent | null | undefined>(
|
const [replyEvent, setReplyEvent] = useState<MatrixEvent | null | undefined>(
|
||||||
timelineSet?.findEventById(eventId)
|
timelineSet?.findEventById(replyEventId)
|
||||||
);
|
);
|
||||||
const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []);
|
const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []);
|
||||||
|
|
||||||
|
@ -62,7 +73,7 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
const loadEvent = async () => {
|
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);
|
const mEvent = new MatrixEvent(evt);
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -78,37 +89,43 @@ export const Reply = as<'div', ReplyProps>(({ mx, room, timelineSet, eventId, ..
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
};
|
};
|
||||||
}, [replyEvent, mx, room, eventId]);
|
}, [replyEvent, mx, room, replyEventId]);
|
||||||
|
|
||||||
const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted';
|
const badEncryption = replyEvent?.getContent().msgtype === 'm.bad.encrypted';
|
||||||
const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody;
|
const bodyJSX = body ? scaleSystemEmoji(trimReplyFromBody(body)) : fallbackBody;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReplyLayout
|
<Box direction="Column" {...props} ref={ref}>
|
||||||
userColor={sender ? colorMXID(sender) : undefined}
|
{threadRootId && (
|
||||||
username={
|
<ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} />
|
||||||
sender && (
|
|
||||||
<Text size="T300" truncate>
|
|
||||||
<b>{getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)}</b>
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{replyEvent !== undefined ? (
|
|
||||||
<Text size="T300" truncate>
|
|
||||||
{badEncryption ? <MessageBadEncryptedContent /> : bodyJSX}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<LinePlaceholder
|
|
||||||
style={{
|
|
||||||
backgroundColor: color.SurfaceVariant.ContainerActive,
|
|
||||||
maxWidth: toRem(placeholderWidth),
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</ReplyLayout>
|
<ReplyLayout
|
||||||
|
as="button"
|
||||||
|
userColor={sender ? colorMXID(sender) : undefined}
|
||||||
|
username={
|
||||||
|
sender && (
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
<b>{getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender)}</b>
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
data-event-id={replyEventId}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{replyEvent !== undefined ? (
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
{badEncryption ? <MessageBadEncryptedContent /> : bodyJSX}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<LinePlaceholder
|
||||||
|
style={{
|
||||||
|
backgroundColor: color.SurfaceVariant.ContainerActive,
|
||||||
|
maxWidth: toRem(placeholderWidth),
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ReplyLayout>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -148,7 +148,7 @@ export function SearchResultGroup({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
const handleOpenClick: MouseEventHandler = (evt) => {
|
||||||
const eventId = evt.currentTarget.getAttribute('data-event-id');
|
const eventId = evt.currentTarget.getAttribute('data-event-id');
|
||||||
if (!eventId) return;
|
if (!eventId) return;
|
||||||
onOpen(room.roomId, eventId);
|
onOpen(room.roomId, eventId);
|
||||||
|
@ -183,15 +183,16 @@ export function SearchResultGroup({
|
||||||
event.sender;
|
event.sender;
|
||||||
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
|
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
|
||||||
|
|
||||||
|
const relation = event.content['m.relates_to'];
|
||||||
const mainEventId =
|
const mainEventId =
|
||||||
event.content['m.relates_to']?.rel_type === RelationType.Replace
|
relation?.rel_type === RelationType.Replace ? relation.event_id : event.event_id;
|
||||||
? event.content['m.relates_to'].event_id
|
|
||||||
: event.event_id;
|
|
||||||
|
|
||||||
const getContent = (() =>
|
const getContent = (() =>
|
||||||
event.content['m.new_content'] ?? event.content) as GetContentCallback;
|
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 (
|
return (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
|
@ -240,11 +241,10 @@ export function SearchResultGroup({
|
||||||
</Box>
|
</Box>
|
||||||
{replyEventId && (
|
{replyEventId && (
|
||||||
<Reply
|
<Reply
|
||||||
as="button"
|
|
||||||
mx={mx}
|
mx={mx}
|
||||||
room={room}
|
room={room}
|
||||||
eventId={replyEventId}
|
replyEventId={replyEventId}
|
||||||
data-event-id={replyEventId}
|
threadRootId={threadRootId}
|
||||||
onClick={handleOpenClick}
|
onClick={handleOpenClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import React, {
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
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 { ReactEditor } from 'slate-react';
|
||||||
import { Transforms, Editor } from 'slate';
|
import { Transforms, Editor } from 'slate';
|
||||||
import {
|
import {
|
||||||
|
@ -106,7 +106,7 @@ import { CommandAutocomplete } from './CommandAutocomplete';
|
||||||
import { Command, SHRUG, useCommands } from '../../hooks/useCommands';
|
import { Command, SHRUG, useCommands } from '../../hooks/useCommands';
|
||||||
import { mobileOrTablet } from '../../utils/user-agent';
|
import { mobileOrTablet } from '../../utils/user-agent';
|
||||||
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
|
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
|
||||||
import { ReplyLayout } from '../../components/message';
|
import { ReplyLayout, ThreadIndicator } from '../../components/message';
|
||||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||||
|
|
||||||
interface RoomInputProps {
|
interface RoomInputProps {
|
||||||
|
@ -310,6 +310,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
event_id: replyDraft.eventId,
|
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);
|
mx.sendMessage(roomId, content);
|
||||||
resetEditor(editor);
|
resetEditor(editor);
|
||||||
|
@ -489,22 +494,25 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
>
|
>
|
||||||
<Icon src={Icons.Cross} size="50" />
|
<Icon src={Icons.Cross} size="50" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<ReplyLayout
|
<Box direction="Column">
|
||||||
userColor={colorMXID(replyDraft.userId)}
|
{replyDraft.relation?.rel_type === RelationType.Thread && <ThreadIndicator />}
|
||||||
username={
|
<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>
|
<Text size="T300" truncate>
|
||||||
<b>
|
{trimReplyFromBody(replyDraft.body)}
|
||||||
{getMemberDisplayName(room, replyDraft.userId) ??
|
|
||||||
getMxIdLocalPart(replyDraft.userId) ??
|
|
||||||
replyDraft.userId}
|
|
||||||
</b>
|
|
||||||
</Text>
|
</Text>
|
||||||
}
|
</ReplyLayout>
|
||||||
>
|
</Box>
|
||||||
<Text size="T300" truncate>
|
|
||||||
{trimReplyFromBody(replyDraft.body)}
|
|
||||||
</Text>
|
|
||||||
</ReplyLayout>
|
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
EventTimeline,
|
EventTimeline,
|
||||||
EventTimelineSet,
|
EventTimelineSet,
|
||||||
EventTimelineSetHandlerMap,
|
EventTimelineSetHandlerMap,
|
||||||
|
IContent,
|
||||||
IEncryptedFile,
|
IEncryptedFile,
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
MatrixEvent,
|
MatrixEvent,
|
||||||
|
@ -837,13 +838,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
markAsRead(mx, room.roomId);
|
markAsRead(mx, room.roomId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenReply: MouseEventHandler<HTMLButtonElement> = useCallback(
|
const handleOpenReply: MouseEventHandler = useCallback(
|
||||||
async (evt) => {
|
async (evt) => {
|
||||||
const replyId = evt.currentTarget.getAttribute('data-reply-id');
|
const targetId = evt.currentTarget.getAttribute('data-event-id');
|
||||||
if (typeof replyId !== 'string') return;
|
if (!targetId) return;
|
||||||
const replyTimeline = getEventTimeline(room, replyId);
|
const replyTimeline = getEventTimeline(room, targetId);
|
||||||
const absoluteIndex =
|
const absoluteIndex =
|
||||||
replyTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, replyTimeline, replyId);
|
replyTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, replyTimeline, targetId);
|
||||||
|
|
||||||
if (typeof absoluteIndex === 'number') {
|
if (typeof absoluteIndex === 'number') {
|
||||||
scrollToItem(absoluteIndex, {
|
scrollToItem(absoluteIndex, {
|
||||||
|
@ -858,7 +859,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setTimeline(getEmptyTimeline());
|
setTimeline(getEmptyTimeline());
|
||||||
loadEventTimeline(replyId);
|
loadEventTimeline(targetId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[room, timeline, scrollToItem, loadEventTimeline]
|
[room, timeline, scrollToItem, loadEventTimeline]
|
||||||
|
@ -909,8 +910,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
const replyEvt = room.findEventById(replyId);
|
const replyEvt = room.findEventById(replyId);
|
||||||
if (!replyEvt) return;
|
if (!replyEvt) return;
|
||||||
const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet());
|
const editedReply = getEditedEvent(replyId, replyEvt, room.getUnfilteredTimelineSet());
|
||||||
const { body, formatted_body: formattedBody }: Record<string, string> =
|
const content: IContent = editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent();
|
||||||
editedReply?.getContent()['m.new_content'] ?? replyEvt.getContent();
|
const { body, formatted_body: formattedBody } = content;
|
||||||
|
const { 'm.relates_to': relation } = replyEvt.getOriginalContent();
|
||||||
const senderId = replyEvt.getSender();
|
const senderId = replyEvt.getSender();
|
||||||
if (senderId && typeof body === 'string') {
|
if (senderId && typeof body === 'string') {
|
||||||
setReplyDraft({
|
setReplyDraft({
|
||||||
|
@ -918,6 +920,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
eventId: replyId,
|
eventId: replyId,
|
||||||
body,
|
body,
|
||||||
formattedBody,
|
formattedBody,
|
||||||
|
relation,
|
||||||
});
|
});
|
||||||
setTimeout(() => ReactEditor.focus(editor), 100);
|
setTimeout(() => ReactEditor.focus(editor), 100);
|
||||||
}
|
}
|
||||||
|
@ -969,7 +972,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
const reactionRelations = getEventReactions(timelineSet, mEventId);
|
const reactionRelations = getEventReactions(timelineSet, mEventId);
|
||||||
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
|
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
|
||||||
const hasReactions = reactions && reactions.length > 0;
|
const hasReactions = reactions && reactions.length > 0;
|
||||||
const { replyEventId } = mEvent;
|
const { replyEventId, threadRootId } = mEvent;
|
||||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||||
|
|
||||||
const editedEvent = getEditedEvent(mEventId, mEvent, timelineSet);
|
const editedEvent = getEditedEvent(mEventId, mEvent, timelineSet);
|
||||||
|
@ -1004,12 +1007,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
reply={
|
reply={
|
||||||
replyEventId && (
|
replyEventId && (
|
||||||
<Reply
|
<Reply
|
||||||
as="button"
|
|
||||||
mx={mx}
|
mx={mx}
|
||||||
room={room}
|
room={room}
|
||||||
timelineSet={timelineSet}
|
timelineSet={timelineSet}
|
||||||
eventId={replyEventId}
|
replyEventId={replyEventId}
|
||||||
data-reply-id={replyEventId}
|
threadRootId={threadRootId}
|
||||||
onClick={handleOpenReply}
|
onClick={handleOpenReply}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -1050,7 +1052,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
const reactionRelations = getEventReactions(timelineSet, mEventId);
|
const reactionRelations = getEventReactions(timelineSet, mEventId);
|
||||||
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
|
const reactions = reactionRelations && reactionRelations.getSortedAnnotationsByKey();
|
||||||
const hasReactions = reactions && reactions.length > 0;
|
const hasReactions = reactions && reactions.length > 0;
|
||||||
const { replyEventId } = mEvent;
|
const { replyEventId, threadRootId } = mEvent;
|
||||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1077,12 +1079,11 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
reply={
|
reply={
|
||||||
replyEventId && (
|
replyEventId && (
|
||||||
<Reply
|
<Reply
|
||||||
as="button"
|
|
||||||
mx={mx}
|
mx={mx}
|
||||||
room={room}
|
room={room}
|
||||||
timelineSet={timelineSet}
|
timelineSet={timelineSet}
|
||||||
eventId={replyEventId}
|
replyEventId={replyEventId}
|
||||||
data-reply-id={replyEventId}
|
threadRootId={threadRootId}
|
||||||
onClick={handleOpenReply}
|
onClick={handleOpenReply}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
IRoomEvent,
|
IRoomEvent,
|
||||||
JoinRule,
|
JoinRule,
|
||||||
Method,
|
Method,
|
||||||
|
RelationType,
|
||||||
Room,
|
Room,
|
||||||
} from 'matrix-js-sdk';
|
} from 'matrix-js-sdk';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
|
@ -352,7 +353,7 @@ function RoomNotificationsGroupComp({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
const handleOpenClick: MouseEventHandler = (evt) => {
|
||||||
const eventId = evt.currentTarget.getAttribute('data-event-id');
|
const eventId = evt.currentTarget.getAttribute('data-event-id');
|
||||||
if (!eventId) return;
|
if (!eventId) return;
|
||||||
onOpen(room.roomId, eventId);
|
onOpen(room.roomId, eventId);
|
||||||
|
@ -403,7 +404,10 @@ function RoomNotificationsGroupComp({
|
||||||
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
|
const senderAvatarMxc = getMemberAvatarMxc(room, event.sender);
|
||||||
const getContent = (() => event.content) as GetContentCallback;
|
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 (
|
return (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
|
@ -452,11 +456,10 @@ function RoomNotificationsGroupComp({
|
||||||
</Box>
|
</Box>
|
||||||
{replyEventId && (
|
{replyEventId && (
|
||||||
<Reply
|
<Reply
|
||||||
as="button"
|
|
||||||
mx={mx}
|
mx={mx}
|
||||||
room={room}
|
room={room}
|
||||||
eventId={replyEventId}
|
replyEventId={replyEventId}
|
||||||
data-event-id={replyEventId}
|
threadRootId={threadRootId}
|
||||||
onClick={handleOpenClick}
|
onClick={handleOpenClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { atom } from 'jotai';
|
||||||
import { atomFamily } from 'jotai/utils';
|
import { atomFamily } from 'jotai/utils';
|
||||||
import { Descendant } from 'slate';
|
import { Descendant } from 'slate';
|
||||||
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
|
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
|
||||||
|
import { IEventRelation } from 'matrix-js-sdk';
|
||||||
import { TListAtom, createListAtom } from '../list';
|
import { TListAtom, createListAtom } from '../list';
|
||||||
import { createUploadAtomFamily } from '../upload';
|
import { createUploadAtomFamily } from '../upload';
|
||||||
import { TUploadContent } from '../../utils/matrix';
|
import { TUploadContent } from '../../utils/matrix';
|
||||||
|
@ -39,7 +40,8 @@ export type IReplyDraft = {
|
||||||
userId: string;
|
userId: string;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
body: string;
|
body: string;
|
||||||
formattedBody?: string;
|
formattedBody?: string | undefined;
|
||||||
|
relation?: IEventRelation | undefined;
|
||||||
};
|
};
|
||||||
const createReplyDraftAtom = () => atom<IReplyDraft | undefined>(undefined);
|
const createReplyDraftAtom = () => atom<IReplyDraft | undefined>(undefined);
|
||||||
export type TReplyDraftAtom = ReturnType<typeof createReplyDraftAtom>;
|
export type TReplyDraftAtom = ReturnType<typeof createReplyDraftAtom>;
|
||||||
|
|
|
@ -389,13 +389,18 @@ export const getEditedEvent = (
|
||||||
return edits && getLatestEdit(mEvent, edits.getRelations());
|
return edits && getLatestEdit(mEvent, edits.getRelations());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canEditEvent = (mx: MatrixClient, mEvent: MatrixEvent) =>
|
export const canEditEvent = (mx: MatrixClient, mEvent: MatrixEvent) => {
|
||||||
mEvent.getSender() === mx.getUserId() &&
|
const content = mEvent.getContent();
|
||||||
!mEvent.isRelation() &&
|
const relationType = content['m.relates_to']?.rel_type;
|
||||||
mEvent.getType() === MessageEvent.RoomMessage &&
|
return (
|
||||||
(mEvent.getContent().msgtype === MsgType.Text ||
|
mEvent.getSender() === mx.getUserId() &&
|
||||||
mEvent.getContent().msgtype === MsgType.Emote ||
|
(!relationType || relationType === RelationType.Thread) &&
|
||||||
mEvent.getContent().msgtype === MsgType.Notice);
|
mEvent.getType() === MessageEvent.RoomMessage &&
|
||||||
|
(content.msgtype === MsgType.Text ||
|
||||||
|
content.msgtype === MsgType.Emote ||
|
||||||
|
content.msgtype === MsgType.Notice)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getLatestEditableEvt = (
|
export const getLatestEditableEvt = (
|
||||||
timeline: EventTimeline,
|
timeline: EventTimeline,
|
||||||
|
|
Loading…
Reference in a new issue