mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	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,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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({
 | 
			
		||||
  marginBottom: toRem(1),
 | 
			
		||||
  minWidth: 0,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>(
 | 
			
		|||
    <Box
 | 
			
		||||
      className={classNames(css.Reply, className)}
 | 
			
		||||
      alignItems="Center"
 | 
			
		||||
      alignSelf="Start"
 | 
			
		||||
      gap="100"
 | 
			
		||||
      {...props}
 | 
			
		||||
      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 = {
 | 
			
		||||
  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<MatrixEvent | null | undefined>(
 | 
			
		||||
    timelineSet?.findEventById(eventId)
 | 
			
		||||
    timelineSet?.findEventById(replyEventId)
 | 
			
		||||
  );
 | 
			
		||||
  const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -62,7 +73,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 +89,43 @@ 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 (
 | 
			
		||||
    <ReplyLayout
 | 
			
		||||
      userColor={sender ? colorMXID(sender) : undefined}
 | 
			
		||||
      username={
 | 
			
		||||
        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%',
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
    <Box direction="Column" {...props} ref={ref}>
 | 
			
		||||
      {threadRootId && (
 | 
			
		||||
        <ThreadIndicator as="button" data-event-id={threadRootId} onClick={onClick} />
 | 
			
		||||
      )}
 | 
			
		||||
    </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');
 | 
			
		||||
    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}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
            )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}
 | 
			
		||||
                />
 | 
			
		||||
              )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<HTMLButtonElement> = (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 (
 | 
			
		||||
            <SequenceCard
 | 
			
		||||
| 
						 | 
				
			
			@ -452,11 +456,10 @@ function RoomNotificationsGroupComp({
 | 
			
		|||
                </Box>
 | 
			
		||||
                {replyEventId && (
 | 
			
		||||
                  <Reply
 | 
			
		||||
                    as="button"
 | 
			
		||||
                    mx={mx}
 | 
			
		||||
                    room={room}
 | 
			
		||||
                    eventId={replyEventId}
 | 
			
		||||
                    data-event-id={replyEventId}
 | 
			
		||||
                    replyEventId={replyEventId}
 | 
			
		||||
                    threadRootId={threadRootId}
 | 
			
		||||
                    onClick={handleOpenClick}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
  relation?: IEventRelation | undefined;
 | 
			
		||||
};
 | 
			
		||||
const createReplyDraftAtom = () => atom<IReplyDraft | undefined>(undefined);
 | 
			
		||||
export type TReplyDraftAtom = ReturnType<typeof createReplyDraftAtom>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.getContent();
 | 
			
		||||
  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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue