/* eslint-disable react/prop-types */ import React, { useState, useEffect, useLayoutEffect } from 'react'; import PropTypes from 'prop-types'; import './ChannelViewContent.scss'; import dateFormat from 'dateformat'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import { redact } from '../../../client/action/room'; import { getUsername, doesRoomHaveUnread } from '../../../util/matrixUtil'; import colorMXID from '../../../util/colorMXID'; import { diffMinutes, isNotInSameDay } from '../../../util/common'; import Divider from '../../atoms/divider/Divider'; import Avatar from '../../atoms/avatar/Avatar'; import IconButton from '../../atoms/button/IconButton'; import { Message, MessageHeader, MessageReply, MessageContent, MessageReactionGroup, MessageReaction, MessageOptions, PlaceholderMessage, } from '../../molecules/message/Message'; import * as Media from '../../molecules/media/Media'; import ChannelIntro from '../../molecules/channel-intro/ChannelIntro'; import TimelineChange from '../../molecules/message/TimelineChange'; import ReplyArrowIC from '../../../../public/res/ic/outlined/reply-arrow.svg'; import BinIC from '../../../../public/res/ic/outlined/bin.svg'; import { parseReply, parseTimelineChange } from './common'; const MAX_MSG_DIFF_MINUTES = 5; let wasAtBottom = true; function ChannelViewContent({ roomId, roomTimeline, timelineScroll, viewEvent, }) { const [isReachedTimelineEnd, setIsReachedTimelineEnd] = useState(false); const [onStateUpdate, updateState] = useState(null); const [onPagination, setOnPagination] = useState(null); 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; function renderMessage(mEvent) { 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 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 ( ); case 'm.image': return ( ); case 'm.audio': return ( ); case 'm.video': if (typeof thumbnailMXC === 'undefined') { thumbnailMXC = mContent.info?.thumbnail_file?.url || null; } return ( ); 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 ( ); } if ( mEvent.getType() !== 'm.room.message' && mEvent.getType() !== 'm.room.encrypted' && mEvent.getType() !== 'm.room.member' ) 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 = ; } if (mEvent.getType() !== 'm.room.member') { const isContentOnly = ( 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 : ( ); const userHeader = isContentOnly ? null : ( ); const userReply = reply === null ? null : ( ); const userContent = ( ); const userReactions = reactions === null ? null : ( { reactions.map((reaction) => ( alert('Sending reactions is yet to be implemented.')} /> )) } ); const userOptions = ( { 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()) && ( { if (window.confirm('Are you sure you want to delete this event')) { redact(roomId, mEvent.getId()); } }} src={BinIC} size="extra-small" tooltip="Delete" /> )} ); const myMessageEl = ( ); prevMEvent = mEvent; return myMessageEl; } prevMEvent = mEvent; const timelineChange = parseTimelineChange(mEvent); if (timelineChange === null) return null; return ( {divider} ); } const roomTopic = roomTimeline.room.currentState.getStateEvents('m.room.topic')[0]?.getContent().topic; return (
{ roomTimeline.timeline[0].getType() !== 'm.room.create' && !isReachedTimelineEnd && ( <> ) } { roomTimeline.timeline[0].getType() !== 'm.room.create' && isReachedTimelineEnd && ( ) } { roomTimeline.timeline.map(renderMessage) }
); } ChannelViewContent.propTypes = { roomId: PropTypes.string.isRequired, roomTimeline: PropTypes.shape({}).isRequired, timelineScroll: PropTypes.shape({ reachBottom: PropTypes.func, autoReachBottom: PropTypes.func, tryRestoringScroll: PropTypes.func, enableSmoothScroll: PropTypes.func, disableSmoothScroll: PropTypes.func, isScrollable: PropTypes.func, }).isRequired, viewEvent: PropTypes.shape({}).isRequired, }; export default ChannelViewContent;