added support for sending reaction
This commit is contained in:
parent
ebac0db0df
commit
fa85e61d6f
6 changed files with 376 additions and 294 deletions
|
@ -166,6 +166,16 @@
|
||||||
}
|
}
|
||||||
.message__reactions {
|
.message__reactions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& .ic-btn-surface {
|
||||||
|
display: none;
|
||||||
|
padding: var(--sp-ultra-tight);
|
||||||
|
margin-top: var(--sp-extra-tight);
|
||||||
|
}
|
||||||
|
&:hover .ic-btn-surface {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.msg__reaction {
|
.msg__reaction {
|
||||||
margin: var(--sp-extra-tight) var(--sp-extra-tight) 0 0;
|
margin: var(--sp-extra-tight) var(--sp-extra-tight) 0 0;
|
||||||
|
|
|
@ -7,10 +7,11 @@ import dateFormat from 'dateformat';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import cons from '../../../client/state/cons';
|
import cons from '../../../client/state/cons';
|
||||||
import { redact } from '../../../client/action/room';
|
import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
|
||||||
import { getUsername, doesRoomHaveUnread } from '../../../util/matrixUtil';
|
import { getUsername, doesRoomHaveUnread } from '../../../util/matrixUtil';
|
||||||
import colorMXID from '../../../util/colorMXID';
|
import colorMXID from '../../../util/colorMXID';
|
||||||
import { diffMinutes, isNotInSameDay } from '../../../util/common';
|
import { diffMinutes, isNotInSameDay } from '../../../util/common';
|
||||||
|
import { openEmojiBoard } from '../../../client/action/navigation';
|
||||||
|
|
||||||
import Divider from '../../atoms/divider/Divider';
|
import Divider from '../../atoms/divider/Divider';
|
||||||
import Avatar from '../../atoms/avatar/Avatar';
|
import Avatar from '../../atoms/avatar/Avatar';
|
||||||
|
@ -30,12 +31,325 @@ import ChannelIntro from '../../molecules/channel-intro/ChannelIntro';
|
||||||
import TimelineChange from '../../molecules/message/TimelineChange';
|
import TimelineChange from '../../molecules/message/TimelineChange';
|
||||||
|
|
||||||
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg';
|
||||||
|
import EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
|
||||||
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
|
||||||
|
|
||||||
import { parseReply, parseTimelineChange } from './common';
|
import { parseReply, parseTimelineChange } from './common';
|
||||||
|
|
||||||
const MAX_MSG_DIFF_MINUTES = 5;
|
const MAX_MSG_DIFF_MINUTES = 5;
|
||||||
|
|
||||||
|
function genPlaceholders() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
|
||||||
|
<PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
|
||||||
|
<PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMedia(mE) {
|
||||||
|
return (
|
||||||
|
mE.getContent()?.msgtype === 'm.file'
|
||||||
|
|| mE.getContent()?.msgtype === 'm.image'
|
||||||
|
|| mE.getContent()?.msgtype === 'm.audio'
|
||||||
|
|| mE.getContent()?.msgtype === 'm.video'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function genMediaContent(mE) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
const mContent = mE.getContent();
|
||||||
|
let mediaMXC = mContent.url;
|
||||||
|
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
||||||
|
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
||||||
|
if (isEncryptedFile) mediaMXC = mContent.file.url;
|
||||||
|
|
||||||
|
switch (mE.getContent()?.msgtype) {
|
||||||
|
case 'm.file':
|
||||||
|
return (
|
||||||
|
<Media.File
|
||||||
|
name={mContent.body}
|
||||||
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||||
|
file={mContent.file}
|
||||||
|
type={mContent.info.mimetype}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'm.image':
|
||||||
|
return (
|
||||||
|
<Media.Image
|
||||||
|
name={mContent.body}
|
||||||
|
width={mContent.info.w || null}
|
||||||
|
height={mContent.info.h || null}
|
||||||
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||||
|
file={isEncryptedFile ? mContent.file : null}
|
||||||
|
type={mContent.info.mimetype}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'm.audio':
|
||||||
|
return (
|
||||||
|
<Media.Audio
|
||||||
|
name={mContent.body}
|
||||||
|
link={mx.mxcUrlToHttp(mediaMXC)}
|
||||||
|
type={mContent.info.mimetype}
|
||||||
|
file={mContent.file}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
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)}
|
||||||
|
thumbnailFile={isEncryptedFile ? mContent.info.thumbnail_file : null}
|
||||||
|
thumbnailType={mContent.info.thumbnail_info?.mimetype || null}
|
||||||
|
width={mContent.info.w || null}
|
||||||
|
height={mContent.info.h || null}
|
||||||
|
file={isEncryptedFile ? mContent.file : null}
|
||||||
|
type={mContent.info.mimetype}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return 'Unable to attach media file!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function genChannelIntro(mEvent, roomTimeline) {
|
||||||
|
const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
||||||
|
return (
|
||||||
|
<ChannelIntro
|
||||||
|
key={mEvent ? mEvent.getId() : Math.random().toString(20).substr(2, 6)}
|
||||||
|
avatarSrc={roomTimeline.room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 80, 80, 'crop')}
|
||||||
|
name={roomTimeline.room.name}
|
||||||
|
heading={`Welcome to ${roomTimeline.room.name}`}
|
||||||
|
desc={`This is the beginning of ${roomTimeline.room.name} channel.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
|
||||||
|
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) {
|
||||||
|
openEmojiBoard({
|
||||||
|
x: e.detail ? e.clientX : '50%',
|
||||||
|
y: e.detail ? e.clientY : '50%',
|
||||||
|
detail: e.detail,
|
||||||
|
}, (emoji) => {
|
||||||
|
toggleEmoji(roomId, eventId, emoji.unicode, roomTimeline);
|
||||||
|
e.target.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
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 username = getUsername(parsedContent.userId);
|
||||||
|
reply = {
|
||||||
|
userId: parsedContent.userId,
|
||||||
|
color: colorMXID(parsedContent.userId),
|
||||||
|
to: username,
|
||||||
|
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')}
|
||||||
|
text={getUsername(mEvent.sender.userId).slice(0, 1)}
|
||||||
|
bgColor={senderMXIDColor}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const userHeader = isContentOnly ? null : (
|
||||||
|
<MessageHeader
|
||||||
|
userId={mEvent.sender.userId}
|
||||||
|
name={getUsername(mEvent.sender.userId)}
|
||||||
|
color={senderMXIDColor}
|
||||||
|
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const userReply = reply === null ? null : (
|
||||||
|
<MessageReply
|
||||||
|
userId={reply.userId}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
{(canIRedact || mEvent.getSender() === mx.getUserId()) && (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm('Are you sure you want to delete this event')) {
|
||||||
|
redactEvent(roomId, mEvent.getId());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
src={BinIC}
|
||||||
|
size="extra-small"
|
||||||
|
tooltip="Delete"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</MessageOptions>
|
||||||
|
);
|
||||||
|
|
||||||
|
const myMessageEl = (
|
||||||
|
<Message
|
||||||
|
key={mEvent.getId()}
|
||||||
|
avatar={userAvatar}
|
||||||
|
header={userHeader}
|
||||||
|
reply={userReply}
|
||||||
|
content={userContent}
|
||||||
|
reactions={userReactions}
|
||||||
|
options={userOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return myMessageEl;
|
||||||
|
}
|
||||||
|
|
||||||
let wasAtBottom = true;
|
let wasAtBottom = true;
|
||||||
function ChannelViewContent({
|
function ChannelViewContent({
|
||||||
roomId, roomTimeline, timelineScroll, viewEvent,
|
roomId, roomTimeline, timelineScroll, viewEvent,
|
||||||
|
@ -121,86 +435,7 @@ function ChannelViewContent({
|
||||||
|
|
||||||
let prevMEvent = null;
|
let prevMEvent = null;
|
||||||
function renderMessage(mEvent) {
|
function renderMessage(mEvent) {
|
||||||
function isMedia(mE) {
|
if (mEvent.getType() === 'm.room.create') return genChannelIntro(mEvent, roomTimeline);
|
||||||
return (
|
|
||||||
mE.getContent()?.msgtype === 'm.file'
|
|
||||||
|| mE.getContent()?.msgtype === 'm.image'
|
|
||||||
|| mE.getContent()?.msgtype === 'm.audio'
|
|
||||||
|| mE.getContent()?.msgtype === 'm.video'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function genMediaContent(mE) {
|
|
||||||
const mContent = mE.getContent();
|
|
||||||
let mediaMXC = mContent.url;
|
|
||||||
let thumbnailMXC = mContent?.info?.thumbnail_url;
|
|
||||||
const isEncryptedFile = typeof mediaMXC === 'undefined';
|
|
||||||
if (isEncryptedFile) mediaMXC = mContent.file.url;
|
|
||||||
|
|
||||||
switch (mE.getContent()?.msgtype) {
|
|
||||||
case 'm.file':
|
|
||||||
return (
|
|
||||||
<Media.File
|
|
||||||
name={mContent.body}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
||||||
file={mContent.file}
|
|
||||||
type={mContent.info.mimetype}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'm.image':
|
|
||||||
return (
|
|
||||||
<Media.Image
|
|
||||||
name={mContent.body}
|
|
||||||
width={mContent.info.w || null}
|
|
||||||
height={mContent.info.h || null}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
||||||
file={isEncryptedFile ? mContent.file : null}
|
|
||||||
type={mContent.info.mimetype}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case 'm.audio':
|
|
||||||
return (
|
|
||||||
<Media.Audio
|
|
||||||
name={mContent.body}
|
|
||||||
link={mx.mxcUrlToHttp(mediaMXC)}
|
|
||||||
type={mContent.info.mimetype}
|
|
||||||
file={mContent.file}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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)}
|
|
||||||
thumbnailFile={isEncryptedFile ? mContent.info.thumbnail_file : null}
|
|
||||||
thumbnailType={mContent.info.thumbnail_info?.mimetype || null}
|
|
||||||
width={mContent.info.w || null}
|
|
||||||
height={mContent.info.h || null}
|
|
||||||
file={isEncryptedFile ? mContent.file : null}
|
|
||||||
type={mContent.info.mimetype}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return 'Unable to attach media file!';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mEvent.getType() === 'm.room.create') {
|
|
||||||
const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
|
||||||
return (
|
|
||||||
<ChannelIntro
|
|
||||||
key={mEvent.getId()}
|
|
||||||
avatarSrc={roomTimeline.room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 80, 80, 'crop')}
|
|
||||||
name={roomTimeline.room.name}
|
|
||||||
heading={`Welcome to ${roomTimeline.room.name}`}
|
|
||||||
desc={`This is the beginning of ${roomTimeline.room.name} channel.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
|
|
||||||
time={`Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
mEvent.getType() !== 'm.room.message'
|
mEvent.getType() !== 'm.room.message'
|
||||||
&& mEvent.getType() !== 'm.room.encrypted'
|
&& mEvent.getType() !== 'm.room.encrypted'
|
||||||
|
@ -217,173 +452,16 @@ function ChannelViewContent({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mEvent.getType() !== 'm.room.member') {
|
if (mEvent.getType() !== 'm.room.member') {
|
||||||
const isContentOnly = (
|
const messageComp = genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent);
|
||||||
prevMEvent !== null
|
|
||||||
&& prevMEvent.getType() !== 'm.room.member'
|
|
||||||
&& diffMinutes(mEvent.getDate(), prevMEvent.getDate()) <= MAX_MSG_DIFF_MINUTES
|
|
||||||
&& prevMEvent.getSender() === mEvent.getSender()
|
|
||||||
);
|
|
||||||
|
|
||||||
const myPowerlevel = roomTimeline.room.getMember(mx.getUserId()).powerLevel;
|
|
||||||
const canIRedact = roomTimeline.room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
|
|
||||||
|
|
||||||
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 username = getUsername(parsedContent.userId);
|
|
||||||
reply = {
|
|
||||||
userId: parsedContent.userId,
|
|
||||||
color: colorMXID(parsedContent.userId),
|
|
||||||
to: username,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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')}
|
|
||||||
text={getUsername(mEvent.sender.userId).slice(0, 1)}
|
|
||||||
bgColor={senderMXIDColor}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const userHeader = isContentOnly ? null : (
|
|
||||||
<MessageHeader
|
|
||||||
userId={mEvent.sender.userId}
|
|
||||||
name={getUsername(mEvent.sender.userId)}
|
|
||||||
color={senderMXIDColor}
|
|
||||||
time={`${dateFormat(mEvent.getDate(), 'hh:MM TT')}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const userReply = reply === null ? null : (
|
|
||||||
<MessageReply
|
|
||||||
userId={reply.userId}
|
|
||||||
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={() => alert('Sending reactions is yet to be implemented.')}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</MessageReactionGroup>
|
|
||||||
);
|
|
||||||
const userOptions = (
|
|
||||||
<MessageOptions>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
viewEvent.emit('reply_to', mEvent.getSender(), mEvent.getId(), isMedia(mEvent) ? mEvent.getContent().body : content);
|
|
||||||
}}
|
|
||||||
src={ReplyArrowIC}
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Reply"
|
|
||||||
/>
|
|
||||||
{(canIRedact || mEvent.getSender() === mx.getUserId()) && (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
if (window.confirm('Are you sure you want to delete this event')) {
|
|
||||||
redact(roomId, mEvent.getId());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
src={BinIC}
|
|
||||||
size="extra-small"
|
|
||||||
tooltip="Delete"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</MessageOptions>
|
|
||||||
);
|
|
||||||
|
|
||||||
const myMessageEl = (
|
|
||||||
<Message
|
|
||||||
key={mEvent.getId()}
|
|
||||||
avatar={userAvatar}
|
|
||||||
header={userHeader}
|
|
||||||
reply={userReply}
|
|
||||||
content={userContent}
|
|
||||||
reactions={userReactions}
|
|
||||||
options={userOptions}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
prevMEvent = mEvent;
|
prevMEvent = mEvent;
|
||||||
return myMessageEl;
|
return (
|
||||||
|
<React.Fragment key={`box-${mEvent.getId()}`}>
|
||||||
|
{divider}
|
||||||
|
{messageComp}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevMEvent = mEvent;
|
prevMEvent = mEvent;
|
||||||
const timelineChange = parseTimelineChange(mEvent);
|
const timelineChange = parseTimelineChange(mEvent);
|
||||||
if (timelineChange === null) return null;
|
if (timelineChange === null) return null;
|
||||||
|
@ -400,30 +478,11 @@ function ChannelViewContent({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic;
|
|
||||||
return (
|
return (
|
||||||
<div className="channel-view__content">
|
<div className="channel-view__content">
|
||||||
<div className="timeline__wrapper">
|
<div className="timeline__wrapper">
|
||||||
{
|
{ roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && genPlaceholders() }
|
||||||
roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && (
|
{ roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && genChannelIntro(undefined, roomTimeline)}
|
||||||
<>
|
|
||||||
<PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
|
|
||||||
<PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
|
|
||||||
<PlaceholderMessage key={Math.random().toString(20).substr(2, 6)} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && (
|
|
||||||
<ChannelIntro
|
|
||||||
key={Math.random().toString(20).substr(2, 6)}
|
|
||||||
avatarSrc={roomTimeline.room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 80, 80, 'crop')}
|
|
||||||
name={roomTimeline.room.name}
|
|
||||||
heading={`Welcome to ${roomTimeline.room.name}`}
|
|
||||||
desc={`This is the beginning of ${roomTimeline.room.name} channel.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{ roomTimeline.timeline.map(renderMessage) }
|
{ roomTimeline.timeline.map(renderMessage) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -432,14 +491,7 @@ function ChannelViewContent({
|
||||||
ChannelViewContent.propTypes = {
|
ChannelViewContent.propTypes = {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
roomTimeline: PropTypes.shape({}).isRequired,
|
roomTimeline: PropTypes.shape({}).isRequired,
|
||||||
timelineScroll: PropTypes.shape({
|
timelineScroll: PropTypes.shape({}).isRequired,
|
||||||
reachBottom: PropTypes.func,
|
|
||||||
autoReachBottom: PropTypes.func,
|
|
||||||
tryRestoringScroll: PropTypes.func,
|
|
||||||
enableSmoothScroll: PropTypes.func,
|
|
||||||
disableSmoothScroll: PropTypes.func,
|
|
||||||
isScrollable: PropTypes.func,
|
|
||||||
}).isRequired,
|
|
||||||
viewEvent: PropTypes.shape({}).isRequired,
|
viewEvent: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,8 @@ import colorMXID from '../../../util/colorMXID';
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
import IconButton from '../../atoms/button/IconButton';
|
import IconButton from '../../atoms/button/IconButton';
|
||||||
import ContextMenu from '../../atoms/context-menu/ContextMenu';
|
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
import { MessageReply } from '../../molecules/message/Message';
|
import { MessageReply } from '../../molecules/message/Message';
|
||||||
import EmojiBoard from '../emoji-board/EmojiBoard';
|
|
||||||
|
|
||||||
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
|
||||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||||
|
@ -303,9 +301,9 @@ function ChannelViewInput({
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
openEmojiBoard({
|
openEmojiBoard({
|
||||||
x: e.detail ? e.clientX + 40 : '10%',
|
x: '10%',
|
||||||
y: e.detail ? e.clientY - 240 : 300,
|
y: 300,
|
||||||
isReverse: !e.detail,
|
isReverse: true,
|
||||||
detail: e.detail,
|
detail: e.detail,
|
||||||
}, addEmoji);
|
}, addEmoji);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -61,8 +61,9 @@ function EmojiBoardOpener() {
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
type="button"
|
type="button"
|
||||||
style={{
|
style={{
|
||||||
width: '0',
|
width: '32px',
|
||||||
height: '0',
|
height: '32px',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
|
|
@ -189,19 +189,7 @@ async function invite(roomId, userId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function redact(roomId, eventId, reason) {
|
|
||||||
const mx = initMatrix.matrixClient;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mx.redactEvent(roomId, eventId, undefined, typeof reason === 'undefined' ? undefined : { reason });
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
join, leave,
|
join, leave,
|
||||||
create, invite,
|
create, invite,
|
||||||
redact,
|
|
||||||
};
|
};
|
||||||
|
|
33
src/client/action/roomTimeline.js
Normal file
33
src/client/action/roomTimeline.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import initMatrix from '../initMatrix';
|
||||||
|
|
||||||
|
async function redactEvent(roomId, eventId, reason) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mx.redactEvent(roomId, eventId, undefined, typeof reason === 'undefined' ? undefined : { reason });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendReaction(roomId, toEventId, reaction) {
|
||||||
|
const mx = initMatrix.matrixClient;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mx.sendEvent(roomId, 'm.reaction', {
|
||||||
|
'm.relates_to': {
|
||||||
|
event_id: toEventId,
|
||||||
|
key: reaction,
|
||||||
|
rel_type: 'm.annotation',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
redactEvent,
|
||||||
|
sendReaction,
|
||||||
|
};
|
Loading…
Reference in a new issue