Improve special read receipt checking

See comments in code
This commit is contained in:
Travis Ralston 2021-03-01 16:21:04 -07:00
parent 9cec382865
commit db8978580c

View file

@ -264,6 +264,79 @@ export default class EventTile extends React.Component {
this._tile = createRef(); this._tile = createRef();
this._replyThread = createRef(); this._replyThread = createRef();
// Throughout the component we manage a read receipt listener to see if our tile still
// qualifies for a "sent" or "sending" state (based on their relevant conditions). We
// don't want to over-subscribe to the read receipt events being fired, so we use a flag
// to determine if we've already subscribed and use a combination of other flags to find
// out if we should even be subscribed at all.
this._isListeningForReceipts = false;
}
/**
* When true, the tile qualifies for some sort of special read receipt. This could be a 'sending'
* or 'sent' receipt, for example.
* @returns {boolean}
* @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;
// Sanity check (should never happen, but we shouldn't explode if it does)
const room = this.context.getRoom(this.props.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;
// 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
// those we rely on local echo giving the impression of things changing, and expect them
// to be quick.
const simpleSendableEvents = [EventType.Sticker, EventType.RoomMessage, EventType.RoomMessageEncrypted];
if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false;
// Default case
return true;
}
get _shouldShowSentReceipt() {
// If we're not even eligible, don't show the receipt.
if (!this._isEligibleForSpecialReceipt) return false;
// Check to make sure the sending state is appropriate. A null/undefined send status means
// that the message is 'sent', so we're just double checking that it's explicitly not sent.
if (this.props.eventSendStatus && this.props.eventSendStatus !== 'sent') return false;
// No point in doing the complex math if we're not going to even show this special receipt.
if (this._shouldShowSendingReceipt) return false;
// Next we check to see if any newer events have read receipts. If they do then we don't
// show our special state - the user already has feedback about their message. We only
// search for the most recent 50 events because surely someone will have sent *something*
// in that time, even if it is a membership event or something.
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
const myUserId = MatrixClientPeg.get().getUserId();
const readUsers = room.getUsersWhoHaveRead(this.props.mxEvent, 50);
const hasBeenRead = readUsers.length === 0 || readUsers.some(u => u !== myUserId);
return !hasBeenRead;
}
get _shouldShowSendingReceipt() {
// If we're not even eligible, don't show the receipt.
if (!this._isEligibleForSpecialReceipt) return false;
// Check the event send status to see if we are pending. Null/undefined status means the
// message was sent, so check for that and 'sent' explicitly.
if (!this.props.eventSendStatus || this.props.eventSendStatus === 'sent') return false;
// Default to showing - there's no other event properties/behaviours we care about at
// this point.
return true;
} }
// TODO: [REACT-WARNING] Move into constructor // TODO: [REACT-WARNING] Move into constructor
@ -281,6 +354,11 @@ export default class EventTile extends React.Component {
if (this.props.showReactions) { if (this.props.showReactions) {
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated); this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
} }
if (this._shouldShowSentReceipt || this._shouldShowSendingReceipt) {
client.on("Room.receipt", this._onRoomReceipt);
this._isListeningForReceipts = true;
}
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@ -305,12 +383,40 @@ export default class EventTile extends React.Component {
const client = this.context; const client = this.context;
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
client.removeListener("Room.receipt", this._onRoomReceipt);
this._isListeningForReceipts = false;
this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted);
if (this.props.showReactions) { if (this.props.showReactions) {
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated); this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
} }
} }
componentDidUpdate(prevProps, prevState, snapshot) {
// If we're not listening for receipts and expect to be, register a listener.
if (!this._isListeningForReceipts && (this._shouldShowSentReceipt || this._shouldShowSendingReceipt)) {
this.context.on("Room.receipt", this._onRoomReceipt);
this._isListeningForReceipts = true;
}
}
_onRoomReceipt = (ev, room) => {
// ignore events for other rooms
const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
if (room !== tileRoom) return;
if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt && !this._isListeningForReceipts) {
return;
}
this.forceUpdate(() => {
// Per elsewhere in this file, we can remove the listener once we will have no further purpose for it.
if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt) {
this.context.removeListener("Room.receipt", this._onRoomReceipt);
this._isListeningForReceipts = false;
}
});
};
/** called when the event is decrypted after we show it. /** called when the event is decrypted after we show it.
*/ */
_onDecrypted = () => { _onDecrypted = () => {
@ -454,26 +560,15 @@ export default class EventTile extends React.Component {
}; };
getReadAvatars() { getReadAvatars() {
// return early if there are no read receipts, with our message state if applicable if (this._shouldShowSentReceipt) {
return <span className="mx_EventTile_readAvatars"><span className='mx_EventTile_receiptSent' /></span>;
}
if (this._shouldShowSendingReceipt) {
return <span className="mx_EventTile_readAvatars"><span className='mx_EventTile_receiptSending' /></span>;
}
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) { if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
const myUserId = MatrixClientPeg.get().getUserId();
if (this.props.mxEvent.getSender() === myUserId && room) {
// We only search for the most recent 50 events because surely someone will have
// sent *something* in that time, even if it is a membership event or something.
const readUsers = room.getUsersWhoHaveRead(this.props.mxEvent, 50);
const hasBeenRead = readUsers.length === 0 || readUsers.some(u => u !== myUserId);
console.log(room.getUsersReadUpTo(this.props.mxEvent));
let receipt = null;
if (!this.props.eventSendStatus || this.props.eventSendStatus === 'sent') {
if (!hasBeenRead) {
receipt = <span className='mx_EventTile_receiptSent' />;
}
} else {
receipt = <span className='mx_EventTile_receiptSending' />;
}
return <span className="mx_EventTile_readAvatars">{receipt}</span>;
}
return (<span className="mx_EventTile_readAvatars" />); return (<span className="mx_EventTile_readAvatars" />);
} }