diff --git a/js/background.js b/js/background.js index 283365e64..73a18ae64 100644 --- a/js/background.js +++ b/js/background.js @@ -106,8 +106,6 @@ window.document.title = window.getTitle(); - // start a background worker for ecc - textsecure.startWorker('js/libsignal-protocol-worker.js'); let messageReceiver; Whisper.events = _.clone(Backbone.Events); Whisper.events.isListenedTo = eventName => diff --git a/js/models/conversations.js b/js/models/conversations.js index a16f2fe50..80de7e568 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1360,7 +1360,11 @@ let title = i18n('delete'); let message = i18n('deleteContactConfirmation'); - if (this.isGroup()) { + if ( + this.isGroup() && + !this.get('left') && + !this.get('isKickedFromGroup') + ) { title = i18n('leaveGroup'); message = i18n('leaveGroupConfirmation'); } diff --git a/libtextsecure/libsignal-protocol.js b/libtextsecure/libsignal-protocol.js index f34a981a1..dfda09ecc 100644 --- a/libtextsecure/libsignal-protocol.js +++ b/libtextsecure/libsignal-protocol.js @@ -25316,77 +25316,6 @@ })(); - ; (function () { - - 'use strict'; - - // I am the...workee? - var origCurve25519 = Internal.curve25519_async; - - Internal.startWorker = function (url) { - Internal.stopWorker(); // there can be only one - - Internal.curve25519_async = new Curve25519Worker(url); - Internal.Curve.async = Internal.wrapCurve25519(Internal.curve25519_async); - libsignal.Curve.async = Internal.wrapCurve(Internal.Curve.async); - }; - - Internal.stopWorker = function () { - if (Internal.curve25519_async instanceof Curve25519Worker) { - var worker = Internal.curve25519_async.worker; - - Internal.curve25519_async = origCurve25519; - Internal.Curve.async = Internal.wrapCurve25519(Internal.curve25519_async); - libsignal.Curve.async = Internal.wrapCurve(Internal.Curve.async); - - worker.terminate(); - } - }; - - libsignal.worker = { - startWorker: Internal.startWorker, - stopWorker: Internal.stopWorker, - }; - - function Curve25519Worker(url) { - this.jobs = {}; - this.jobId = 0; - this.worker = new Worker(url); - this.worker.onmessage = function (e) { - var job = this.jobs[e.data.id]; - if (e.data.error && typeof job.onerror === 'function') { - job.onerror(new Error(e.data.error)); - } else if (typeof job.onsuccess === 'function') { - job.onsuccess(e.data.result); - } - delete this.jobs[e.data.id]; - }.bind(this); - } - - Curve25519Worker.prototype = { - constructor: Curve25519Worker, - postMessage: function (methodName, args, onsuccess, onerror) { - return new Promise(function (resolve, reject) { - this.jobs[this.jobId] = { onsuccess: resolve, onerror: reject }; - this.worker.postMessage({ id: this.jobId, methodName: methodName, args: args }); - this.jobId++; - }.bind(this)); - }, - keyPair: function (privKey) { - return this.postMessage('keyPair', [privKey]); - }, - sharedSecret: function (pubKey, privKey) { - return this.postMessage('sharedSecret', [pubKey, privKey]); - }, - sign: function (privKey, message) { - return this.postMessage('sign', [privKey, message]); - }, - verify: function (pubKey, message, sig) { - return this.postMessage('verify', [pubKey, message, sig]); - } - }; - - })(); /* Copyright 2013 Daniel Wirtz diff --git a/libtextsecure/protocol_wrapper.js b/libtextsecure/protocol_wrapper.js index 1846f26b9..73391ae4e 100644 --- a/libtextsecure/protocol_wrapper.js +++ b/libtextsecure/protocol_wrapper.js @@ -1,11 +1,8 @@ -/* global window, textsecure, SignalProtocolStore, libsignal */ +/* global window, textsecure, SignalProtocolStore */ // eslint-disable-next-line func-names (function() { window.textsecure = window.textsecure || {}; window.textsecure.storage = window.textsecure.storage || {}; textsecure.storage.protocol = new SignalProtocolStore(); - - textsecure.startWorker = libsignal.worker.startWorker; - textsecure.stopWorker = libsignal.worker.stopWorker; })(); 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/components/session/menu/ConversationHeaderMenu.tsx b/ts/components/session/menu/ConversationHeaderMenu.tsx index caa274622..9dcd0a37e 100644 --- a/ts/components/session/menu/ConversationHeaderMenu.tsx +++ b/ts/components/session/menu/ConversationHeaderMenu.tsx @@ -125,6 +125,8 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { isMe, isGroup, isPublic, + left, + isKickedFromGroup, onDeleteContact, window.i18n )} diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index f18bb260d..7c3eadfb5 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -92,6 +92,8 @@ export const ConversationListItemContextMenu = ( isMe, type === 'group', isPublic, + left, + isKickedFromGroup, onDeleteContact, window.i18n )} diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 87072aad1..25b83ae7d 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -39,9 +39,12 @@ function showCopyId(isPublic: boolean, isGroup: boolean): boolean { function showDeleteContact( isMe: boolean, isGroup: boolean, - isPublic: boolean + isPublic: boolean, + isGroupLeft: boolean, + isKickedFromGroup: boolean ): boolean { - return !isMe && Boolean(!isGroup || isPublic); + // you need to have left a closed group first to be able to delete it completely. + return (!isMe && !isGroup) || (isGroup && (isGroupLeft || isKickedFromGroup)); } function showAddModerators( @@ -97,10 +100,20 @@ export function getDeleteContactMenuItem( isMe: boolean | undefined, isGroup: boolean | undefined, isPublic: boolean | undefined, + isLeft: boolean | undefined, + isKickedFromGroup: boolean | undefined, action: any, i18n: LocalizerType ): JSX.Element | null { - if (showDeleteContact(Boolean(isMe), Boolean(isGroup), Boolean(isPublic))) { + if ( + showDeleteContact( + Boolean(isMe), + Boolean(isGroup), + Boolean(isPublic), + Boolean(isLeft), + Boolean(isKickedFromGroup) + ) + ) { if (isPublic) { return {i18n('leaveGroup')}; } 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 diff --git a/ts/util/lint/linter.ts b/ts/util/lint/linter.ts index 48f5e31b7..e8b939cb7 100644 --- a/ts/util/lint/linter.ts +++ b/ts/util/lint/linter.ts @@ -64,7 +64,6 @@ const excludedFiles = [ '^test/test.js', // From libsignal-protocol-javascript project - '^js/libsignal-protocol-worker.js', '^libtextsecure/libsignal-protocol.js', // Test files