added support for sending reaction

This commit is contained in:
unknown 2021-08-15 13:59:09 +05:30
parent ebac0db0df
commit fa85e61d6f
6 changed files with 376 additions and 294 deletions

View file

@ -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;

View file

@ -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,
}; };

View file

@ -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);
}} }}

View file

@ -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,

View file

@ -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,
}; };

View 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,
};