diff --git a/src/views/messages-view.jsx b/src/views/messages-view.jsx index d4b919dd..7664e09a 100644 --- a/src/views/messages-view.jsx +++ b/src/views/messages-view.jsx @@ -1654,6 +1654,23 @@ class MessagesView extends React.Component { const RISK_RANK = {block: 2, warn: 1, allow: 0}; const urls = Array.from(new Set(urlMatches)); return Promise.all(urls.map(url => checkURL(url, 'message_compose'))).then(results => { + // The user may have already switched to a different conversation while checkURL was + // still in flight. getDerivedStateFromProps only protects against an ALREADY-shown + // dialog surviving a later topic switch (it nulls riskDialog when nextProps.topic != + // prevState.topic) -- it can't protect against this .then() running AFTER that switch, + // which would otherwise setState({riskDialog:{...}}) fresh right here, popping the risk + // dialog up while a completely different conversation is now open. doSend() still calls + // this.props.sendMessage using whatever topic is current AT CONFIRM TIME (see doSend's + // own comment above), not the one this message was actually typed in, so confirming that + // dialog would silently deliver this message into the wrong conversation. Confirmed live: + // typed a flagged URL in topic A, switched to topic B before checkURL resolved, and the + // resulting dialog rendered while viewing topic B -- confirming it delivered topic A's + // message into topic B instead. If the topic has moved on, treat this message the same + // as if it were abandoned -- no dialog, no send -- the same "lost, but not hung forever" + // outcome already accepted just below for two flagged messages sent in quick succession. + if (this.state.topic !== topicForDraft) { + return false; + } const result = results.reduce((worst, r) => (RISK_RANK[r.decision] || 0) > (RISK_RANK[worst.decision] || 0) ? r : worst); if (result.decision === 'allow') { return doSend();