diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 6e17a1b76..14c82c678 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -165,6 +165,7 @@ export class SessionCompositionBox extends React.Component { this.onChooseAttachment = this.onChooseAttachment.bind(this); this.onClickAttachment = this.onClickAttachment.bind(this); this.renderCaptionEditor = this.renderCaptionEditor.bind(this); + this.abortLinkPreviewFetch = this.abortLinkPreviewFetch.bind(this); // On Sending this.onSendMessage = this.onSendMessage.bind(this); @@ -183,7 +184,7 @@ export class SessionCompositionBox extends React.Component { } public componentWillUnmount() { - this.linkPreviewAbortController?.abort(); + this.abortLinkPreviewFetch(); this.linkPreviewAbortController = undefined; } public componentDidUpdate(prevProps: Props, _prevState: State) { @@ -566,7 +567,7 @@ export class SessionCompositionBox extends React.Component { }, }); const abortController = new AbortController(); - this.linkPreviewAbortController?.abort(); + this.abortLinkPreviewFetch(); this.linkPreviewAbortController = abortController; setTimeout(() => { abortController.abort(); @@ -590,31 +591,64 @@ export class SessionCompositionBox extends React.Component { } } } - this.setState({ - stagedLinkPreview: { - isLoaded: true, - title: ret?.title || null, - description: ret?.description || '', - url: ret?.url || null, - domain: - (ret?.url && window.Signal.LinkPreviews.getDomain(ret.url)) || '', - image, - }, - }); + // we finished loading the preview, and checking the abortConrtoller, we are still not aborted. + // => update the staged preview + if ( + this.linkPreviewAbortController && + !this.linkPreviewAbortController.signal.aborted + ) { + this.setState({ + stagedLinkPreview: { + isLoaded: true, + title: ret?.title || null, + description: ret?.description || '', + url: ret?.url || null, + domain: + (ret?.url && window.Signal.LinkPreviews.getDomain(ret.url)) || + '', + image, + }, + }); + } else if (this.linkPreviewAbortController) { + this.setState({ + stagedLinkPreview: { + isLoaded: false, + title: null, + description: null, + url: null, + domain: null, + image: undefined, + }, + }); + this.linkPreviewAbortController = undefined; + } }) .catch(err => { window.log.warn('fetch link preview: ', err); - abortController.abort(); - this.setState({ - stagedLinkPreview: { - isLoaded: true, - title: null, - domain: null, - description: null, - url: firstLink, - image: undefined, - }, - }); + const aborted = this.linkPreviewAbortController?.signal.aborted; + this.linkPreviewAbortController = undefined; + // if we were aborted, it either means the UI was unmount, or more probably, + // than the message was sent without the link preview. + // So be sure to reset the staged link preview so it is not sent with the next message. + + // if we were not aborted, it's probably just an error on the fetch. Nothing to do excpet mark the fetch as done (with errors) + + if (aborted) { + this.setState({ + stagedLinkPreview: undefined, + }); + } else { + this.setState({ + stagedLinkPreview: { + isLoaded: true, + title: null, + description: null, + url: firstLink, + domain: null, + image: undefined, + }, + }); + } }); } @@ -751,6 +785,8 @@ export class SessionCompositionBox extends React.Component { // tslint:disable-next-line: cyclomatic-complexity private async onSendMessage() { + this.abortLinkPreviewFetch(); + // this is dirty but we have to replace all @(xxx) by @xxx manually here const cleanMentions = (text: string): string => { const matches = text.match(this.mentionsRegex); @@ -835,10 +871,13 @@ export class SessionCompositionBox extends React.Component { 'attachments' ); + // we consider that a link previews without a title at least is not a preview const linkPreviews = - (stagedLinkPreview && [ - _.pick(stagedLinkPreview, 'url', 'image', 'title'), - ]) || + (stagedLinkPreview && + stagedLinkPreview.isLoaded && + stagedLinkPreview.title?.length && [ + _.pick(stagedLinkPreview, 'url', 'image', 'title'), + ]) || []; try { @@ -854,20 +893,15 @@ export class SessionCompositionBox extends React.Component { // Message sending sucess this.props.onMessageSuccess(); + this.props.clearAttachments(); - // Empty composition box + // Empty composition box and stagedAttachments this.setState({ message: '', showEmojiPanel: false, + stagedLinkPreview: undefined, + ignoredLink: undefined, }); - // Empty stagedAttachments - this.props.clearAttachments(); - if (stagedLinkPreview && stagedLinkPreview.url) { - this.setState({ - stagedLinkPreview: undefined, - ignoredLink: undefined, - }); - } } catch (e) { // Message sending failed window.log.error(e); @@ -983,4 +1017,8 @@ export class SessionCompositionBox extends React.Component { // Focus the textarea when user clicks anywhere in the composition box this.textarea.current?.focus(); } + + private abortLinkPreviewFetch() { + this.linkPreviewAbortController?.abort(); + } } diff --git a/ts/session/utils/Promise.ts b/ts/session/utils/Promise.ts index 237938e77..506d0ef51 100644 --- a/ts/session/utils/Promise.ts +++ b/ts/session/utils/Promise.ts @@ -131,3 +131,11 @@ export async function timeout( return Promise.race([timeoutPromise, promise]); } + +export async function delay(timeoutMs: number = 2000): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve(true); + }, timeoutMs); + }); +} diff --git a/ts/util/linkPreviewFetch.ts b/ts/util/linkPreviewFetch.ts index 2f3900247..6193db7b0 100644 --- a/ts/util/linkPreviewFetch.ts +++ b/ts/util/linkPreviewFetch.ts @@ -9,6 +9,7 @@ import { IMAGE_WEBP, MIMEType, } from '../types/MIME'; +import { PromiseUtils } from '../session/utils'; const MAX_REQUEST_COUNT_WITH_REDIRECTS = 20; // tslint:disable: prefer-for-of