2021-08-04 12:52:59 +03:00
|
|
|
/* eslint-disable react/prop-types */
|
|
|
|
import React, { useState, useEffect, useLayoutEffect } from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
2021-08-31 16:13:31 +03:00
|
|
|
import './RoomViewContent.scss';
|
2021-08-04 12:52:59 +03:00
|
|
|
|
|
|
|
import dateFormat from 'dateformat';
|
|
|
|
|
|
|
|
import initMatrix from '../../../client/initMatrix';
|
|
|
|
import cons from '../../../client/state/cons';
|
2021-08-15 11:29:09 +03:00
|
|
|
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
2021-08-25 11:36:13 +03:00
|
|
|
import { getUsername, getUsernameOfRoomMember, doesRoomHaveUnread } from '../../../util/matrixUtil';
|
2021-08-04 12:52:59 +03:00
|
|
|
import colorMXID from '../../../util/colorMXID';
|
2021-09-09 16:06:39 +03:00
|
|
|
import { diffMinutes, isNotInSameDay, getEventCords } from '../../../util/common';
|
2021-08-16 15:21:23 +03:00
|
|
|
import { openEmojiBoard, openReadReceipts } from '../../../client/action/navigation';
|
2021-08-04 12:52:59 +03:00
|
|
|
|
|
|
|
import Divider from '../../atoms/divider/Divider';
|
2021-08-10 14:28:44 +03:00
|
|
|
import Avatar from '../../atoms/avatar/Avatar';
|
2021-08-11 10:59:01 +03:00
|
|
|
import IconButton from '../../atoms/button/IconButton';
|
2021-08-20 16:42:57 +03:00
|
|
|
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
|
2021-08-10 14:28:44 +03:00
|
|
|
import {
|
|
|
|
Message,
|
|
|
|
MessageHeader,
|
|
|
|
MessageReply,
|
|
|
|
MessageContent,
|
2021-08-20 16:42:57 +03:00
|
|
|
MessageEdit,
|
2021-08-10 14:28:44 +03:00
|
|
|
MessageReactionGroup,
|
|
|
|
MessageReaction,
|
2021-08-11 10:59:01 +03:00
|
|
|
MessageOptions,
|
2021-08-10 14:28:44 +03:00
|
|
|
PlaceholderMessage,
|
|
|
|
} from '../../molecules/message/Message';
|
2021-08-04 12:52:59 +03:00
|
|
|
import * as Media from '../../molecules/media/Media';
|
2021-08-31 16:13:31 +03:00
|
|
|
import RoomIntro from '../../molecules/room-intro/RoomIntro';
|
2021-08-04 12:52:59 +03:00
|
|
|
import TimelineChange from '../../molecules/message/TimelineChange';
|
|
|
|
|
2021-08-11 10:59:01 +03:00
|
|
|
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
2021-08-15 11:29:09 +03:00
|
|
|
import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
|
2021-08-20 16:42:57 +03:00
|
|
|
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
|
|
|
|
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
|
2021-08-16 15:21:23 +03:00
|
|
|
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
|
2021-08-11 10:59:01 +03:00
|
|
|
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
|
|
|
|
2021-08-04 12:52:59 +03:00
|
|
|
import { parseReply, parseTimelineChange } from './common';
|
|
|
|
|
|
|
|
const MAX_MSG_DIFF_MINUTES = 5;
|
|
|
|
|
2021-08-15 11:29:09 +03:00
|
|
|
function genPlaceholders() {
|
|
|
|
return (
|
|
|
|
<>
|
2021-08-21 16:09:21 +03:00
|
|
|
<PlaceholderMessage key="placeholder-1" />
|
|
|
|
<PlaceholderMessage key="placeholder-2" />
|
|
|
|
<PlaceholderMessage key="placeholder-3" />
|
2021-08-15 11:29:09 +03:00
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function isMedia(mE) {
|
|
|
|
return (
|
|
|
|
mE.getContent()?.msgtype === 'm.file'
|
|
|
|
|| mE.getContent()?.msgtype === 'm.image'
|
|
|
|
|| mE.getContent()?.msgtype === 'm.audio'
|
|
|
|
|| mE.getContent()?.msgtype === 'm.video'
|
2021-08-18 12:26:23 +03:00
|
|
|
|| mE.getType() === 'm.sticker'
|
2021-08-15 11:29:09 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function genMediaContent(mE) {
|
|
|
|
const mx = initMatrix.matrixClient;
|
|
|
|
const mContent = mE.getContent();
|
2021-08-18 11:25:44 +03:00
|
|
|
if (!mContent || !mContent.body) return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
|
|
|
|
|
|
|
let mediaMXC = mContent?.url;
|
2021-08-15 11:29:09 +03:00
|
|
|
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
2021-08-18 11:25:44 +03:00
|
|
|
if (isEncryptedFile) mediaMXC = mContent?.file?.url;
|
|
|
|
|
|
|
|
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
|
|
|
|
|
|
|
if (typeof mediaMXC === 'undefined' || mediaMXC === '') return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
2021-08-15 11:29:09 +03:00
|
|
|
|
2021-08-18 12:26:23 +03:00
|
|
|
let msgType = mE.getContent()?.msgtype;
|
|
|
|
if (mE.getType() === 'm.sticker') msgType = 'm.image';
|
|
|
|
|
|
|
|
switch (msgType) {
|
2021-08-15 11:29:09 +03:00
|
|
|
case 'm.file':
|
|
|
|
return (
|
|
|
|
<Media.File
|
|
|
|
name={mContent.body}
|
|
|
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
2021-08-18 11:25:44 +03:00
|
|
|
type={mContent.info?.mimetype}
|
|
|
|
file={mContent.file || null}
|
2021-08-15 11:29:09 +03:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
case 'm.image':
|
|
|
|
return (
|
|
|
|
<Media.Image
|
|
|
|
name={mContent.body}
|
2021-08-18 11:25:44 +03:00
|
|
|
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
|
|
|
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
2021-08-15 11:29:09 +03:00
|
|
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
|
|
file={isEncryptedFile ? mContent.file : null}
|
2021-08-18 11:25:44 +03:00
|
|
|
type={mContent.info?.mimetype}
|
2021-08-15 11:29:09 +03:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
case 'm.audio':
|
|
|
|
return (
|
|
|
|
<Media.Audio
|
|
|
|
name={mContent.body}
|
|
|
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
2021-08-18 11:25:44 +03:00
|
|
|
type={mContent.info?.mimetype}
|
|
|
|
file={mContent.file || null}
|
2021-08-15 11:29:09 +03:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
case 'm.video':
|
|
|
|
if (typeof thumbnailMXC === 'undefined') {
|
|
|
|
thumbnailMXC = mContent.info?.thumbnail_file?.url || null;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<Media.Video
|
|
|
|
name={mContent.body}
|
|
|
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
|
|
thumbnail={thumbnailMXC === null ? null : mx.mxcUrlToHttp(thumbnailMXC)}
|
2021-08-18 11:25:44 +03:00
|
|
|
thumbnailFile={isEncryptedFile ? mContent.info?.thumbnail_file : null}
|
|
|
|
thumbnailType={mContent.info?.thumbnail_info?.mimetype || null}
|
|
|
|
width={typeof mContent.info?.w === 'number' ? mContent.info?.w : null}
|
|
|
|
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
|
2021-08-15 11:29:09 +03:00
|
|
|
file={isEncryptedFile ? mContent.file : null}
|
2021-08-18 11:25:44 +03:00
|
|
|
type={mContent.info?.mimetype}
|
2021-08-15 11:29:09 +03:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
default:
|
2021-08-18 11:25:44 +03:00
|
|
|
return <span style={{ color: 'var(--bg-danger)' }}>Malformed event</span>;
|
2021-08-15 11:29:09 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-31 16:13:31 +03:00
|
|
|
function genRoomIntro(mEvent, roomTimeline) {
|
2021-08-17 14:21:22 +03:00
|
|
|
const mx = initMatrix.matrixClient;
|
2021-08-15 11:29:09 +03:00
|
|
|
const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
2021-08-17 14:21:22 +03:00
|
|
|
const isDM = initMatrix.roomList.directs.has(roomTimeline.roomId);
|
|
|
|
let avatarSrc = roomTimeline.room.getAvatarUrl(mx.baseUrl, 80, 80, 'crop');
|
|
|
|
avatarSrc = isDM ? roomTimeline.room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 80, 80, 'crop') : avatarSrc;
|
2021-08-15 11:29:09 +03:00
|
|
|
return (
|
2021-08-31 16:13:31 +03:00
|
|
|
<RoomIntro
|
|
|
|
key={mEvent ? mEvent.getId() : 'room-intro'}
|
2021-08-17 14:34:21 +03:00
|
|
|
roomId={roomTimeline.roomId}
|
2021-08-17 14:21:22 +03:00
|
|
|
avatarSrc={avatarSrc}
|
2021-08-15 11:29:09 +03:00
|
|
|
name={roomTimeline.room.name}
|
|
|
|
heading={`Welcome to ${roomTimeline.room.name}`}
|
2021-08-31 16:13:31 +03:00
|
|
|
desc={`This is the beginning of ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
|
2021-08-15 11:29:09 +03:00
|
|
|
time={mEvent ? `Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function getMyEmojiEventId(emojiKey, eventId, roomTimeline) {
|
|
|
|
const mx = initMatrix.matrixClient;
|
|
|
|
const rEvents = roomTimeline.reactionTimeline.get(eventId);
|
|
|
|
let rEventId = null;
|
|
|
|
rEvents?.find((rE) => {
|
|
|
|
if (rE.getRelation() === null) return false;
|
|
|
|
if (rE.getRelation().key === emojiKey && rE.getSender() === mx.getUserId()) {
|
|
|
|
rEventId = rE.getId();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
return rEventId;
|
|
|
|
}
|
|
|
|
|
|
|
|
function toggleEmoji(roomId, eventId, emojiKey, roomTimeline) {
|
|
|
|
const myAlreadyReactEventId = getMyEmojiEventId(emojiKey, eventId, roomTimeline);
|
|
|
|
if (typeof myAlreadyReactEventId === 'string') {
|
|
|
|
if (myAlreadyReactEventId.indexOf('~') === 0) return;
|
|
|
|
redactEvent(roomId, myAlreadyReactEventId);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
sendReaction(roomId, eventId, emojiKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
function pickEmoji(e, roomId, eventId, roomTimeline) {
|
2021-09-09 16:06:39 +03:00
|
|
|
openEmojiBoard(getEventCords(e), (emoji) => {
|
2021-08-15 11:29:09 +03:00
|
|
|
toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
|
|
|
|
e.target.click();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-04 12:52:59 +03:00
|
|
|
let wasAtBottom = true;
|
2021-08-31 16:13:31 +03:00
|
|
|
function RoomViewContent({
|
2021-08-04 12:52:59 +03:00
|
|
|
roomId, roomTimeline, timelineScroll, viewEvent,
|
|
|
|
}) {
|
|
|
|
const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false);
|
|
|
|
const [onStateUpdate, updateState] = useState(null);
|
|
|
|
const [onPagination, setOnPagination] = useState(null);
|
2021-08-20 16:42:57 +03:00
|
|
|
const [editEvent, setEditEvent] = useState(null);
|
2021-08-04 12:52:59 +03:00
|
|
|
const mx = initMatrix.matrixClient;
|
|
|
|
|
|
|
|
function autoLoadTimeline() {
|
|
|
|
if (timelineScroll.isScrollable() === true) return;
|
|
|
|
roomTimeline.paginateBack();
|
|
|
|
}
|
|
|
|
function trySendingReadReceipt() {
|
|
|
|
const { room, timeline } = roomTimeline;
|
|
|
|
if (doesRoomHaveUnread(room) && timeline.length !== 0) {
|
|
|
|
mx.sendReadReceipt(timeline[timeline.length - 1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function onReachedTop() {
|
|
|
|
if (roomTimeline.isOngoingPagination || isReachedTimelineEnd) return;
|
|
|
|
roomTimeline.paginateBack();
|
|
|
|
}
|
|
|
|
function toggleOnReachedBottom(isBottom) {
|
|
|
|
wasAtBottom = isBottom;
|
|
|
|
if (!isBottom) return;
|
|
|
|
trySendingReadReceipt();
|
|
|
|
}
|
|
|
|
|
|
|
|
const updatePAG = (canPagMore) => {
|
|
|
|
if (!canPagMore) {
|
|
|
|
setIsReachedTimelineEnd(true);
|
|
|
|
} else {
|
|
|
|
setOnPagination({});
|
|
|
|
autoLoadTimeline();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// force update RoomTimeline on cons.events.roomTimeline.EVENT
|
|
|
|
const updateRT = () => {
|
|
|
|
if (wasAtBottom) {
|
|
|
|
trySendingReadReceipt();
|
|
|
|
}
|
|
|
|
updateState({});
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setIsReachedTimelineEnd(false);
|
|
|
|
wasAtBottom = true;
|
|
|
|
}, [roomId]);
|
|
|
|
useEffect(() => trySendingReadReceipt(), [roomTimeline]);
|
|
|
|
|
|
|
|
// init room setup completed.
|
|
|
|
// listen for future. setup stateUpdate listener.
|
|
|
|
useEffect(() => {
|
|
|
|
roomTimeline.on(cons.events.roomTimeline.EVENT, updateRT);
|
|
|
|
roomTimeline.on(cons.events.roomTimeline.PAGINATED, updatePAG);
|
|
|
|
viewEvent.on('reached-top', onReachedTop);
|
|
|
|
viewEvent.on('toggle-reached-bottom', toggleOnReachedBottom);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
roomTimeline.removeListener(cons.events.roomTimeline.EVENT, updateRT);
|
|
|
|
roomTimeline.removeListener(cons.events.roomTimeline.PAGINATED, updatePAG);
|
|
|
|
viewEvent.removeListener('reached-top', onReachedTop);
|
|
|
|
viewEvent.removeListener('toggle-reached-bottom', toggleOnReachedBottom);
|
|
|
|
};
|
|
|
|
}, [roomTimeline, isReachedTimelineEnd, onPagination]);
|
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
timelineScroll.reachBottom();
|
|
|
|
autoLoadTimeline();
|
|
|
|
}, [roomTimeline]);
|
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
if (onPagination === null) return;
|
|
|
|
timelineScroll.tryRestoringScroll();
|
|
|
|
}, [onPagination]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (onStateUpdate === null) return;
|
|
|
|
if (wasAtBottom) timelineScroll.reachBottom();
|
|
|
|
}, [onStateUpdate]);
|
|
|
|
|
|
|
|
let prevMEvent = null;
|
2021-08-20 16:42:57 +03:00
|
|
|
function genMessage(mEvent) {
|
|
|
|
const myPowerlevel = roomTimeline.room.getMember(mx.getUserId()).powerLevel;
|
|
|
|
const canIRedact = roomTimeline.room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
|
|
|
|
|
|
|
|
const isContentOnly = (
|
|
|
|
prevMEvent !== null
|
|
|
|
&& prevMEvent.getType() !== 'm.room.member'
|
|
|
|
&& diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
|
|
|
|
&& prevMEvent.getSender() === mEvent.getSender()
|
|
|
|
);
|
|
|
|
|
|
|
|
let content = mEvent.getContent().body;
|
|
|
|
if (typeof content === 'undefined') return null;
|
|
|
|
let reply = null;
|
|
|
|
let reactions = null;
|
|
|
|
let isMarkdown = mEvent.getContent().format === 'org.matrix.custom.html';
|
|
|
|
const isReply = typeof mEvent.getWireContent()['m.relates_to']?.['m.in_reply_to'] !== 'undefined';
|
|
|
|
const isEdited = roomTimeline.editedTimeline.has(mEvent.getId());
|
|
|
|
const haveReactions = roomTimeline.reactionTimeline.has(mEvent.getId());
|
|
|
|
|
|
|
|
if (isReply) {
|
|
|
|
const parsedContent = parseReply(content);
|
|
|
|
if (parsedContent !== null) {
|
|
|
|
const c = roomTimeline.room.currentState;
|
|
|
|
const displayNameToUserIds = c.getUserIdsWithDisplayName(parsedContent.displayName);
|
|
|
|
const ID = parsedContent.userId || displayNameToUserIds[0];
|
|
|
|
reply = {
|
|
|
|
color: colorMXID(ID || parsedContent.displayName),
|
|
|
|
to: parsedContent.displayName || getUsername(parsedContent.userId),
|
|
|
|
content: parsedContent.replyContent,
|
|
|
|
};
|
|
|
|
content = parsedContent.content;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isEdited) {
|
|
|
|
const editedList = roomTimeline.editedTimeline.get(mEvent.getId());
|
|
|
|
const latestEdited = editedList[editedList.length - 1];
|
|
|
|
if (typeof latestEdited.getContent()['m.new_content'] === 'undefined') return null;
|
|
|
|
const latestEditBody = latestEdited.getContent()['m.new_content'].body;
|
|
|
|
const parsedEditedContent = parseReply(latestEditBody);
|
|
|
|
isMarkdown = latestEdited.getContent()['m.new_content'].format === 'org.matrix.custom.html';
|
|
|
|
if (parsedEditedContent === null) {
|
|
|
|
content = latestEditBody;
|
|
|
|
} else {
|
|
|
|
content = parsedEditedContent.content;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (haveReactions) {
|
|
|
|
reactions = [];
|
|
|
|
roomTimeline.reactionTimeline.get(mEvent.getId()).forEach((rEvent) => {
|
|
|
|
if (rEvent.getRelation() === null) return;
|
|
|
|
function alreadyHaveThisReaction(rE) {
|
|
|
|
for (let i = 0; i < reactions.length; i += 1) {
|
|
|
|
if (reactions[i].key === rE.getRelation().key) return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (alreadyHaveThisReaction(rEvent)) {
|
|
|
|
for (let i = 0; i < reactions.length; i += 1) {
|
|
|
|
if (reactions[i].key === rEvent.getRelation().key) {
|
|
|
|
reactions[i].users.push(rEvent.getSender());
|
|
|
|
if (reactions[i].isActive !== true) {
|
|
|
|
const myUserId = initMatrix.matrixClient.getUserId();
|
|
|
|
reactions[i].isActive = rEvent.getSender() === myUserId;
|
|
|
|
if (reactions[i].isActive) reactions[i].id = rEvent.getId();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
reactions.push({
|
|
|
|
id: rEvent.getId(),
|
|
|
|
key: rEvent.getRelation().key,
|
|
|
|
users: [rEvent.getSender()],
|
|
|
|
isActive: (rEvent.getSender() === initMatrix.matrixClient.getUserId()),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const senderMXIDColor = colorMXID(mEvent.sender.userId);
|
|
|
|
const userAvatar = isContentOnly ? null : (
|
|
|
|
<Avatar
|
|
|
|
imageSrc={mEvent.sender.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop')}
|
2021-08-25 11:36:13 +03:00
|
|
|
text={getUsernameOfRoomMember(mEvent.sender).slice(0, 1)}
|
2021-08-20 16:42:57 +03:00
|
|
|
bgColor={senderMXIDColor}
|
|
|
|
size="small"
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
const userHeader = isContentOnly ? null : (
|
|
|
|
<MessageHeader
|
|
|
|
userId={mEvent.sender.userId}
|
2021-08-25 11:36:13 +03:00
|
|
|
name={getUsernameOfRoomMember(mEvent.sender)}
|
2021-08-20 16:42:57 +03:00
|
|
|
color={senderMXIDColor}
|
|
|
|
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
const userReply = reply === null ? null : (
|
|
|
|
<MessageReply
|
|
|
|
name={reply.to}
|
|
|
|
color={reply.color}
|
|
|
|
content={reply.content}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
const userContent = (
|
|
|
|
<MessageContent
|
|
|
|
isMarkdown={isMarkdown}
|
|
|
|
content={isMedia(mEvent) ? genMediaContent(mEvent) : content}
|
|
|
|
isEdited={isEdited}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
const userReactions = reactions === null ? null : (
|
|
|
|
<MessageReactionGroup>
|
|
|
|
{
|
|
|
|
reactions.map((reaction) => (
|
|
|
|
<MessageReaction
|
|
|
|
key={reaction.id}
|
|
|
|
reaction={reaction.key}
|
|
|
|
users={reaction.users}
|
|
|
|
isActive={reaction.isActive}
|
|
|
|
onClick={() => {
|
|
|
|
toggleEmoji(roomId, mEvent.getId(), reaction.key, roomTimeline);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
))
|
|
|
|
}
|
|
|
|
<IconButton
|
|
|
|
onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
|
|
|
|
src={EmojiAddIC}
|
|
|
|
size="extra-small"
|
|
|
|
tooltip="Add reaction"
|
|
|
|
/>
|
|
|
|
</MessageReactionGroup>
|
|
|
|
);
|
|
|
|
const userOptions = (
|
|
|
|
<MessageOptions>
|
|
|
|
<IconButton
|
|
|
|
onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
|
|
|
|
src={EmojiAddIC}
|
|
|
|
size="extra-small"
|
|
|
|
tooltip="Add reaction"
|
|
|
|
/>
|
|
|
|
<IconButton
|
|
|
|
onClick={() => {
|
|
|
|
viewEvent.emit('reply_to', mEvent.getSender(), mEvent.getId(), isMedia(mEvent) ? mEvent.getContent().body : content);
|
|
|
|
}}
|
|
|
|
src={ReplyArrowIC}
|
|
|
|
size="extra-small"
|
|
|
|
tooltip="Reply"
|
|
|
|
/>
|
|
|
|
{(mEvent.getSender() === mx.getUserId() && !isMedia(mEvent)) && (
|
|
|
|
<IconButton
|
|
|
|
onClick={() => setEditEvent(mEvent)}
|
|
|
|
src={PencilIC}
|
|
|
|
size="extra-small"
|
|
|
|
tooltip="Edit"
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<ContextMenu
|
|
|
|
content={() => (
|
|
|
|
<>
|
|
|
|
<MenuHeader>Options</MenuHeader>
|
|
|
|
<MenuItem
|
|
|
|
iconSrc={EmojiAddIC}
|
|
|
|
onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
|
|
|
|
>
|
2021-08-22 15:45:20 +03:00
|
|
|
Add reaction
|
2021-08-20 16:42:57 +03:00
|
|
|
</MenuItem>
|
|
|
|
<MenuItem
|
|
|
|
iconSrc={ReplyArrowIC}
|
|
|
|
onClick={() => {
|
|
|
|
viewEvent.emit('reply_to', mEvent.getSender(), mEvent.getId(), isMedia(mEvent) ? mEvent.getContent().body : content);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Reply
|
|
|
|
</MenuItem>
|
|
|
|
{(mEvent.getSender() === mx.getUserId() && !isMedia(mEvent)) && (
|
|
|
|
<MenuItem iconSrc={PencilIC} onClick={() => setEditEvent(mEvent)}>Edit</MenuItem>
|
|
|
|
)}
|
|
|
|
<MenuItem
|
|
|
|
iconSrc={TickMarkIC}
|
|
|
|
onClick={() => openReadReceipts(roomId, mEvent.getId())}
|
|
|
|
>
|
|
|
|
Read receipts
|
|
|
|
</MenuItem>
|
|
|
|
{(canIRedact || mEvent.getSender() === mx.getUserId()) && (
|
|
|
|
<>
|
|
|
|
<MenuBorder />
|
|
|
|
<MenuItem
|
|
|
|
variant="danger"
|
|
|
|
iconSrc={BinIC}
|
|
|
|
onClick={() => {
|
|
|
|
if (window.confirm('Are you sure you want to delete this event')) {
|
|
|
|
redactEvent(roomId, mEvent.getId());
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
Delete
|
|
|
|
</MenuItem>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
render={(toggleMenu) => (
|
|
|
|
<IconButton
|
|
|
|
onClick={toggleMenu}
|
|
|
|
src={VerticalMenuIC}
|
|
|
|
size="extra-small"
|
|
|
|
tooltip="Options"
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
</MessageOptions>
|
|
|
|
);
|
|
|
|
|
|
|
|
const isEditingEvent = editEvent?.getId() === mEvent.getId();
|
|
|
|
const myMessageEl = (
|
|
|
|
<Message
|
|
|
|
key={mEvent.getId()}
|
|
|
|
avatar={userAvatar}
|
|
|
|
header={userHeader}
|
|
|
|
reply={userReply}
|
|
|
|
content={editEvent !== null && isEditingEvent ? null : userContent}
|
|
|
|
editContent={editEvent !== null && isEditingEvent ? (
|
|
|
|
<MessageEdit
|
|
|
|
content={content}
|
|
|
|
onSave={(newBody) => {
|
|
|
|
if (newBody !== content) {
|
|
|
|
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
|
|
|
|
}
|
|
|
|
setEditEvent(null);
|
|
|
|
}}
|
|
|
|
onCancel={() => setEditEvent(null)}
|
|
|
|
/>
|
|
|
|
) : null}
|
|
|
|
reactions={userReactions}
|
|
|
|
options={editEvent !== null && isEditingEvent ? null : userOptions}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
return myMessageEl;
|
|
|
|
}
|
|
|
|
|
2021-08-04 12:52:59 +03:00
|
|
|
function renderMessage(mEvent) {
|
2021-08-31 16:13:31 +03:00
|
|
|
if (mEvent.getType() === 'm.room.create') return genRoomIntro(mEvent, roomTimeline);
|
2021-08-04 12:52:59 +03:00
|
|
|
if (
|
|
|
|
mEvent.getType() !== 'm.room.message'
|
|
|
|
&& mEvent.getType() !== 'm.room.encrypted'
|
|
|
|
&& mEvent.getType() !== 'm.room.member'
|
2021-08-18 12:26:23 +03:00
|
|
|
&& mEvent.getType() !== 'm.sticker'
|
2021-08-04 12:52:59 +03:00
|
|
|
) return false;
|
|
|
|
if (mEvent.getRelation()?.rel_type === 'm.replace') return false;
|
|
|
|
|
|
|
|
// ignore if message is deleted
|
|
|
|
if (mEvent.isRedacted()) return false;
|
|
|
|
|
|
|
|
let divider = null;
|
|
|
|
if (prevMEvent !== null && isNotInSameDay(mEvent.getDate(), prevMEvent.getDate())) {
|
|
|
|
divider = <Divider key={`divider-${mEvent.getId()}`} text={`${dateFormat(mEvent.getDate(), 'mmmm dd, yyyy')}`} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mEvent.getType() !== 'm.room.member') {
|
2021-08-20 16:42:57 +03:00
|
|
|
const messageComp = genMessage(mEvent);
|
2021-08-04 12:52:59 +03:00
|
|
|
prevMEvent = mEvent;
|
2021-08-15 11:29:09 +03:00
|
|
|
return (
|
|
|
|
<React.Fragment key={`box-${mEvent.getId()}`}>
|
|
|
|
{divider}
|
|
|
|
{messageComp}
|
|
|
|
</React.Fragment>
|
|
|
|
);
|
2021-08-04 12:52:59 +03:00
|
|
|
}
|
2021-08-15 11:29:09 +03:00
|
|
|
|
2021-08-04 12:52:59 +03:00
|
|
|
prevMEvent = mEvent;
|
|
|
|
const timelineChange = parseTimelineChange(mEvent);
|
|
|
|
if (timelineChange === null) return null;
|
|
|
|
return (
|
|
|
|
<React.Fragment key={`box-${mEvent.getId()}`}>
|
|
|
|
{divider}
|
|
|
|
<TimelineChange
|
|
|
|
key={mEvent.getId()}
|
|
|
|
variant={timelineChange.variant}
|
|
|
|
content={timelineChange.content}
|
|
|
|
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
|
|
|
/>
|
|
|
|
</React.Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2021-08-31 16:13:31 +03:00
|
|
|
<div className="room-view__content">
|
2021-08-04 12:52:59 +03:00
|
|
|
<div className="timeline__wrapper">
|
2021-08-15 11:29:09 +03:00
|
|
|
{ roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && genPlaceholders() }
|
2021-08-31 16:13:31 +03:00
|
|
|
{ roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && genRoomIntro(undefined, roomTimeline)}
|
2021-08-04 12:52:59 +03:00
|
|
|
{ roomTimeline.timeline.map(renderMessage) }
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2021-08-31 16:13:31 +03:00
|
|
|
RoomViewContent.propTypes = {
|
2021-08-04 12:52:59 +03:00
|
|
|
roomId: PropTypes.string.isRequired,
|
|
|
|
roomTimeline: PropTypes.shape({}).isRequired,
|
2021-08-15 11:29:09 +03:00
|
|
|
timelineScroll: PropTypes.shape({}).isRequired,
|
2021-08-04 12:52:59 +03:00
|
|
|
viewEvent: PropTypes.shape({}).isRequired,
|
|
|
|
};
|
|
|
|
|
2021-08-31 16:13:31 +03:00
|
|
|
export default RoomViewContent;
|