added support for msg editing [#40]

This commit is contained in:
unknown 2021-08-20 19:12:57 +05:30
parent fe3d2e0af4
commit ac4c0ec1f6
4 changed files with 307 additions and 191 deletions

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
<path d="M20.6,5.6l-2.2-2.2C18,3,17.5,2.8,17,2.8S16,3,15.6,3.4L3,16v5h5L20.6,8.4C21.4,7.6,21.4,6.4,20.6,5.6z M7.2,19H5v-2.2
l9.2-9.2l2.2,2.2L7.2,19z M15.6,6.2L17,4.8c0,0,0,0,0,0L19.2,7l-1.4,1.4L15.6,6.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 652 B

View file

@ -44,6 +44,7 @@
justify-content: start; justify-content: start;
border-radius: 0; border-radius: 0;
box-shadow: none; box-shadow: none;
white-space: nowrap;
.text:first-child { .text:first-child {
margin: { margin: {

View file

@ -16,11 +16,13 @@ import { openEmojiBoard, openReadReceipts } from '../../../client/action/navigat
import Divider from '../../atoms/divider/Divider'; import Divider from '../../atoms/divider/Divider';
import Avatar from '../../atoms/avatar/Avatar'; import Avatar from '../../atoms/avatar/Avatar';
import IconButton from '../../atoms/button/IconButton'; import IconButton from '../../atoms/button/IconButton';
import ContextMenu, { MenuHeader, MenuItem, MenuBorder } from '../../atoms/context-menu/ContextMenu';
import { import {
Message, Message,
MessageHeader, MessageHeader,
MessageReply, MessageReply,
MessageContent, MessageContent,
MessageEdit,
MessageReactionGroup, MessageReactionGroup,
MessageReaction, MessageReaction,
MessageOptions, MessageOptions,
@ -32,6 +34,8 @@ 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 EmojiAddIC from '../../../../public/res/ic/outlined/emoji-add.svg';
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg'; import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg'; import BinIC from '../../../../public/res/ic/outlined/bin.svg';
@ -182,8 +186,92 @@ function pickEmoji(e, roomId, eventId, roomTimeline) {
}); });
} }
function genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent) { 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 [editEvent, setEditEvent] = useState(null);
const mx = initMatrix.matrixClient; 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 genMessage(mEvent) {
const myPowerlevel = roomTimeline.room.getMember(mx.getUserId()).powerLevel; const myPowerlevel = roomTimeline.room.getMember(mx.getUserId()).powerLevel;
const canIRedact = roomTimeline.room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel); const canIRedact = roomTimeline.room.currentState.hasSufficientPowerLevelFor('redact', myPowerlevel);
@ -207,7 +295,8 @@ function genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent) {
const parsedContent = parseReply(content); const parsedContent = parseReply(content);
if (parsedContent !== null) { if (parsedContent !== null) {
const c = roomTimeline.room.currentState; const c = roomTimeline.room.currentState;
const ID = parsedContent.userId || c.getUserIdsWithDisplayName(parsedContent.displayName)[0]; const displayNameToUserIds = c.getUserIdsWithDisplayName(parsedContent.displayName);
const ID = parsedContent.userId || displayNameToUserIds[0];
reply = { reply = {
color: colorMXID(ID || parsedContent.displayName), color: colorMXID(ID || parsedContent.displayName),
to: parsedContent.displayName || getUsername(parsedContent.userId), to: parsedContent.displayName || getUsername(parsedContent.userId),
@ -334,125 +423,98 @@ function genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent) {
size="extra-small" size="extra-small"
tooltip="Reply" tooltip="Reply"
/> />
{(mEvent.getSender() === mx.getUserId() && !isMedia(mEvent)) && (
<IconButton <IconButton
onClick={() => openReadReceipts(roomId, mEvent.getId())} onClick={() => setEditEvent(mEvent)}
src={TickMarkIC} src={PencilIC}
size="extra-small" size="extra-small"
tooltip="Read receipts" tooltip="Edit"
/> />
)}
<ContextMenu
content={() => (
<>
<MenuHeader>Options</MenuHeader>
<MenuItem
iconSrc={EmojiAddIC}
onClick={(e) => pickEmoji(e, roomId, mEvent.getId(), roomTimeline)}
>
Add reaciton
</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()) && ( {(canIRedact || mEvent.getSender() === mx.getUserId()) && (
<IconButton <>
<MenuBorder />
<MenuItem
variant="danger"
iconSrc={BinIC}
onClick={() => { onClick={() => {
if (window.confirm('Are you sure you want to delete this event')) { if (window.confirm('Are you sure you want to delete this event')) {
redactEvent(roomId, mEvent.getId()); redactEvent(roomId, mEvent.getId());
} }
}} }}
src={BinIC} >
Delete
</MenuItem>
</>
)}
</>
)}
render={(toggleMenu) => (
<IconButton
onClick={toggleMenu}
src={VerticalMenuIC}
size="extra-small" size="extra-small"
tooltip="Delete" tooltip="Options"
/> />
)} )}
/>
</MessageOptions> </MessageOptions>
); );
const isEditingEvent = editEvent?.getId() === mEvent.getId();
const myMessageEl = ( const myMessageEl = (
<Message <Message
key={mEvent.getId()} key={mEvent.getId()}
avatar={userAvatar} avatar={userAvatar}
header={userHeader} header={userHeader}
reply={userReply} reply={userReply}
content={userContent} 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} reactions={userReactions}
options={userOptions} options={editEvent !== null && isEditingEvent ? null : userOptions}
/> />
); );
return myMessageEl; return myMessageEl;
} }
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 renderMessage(mEvent) {
if (mEvent.getType() === 'm.room.create') return genChannelIntro(mEvent, roomTimeline); if (mEvent.getType() === 'm.room.create') return genChannelIntro(mEvent, roomTimeline);
if ( if (
@ -472,7 +534,7 @@ function ChannelViewContent({
} }
if (mEvent.getType() !== 'm.room.member') { if (mEvent.getType() !== 'm.room.member') {
const messageComp = genMessage(roomId, prevMEvent, mEvent, roomTimeline, viewEvent); const messageComp = genMessage(mEvent);
prevMEvent = mEvent; prevMEvent = mEvent;
return ( return (
<React.Fragment key={`box-${mEvent.getId()}`}> <React.Fragment key={`box-${mEvent.getId()}`}>

View file

@ -86,7 +86,10 @@ function getFormattedBody(markdown) {
extensions: [gfm()], extensions: [gfm()],
htmlExtensions: [gfmHtml], htmlExtensions: [gfmHtml],
}); });
return result; const bodyParts = result.match(/^(<p>)(.*)(<\/p>)$/);
if (bodyParts === null) return result;
if (bodyParts[2].indexOf('</p>') >= 0) return result;
return bodyParts[2];
} }
function getReplyFormattedBody(roomId, reply) { function getReplyFormattedBody(roomId, reply) {
@ -212,8 +215,11 @@ class RoomsInput extends EventEmitter {
msgtype: 'm.text', msgtype: 'm.text',
}; };
if (settings.isMarkdown) { if (settings.isMarkdown) {
const formattedBody = getFormattedBody(input.message);
if (formattedBody !== input.message) {
content.format = 'org.matrix.custom.html'; content.format = 'org.matrix.custom.html';
content.formatted_body = getFormattedBody(input.message); content.formatted_body = formattedBody;
}
} }
if (typeof input.replyTo !== 'undefined') { if (typeof input.replyTo !== 'undefined') {
content = bindReplyToContent(roomId, input.replyTo, content); content = bindReplyToContent(roomId, input.replyTo, content);
@ -326,6 +332,45 @@ class RoomsInput extends EventEmitter {
} }
return { url }; return { url };
} }
async sendEditedMessage(roomId, mEvent, editedBody) {
const isReply = typeof mEvent.getWireContent()['m.relates_to']?.['m.in_reply_to'] !== 'undefined';
const content = {
body: ` * ${editedBody}`,
msgtype: 'm.text',
'm.new_content': {
body: editedBody,
msgtype: 'm.text',
},
'm.relates_to': {
event_id: mEvent.getId(),
rel_type: 'm.replace',
},
};
if (settings.isMarkdown) {
const formattedBody = getFormattedBody(editedBody);
if (formattedBody !== editedBody) {
content.formatted_body = ` * ${formattedBody}`;
content.format = 'org.matrix.custom.html';
content['m.new_content'].formatted_body = formattedBody;
content['m.new_content'].format = 'org.matrix.custom.html';
}
}
if (isReply) {
const evBody = mEvent.getContent().body;
const replyHead = evBody.slice(0, evBody.indexOf('\n\n'));
const evFBody = mEvent.getContent().formatted_body;
const fReplyHead = evFBody.slice(0, evFBody.indexOf('</mx-reply>'));
content.format = 'org.matrix.custom.html';
content.formatted_body = `${fReplyHead}</mx-reply>${(content.formatted_body || content.body)}`;
content.body = `${replyHead}\n\n${content.body}`;
}
this.matrixClient.sendMessage(roomId, content);
}
} }
export default RoomsInput; export default RoomsInput;