diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index ebc4ce7ce8..c67001cc87 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -263,7 +263,8 @@ export default class TextualBody extends React.Component { //console.info("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); // exploit that events are immutable :) - return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || + return (nextProps.mxEvent !== this.props.mxEvent || + nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || nextProps.highlights !== this.props.highlights || nextProps.replacingEventId !== this.props.replacingEventId || nextProps.highlightLink !== this.props.highlightLink || diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 0099bf73fb..07c6427992 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -300,6 +300,9 @@ interface IState { // The Relations model from the JS SDK for reactions to `mxEvent` reactions: Relations; + // Our snapshotted/local copy of the props.mxEvent, for local echo reasons + mxEvent: MatrixEvent; + hover: boolean; } @@ -334,6 +337,8 @@ export default class EventTile extends React.Component { // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), + mxEvent: this.mxEvent.toSnapshot(), // snapshot up front to verify it all works + hover: false, }; @@ -350,6 +355,10 @@ export default class EventTile extends React.Component { this.ref = React.createRef(); } + private get mxEvent(): MatrixEvent { + return this.state?.mxEvent ?? this.props.mxEvent; + } + /** * When true, the tile qualifies for some sort of special read receipt. This could be a 'sending' * or 'sent' receipt, for example. @@ -358,16 +367,16 @@ export default class EventTile extends React.Component { private get isEligibleForSpecialReceipt() { // First, if there are other read receipts then just short-circuit this. if (this.props.readReceipts && this.props.readReceipts.length > 0) return false; - if (!this.props.mxEvent) return false; + if (!this.mxEvent) return false; // Sanity check (should never happen, but we shouldn't explode if it does) - const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + const room = this.context.getRoom(this.mxEvent.getRoomId()); if (!room) return false; // Quickly check to see if the event was sent by us. If it wasn't, it won't qualify for // special read receipts. const myUserId = MatrixClientPeg.get().getUserId(); - if (this.props.mxEvent.getSender() !== myUserId) return false; + if (this.mxEvent.getSender() !== myUserId) return false; // Finally, determine if the type is relevant to the user. This notably excludes state // events and pretty much anything that can't be sent by the composer as a message. For @@ -378,7 +387,7 @@ export default class EventTile extends React.Component { EventType.RoomMessage, EventType.RoomMessageEncrypted, ]; - if (!simpleSendableEvents.includes(this.props.mxEvent.getType() as EventType)) return false; + if (!simpleSendableEvents.includes(this.mxEvent.getType() as EventType)) return false; // Default case return true; @@ -420,7 +429,7 @@ export default class EventTile extends React.Component { // TODO: [REACT-WARNING] Move into constructor // eslint-disable-next-line camelcase UNSAFE_componentWillMount() { - this.verifyEvent(this.props.mxEvent); + this.verifyEvent(this.mxEvent); } componentDidMount() { @@ -450,11 +459,21 @@ export default class EventTile extends React.Component { } shouldComponentUpdate(nextProps, nextState) { + // If the echo changed meaningfully, update. + if (!this.state.mxEvent?.isEquivalentTo(nextProps.mxEvent)) { + return true; + } + if (objectHasDiff(this.state, nextState)) { return true; } - return !this.propsEqual(this.props, nextProps); + if (!this.propsEqual(this.props, nextProps)) { + return true; + } + + // Always assume there's no significant change. + return false; } componentWillUnmount() { @@ -475,11 +494,18 @@ export default class EventTile extends React.Component { this.context.on("Room.receipt", this.onRoomReceipt); this.isListeningForReceipts = true; } + + // Update the state again if the snapshot needs updating. Note that this will fire + // a second state update to re-render child components, which ultimately calls didUpdate + // again, so we break that loop with a reference check first (faster than comparing events). + if (this.state.mxEvent === prevState.mxEvent && !this.state?.mxEvent.isEquivalentTo(this.props.mxEvent)) { + this.setState({mxEvent: this.props.mxEvent.toSnapshot()}); + } } private onRoomReceipt = (ev, room) => { // ignore events for other rooms - const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const tileRoom = MatrixClientPeg.get().getRoom(this.mxEvent.getRoomId()); if (room !== tileRoom) return; if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) { @@ -503,19 +529,19 @@ export default class EventTile extends React.Component { // we need to re-verify the sending device. // (we call onHeightChanged in verifyEvent to handle the case where decryption // has caused a change in size of the event tile) - this.verifyEvent(this.props.mxEvent); + this.verifyEvent(this.mxEvent); this.forceUpdate(); }; private onDeviceVerificationChanged = (userId, device) => { - if (userId === this.props.mxEvent.getSender()) { - this.verifyEvent(this.props.mxEvent); + if (userId === this.mxEvent.getSender()) { + this.verifyEvent(this.mxEvent); } }; private onUserVerificationChanged = (userId, _trustStatus) => { - if (userId === this.props.mxEvent.getSender()) { - this.verifyEvent(this.props.mxEvent); + if (userId === this.mxEvent.getSender()) { + this.verifyEvent(this.mxEvent); } }; @@ -622,11 +648,11 @@ export default class EventTile extends React.Component { } shouldHighlight() { - const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent); + const actions = this.context.getPushActionsForEvent(this.mxEvent.replacingEvent() || this.mxEvent); if (!actions || !actions.tweaks) { return false; } // don't show self-highlights from another of our clients - if (this.props.mxEvent.getSender() === this.context.credentials.userId) { + if (this.mxEvent.getSender() === this.context.credentials.userId) { return false; } @@ -641,7 +667,7 @@ export default class EventTile extends React.Component { getReadAvatars() { if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { - return ; + return ; } // return early if there are no read receipts @@ -728,7 +754,7 @@ export default class EventTile extends React.Component { } onSenderProfileClick = event => { - const mxEvent = this.props.mxEvent; + const mxEvent = this.mxEvent; dis.dispatch({ action: Action.ComposerInsert, userId: mxEvent.getSender(), @@ -745,7 +771,7 @@ export default class EventTile extends React.Component { // Cancel any outgoing key request for this event and resend it. If a response // is received for the request with the required keys, the event could be // decrypted successfully. - this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent); + this.context.cancelAndResendEventRoomKeyRequest(this.mxEvent); }; onPermalinkClicked = e => { @@ -754,14 +780,14 @@ export default class EventTile extends React.Component { e.preventDefault(); dis.dispatch({ action: 'view_room', - event_id: this.props.mxEvent.getId(), + event_id: this.mxEvent.getId(), highlighted: true, - room_id: this.props.mxEvent.getRoomId(), + room_id: this.mxEvent.getRoomId(), }); }; private renderE2EPadlock() { - const ev = this.props.mxEvent; + const ev = this.mxEvent; // event could not be decrypted if (ev.getContent().msgtype === 'm.bad.encrypted') { @@ -820,7 +846,7 @@ export default class EventTile extends React.Component { ) { return null; } - const eventId = this.props.mxEvent.getId(); + const eventId = this.mxEvent.getId(); return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); }; @@ -839,13 +865,13 @@ export default class EventTile extends React.Component { const SenderProfile = sdk.getComponent('messages.SenderProfile'); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - //console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); + //console.info("EventTile showUrlPreview for %s is %s", this.mxEvent.getId(), this.props.showUrlPreview); - const content = this.props.mxEvent.getContent(); + const content = this.mxEvent.getContent(); const msgtype = content.msgtype; - const eventType = this.props.mxEvent.getType(); + const eventType = this.mxEvent.getType(); - let tileHandler = getHandlerTile(this.props.mxEvent); + let tileHandler = getHandlerTile(this.mxEvent); // Info messages are basically information about commands processed on a room const isBubbleMessage = eventType.startsWith("m.key.verification") || @@ -862,7 +888,7 @@ export default class EventTile extends React.Component { // source tile when there's no regular tile for an event and also for // replace relations (which otherwise would display as a confusing // duplicate of the thing they are replacing). - if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) { + if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.mxEvent)) { tileHandler = "messages.ViewSourceEvent"; // Reuse info message avatar and sender profile styling isInfoMessage = true; @@ -881,8 +907,8 @@ export default class EventTile extends React.Component { const EventTileType = sdk.getComponent(tileHandler); const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1); - const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; - const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); + const isRedacted = isMessageEvent(this.mxEvent) && this.props.isRedacted; + const isEncryptionFailure = this.mxEvent.isDecryptionFailure(); const isEditing = !!this.props.editState; const classes = classNames({ @@ -912,14 +938,14 @@ export default class EventTile extends React.Component { let permalink = "#"; if (this.props.permalinkCreator) { - permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); + permalink = this.props.permalinkCreator.forEvent(this.mxEvent.getId()); } // we can't use local echoes as scroll tokens, because their event IDs change. // Local echos have a send "status". - const scrollToken = this.props.mxEvent.status + const scrollToken = this.mxEvent.status ? undefined - : this.props.mxEvent.getId(); + : this.mxEvent.getId(); let avatar; let sender; @@ -949,15 +975,15 @@ export default class EventTile extends React.Component { needsSenderProfile = true; } - if (this.props.mxEvent.sender && avatarSize) { + if (this.mxEvent.sender && avatarSize) { let member; // set member to receiver (target) if it is a 3PID invite // so that the correct avatar is shown as the text is // `$target accepted the invitation for $email` - if (this.props.mxEvent.getContent().third_party_invite) { - member = this.props.mxEvent.target; + if (this.mxEvent.getContent().third_party_invite) { + member = this.mxEvent.target; } else { - member = this.props.mxEvent.sender; + member = this.mxEvent.sender; } avatar = (
@@ -972,17 +998,17 @@ export default class EventTile extends React.Component { if (needsSenderProfile) { if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') { sender = ; } else { - sender = ; + sender = ; } } const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); const actionBar = !isEditing ? { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const showTimestamp = this.props.mxEvent.getTs() && + const showTimestamp = this.mxEvent.getTs() && (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused); const timestamp = showTimestamp ? - : null; + : null; const keyRequestHelpText =
@@ -1033,7 +1059,7 @@ export default class EventTile extends React.Component { if (!isRedacted) { const ReactionsRow = sdk.getComponent('messages.ReactionsRow'); reactionsRow = ; } @@ -1041,7 +1067,7 @@ export default class EventTile extends React.Component { const linkedTimestamp = { timestamp } ; @@ -1060,7 +1086,7 @@ export default class EventTile extends React.Component { switch (this.props.tileShape) { case 'notif': { - const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + const room = this.context.getRoom(this.mxEvent.getRoomId()); return React.createElement(this.props.as || "li", { "className": classes, "aria-live": ariaLive, @@ -1082,7 +1108,7 @@ export default class EventTile extends React.Component {
,
{ }, [
{ let thread; if (this.props.tileShape === 'reply_preview') { thread = ReplyThread.makeThread( - this.props.mxEvent, + this.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread, @@ -1150,7 +1176,7 @@ export default class EventTile extends React.Component { { groupPadlock } { thread } { } default: { const thread = ReplyThread.makeThread( - this.props.mxEvent, + this.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread, @@ -1190,7 +1216,7 @@ export default class EventTile extends React.Component { { groupPadlock } { thread }