瀏覽代碼

Merge pull request #856 from threema-ch/fix-create-message-race-conditions

Recurrent wakeup for unsent messages
Lennart Grahl 6 年之前
父節點
當前提交
56eb4393f2

+ 2 - 2
public/i18n/cs.json

@@ -205,8 +205,8 @@
         "SENT": "Zpráva byla doručena na Threema server",
         "DELIVERED": "Zpráva byla doručena do zařízení příjemce",
         "READ": "Zpráva byla přečtena příjemcem",
-        "FAILED": "Zpráva nemohla být odeslána",
-        "TIMEOUT": "Zpráva nemohla být přenesena do vašeho zařízení."},
+        "FAILED": "Zpráva nemohla být odeslána"
+    },
     "messageTypes": {
         "AUDIO_MESSAGE": "Zvuková zpráva",
         "FILE_MESSAGE": "Souborová zpráva",

+ 1 - 2
public/i18n/de.json

@@ -206,8 +206,7 @@
         "SENT": "Die Nachricht wurde erfolgreich an den Server übermittelt",
         "DELIVERED": "Die Nachricht ist beim Gerät des Empfängers angekommen",
         "READ": "Die Nachricht wurde vom Empfänger gelesen",
-        "FAILED": "Die Nachricht konnte nicht gesendet werden",
-        "TIMEOUT": "Die Nachricht konnte nicht auf Ihr Gerät übertragen werden."
+        "FAILED": "Die Nachricht konnte nicht gesendet werden"
     },
     "messageTypes": {
         "AUDIO_MESSAGE": "Sprachnachricht",

+ 0 - 1
public/i18n/en.json

@@ -207,7 +207,6 @@
         "DELIVERED": "The message was delivered to the recipient's device",
         "READ": "The message was read by the recipient",
         "FAILED": "The message could not be sent",
-        "TIMEOUT": "The message could not be transferred to your device.",
         "UNKNOWN": ""
     },
     "messageTypes": {

+ 2 - 2
public/i18n/es.json

@@ -205,8 +205,8 @@
         "SENT": "El mensaje se ha enviado al servidor de Threema",
         "DELIVERED": "El mensaje se ha enviado al dispositivo del destinatario",
         "READ": "El mensaje ha sido leído por el destinatario",
-        "FAILED": "No se pudo enviar el mensaje",
-        "TIMEOUT": "No se pudo transferir el mensaje a tu dispositivo."},
+        "FAILED": "No se pudo enviar el mensaje"
+    },
     "messageTypes": {
         "AUDIO_MESSAGE": "Mensaje de audio",
         "FILE_MESSAGE": "Archivo",

+ 2 - 2
public/i18n/fr.json

@@ -205,8 +205,8 @@
         "SENT": "Le message a été transmis au serveur Threema",
         "DELIVERED": "Le message a été transmis sur l'appareil du destinataire",
         "READ": "Le message a été lu par le destinataire",
-        "FAILED": "Le message n'a pas pu être envoyé",
-        "TIMEOUT": "Le message ne peut être transféré sur votre appareil."},
+        "FAILED": "Le message n'a pas pu être envoyé"
+    },
     "messageTypes": {
         "AUDIO_MESSAGE": "Message audio",
         "FILE_MESSAGE": "Fichier",

+ 2 - 2
public/i18n/nl.json

@@ -205,8 +205,8 @@
         "SENT": "Het bericht is afgeleverd op de Threema server",
         "DELIVERED": "Het bericht is afgeleverd op het apparaat van de ontvanger",
         "READ": "Het bericht is gelezen door de ontvanger",
-        "FAILED": "Het bericht kon niet worden verstuurd",
-        "TIMEOUT": "Het bericht kon niet worden afgeleverd naar je apparaat"},
+        "FAILED": "Het bericht kon niet worden verstuurd"
+    },
     "messageTypes": {
         "AUDIO_MESSAGE": "Audiobericht",
         "FILE_MESSAGE": "Bericht met bestand",

+ 2 - 2
public/i18n/pt.json

@@ -205,8 +205,8 @@
         "SENT": "A mensagem foi entregue ao servidor do Threema",
         "DELIVERED": "A mensagem foi entregue no dispositivo do destinatário",
         "READ": "A mensagem foi lida pelo destinatário",
-        "FAILED": "A mensagem não pode ser enviada",
-        "TIMEOUT": "Não foi possível transferir a mensagem para o seu dispositivo"},
+        "FAILED": "A mensagem não pode ser enviada"
+    },
     "messageTypes": {
         "AUDIO_MESSAGE": "Mensagem de som",
         "FILE_MESSAGE": "Mensagem com anexo",

+ 1 - 1
public/i18n/sk.json

@@ -206,7 +206,7 @@
         "DELIVERED": "Správa bola doručená na zariadenie príjemcu",
         "READ": "Správa bola prečítaná príjemcom",
         "FAILED": "Správa nemohla byť poslaná",
-        "TIMEOUT": "Správa nemôže byť prenesená do Vašeho zariadenia."},
+    },
     "messageTypes": {
         "AUDIO_MESSAGE": "Zvuková správa",
         "FILE_MESSAGE": "Súborová správa",

+ 2 - 2
public/i18n/uk.json

@@ -205,8 +205,8 @@
         "SENT": "Це повідомлення було доставлене на сервер Threema",
         "DELIVERED": "Це повідомлення було доставлене на пристрій адресата",
         "READ": "Це повідомлення було прочитане адресатом",
-        "FAILED": "Це повідомлення не може бути надіслане",
-        "TIMEOUT": "Не вдалося передати повідомлення на ваш пристрій."},
+        "FAILED": "Це повідомлення не може бути надіслане"
+    },
     "messageTypes": {
         "AUDIO_MESSAGE": "Аудіоповідомлення",
         "FILE_MESSAGE": "Файлове повідомлення",

+ 2 - 2
public/i18n/zh.json

@@ -205,8 +205,8 @@
         "SENT": "消息已经被发送到服务器",
         "DELIVERED": "消息已经送达对方的设备",
         "READ": "消息已读",
-        "FAILED": "消息无法发送",
-        "TIMEOUT": "消息无法发送到您的设备"},
+        "FAILED": "消息无法发送"
+    },
     "messageTypes": {
         "AUDIO_MESSAGE": "音频消息",
         "FILE_MESSAGE": "文件消息",

+ 0 - 4
src/filters.ts

@@ -238,8 +238,6 @@ angular.module('3ema.filters', [])
                 return 'thumb_up';
             case 'user-dec':
                 return 'thumb_down';
-            case 'timeout':
-                return 'sync_problem';
             default:
                 return '';
         }
@@ -282,8 +280,6 @@ angular.module('3ema.filters', [])
                 return 'messageStates.USER_ACK';
             case 'user-dec':
                 return 'messageStates.USER_DEC';
-            case 'timeout':
-                return 'messageStates.TIMEOUT';
             default:
                 return 'messageStates.UNKNOWN';
         }

+ 14 - 13
src/services/message.ts

@@ -228,19 +228,20 @@ export class MessageService {
             default:
                 throw new Error(`Cannot create temporary message for type: ${type}`);
         }
-
-        // Add delay for timeout checking
-        // TODO: This should be removed once Android has reliable message delivery.
-        this.timeoutService.register(() => {
-            // Set the state to timeout if it is still pending.
-            // Note: If sending the message worked, by now the message object
-            // will have been replaced by a new one and the state change would
-            // have no effect anyways...
-            if (message.state === 'pending') {
-                message.state = 'timeout';
-            }
-        }, this.timeoutDelaySeconds * 1000, true, 'messageTimeout');
-
         return message;
     }
+
+    /**
+     * Return whether the app has attempted to send this message to the server
+     * (successful or not).
+     */
+    public isSentOrSendingFailed(message: threema.Message): boolean {
+        switch (message.state) {
+            case 'pending':
+            case 'sending':
+                return false;
+            default:
+                return true;
+        }
+    }
 }

+ 44 - 22
src/services/webclient.ts

@@ -2583,18 +2583,18 @@ export class WebClientService {
         this._receiveReplyReceiver(message, 'distributionList', future);
     }
 
-    private _receiveCreateMessage(message: threema.WireMessage): void {
+    private _receiveCreateMessage(wireMessage: threema.WireMessage): void {
         this.arpLog.debug('Received create message response');
-        const future = this.popWireMessageFuture(message);
+        const future = this.popWireMessageFuture(wireMessage);
 
         // Handle error (if any)
-        if (!message.ack.success) {
-            return future.reject(message.ack.error);
+        if (!wireMessage.ack.success) {
+            return future.reject(wireMessage.ack.error);
         }
 
         // Unpack data and arguments
-        const args = message.args;
-        const data = message.data;
+        const args = wireMessage.args;
+        const data = wireMessage.data;
         if (args === undefined || data === undefined) {
             this.arpLog.warn('Invalid create message received, arguments or data missing');
             return future.reject('invalidResponse');
@@ -2613,12 +2613,22 @@ export class WebClientService {
             type: receiverType,
             id: receiverId,
         } as threema.Receiver;
-        this.messages.bindTemporaryToMessageId(
+        const message = this.messages.bindTemporaryToMessageId(
             receiver,
-            message.ack.id,
+            wireMessage.ack.id,
             messageId,
         );
         future.resolve(messageId);
+
+        // Add a special future that resolves once the message has been
+        // identified as sent. As long as an unacknowledged wire message future
+        // exists, the app will be continuously awoken if the connection
+        // has been lost.
+        if (!this.messageService.isSentOrSendingFailed(message)) {
+            const sentId = `${message.id}-sent`;
+            this.wireMessageFutures.set(sentId, new Future());
+            this.arpLogV.debug(`Added special wire message future: ${sentId}`);
+        }
     }
 
     private _receiveResponseConversations(message: threema.WireMessage) {
@@ -2863,18 +2873,18 @@ export class WebClientService {
         }
     }
 
-    private _receiveUpdateMessages(message: threema.WireMessage): void {
+    private _receiveUpdateMessages(wireMessage: threema.WireMessage): void {
         this.arpLog.debug('Received messages update');
-        const future = this.popWireMessageFuture(message, true);
+        const future = this.popWireMessageFuture(wireMessage, true);
 
         // Handle error (if any)
-        if (message.ack !== undefined && !message.ack.success) {
-            return future.reject(message.ack.error);
+        if (wireMessage.ack !== undefined && !wireMessage.ack.success) {
+            return future.reject(wireMessage.ack.error);
         }
 
         // Unpack data and arguments
-        const args = message.args;
-        const data: threema.Message[] = message.data;
+        const args = wireMessage.args;
+        const data: threema.Message[] = wireMessage.data;
         if (args === undefined || data === undefined) {
             this.arpLog.warn('Invalid message update, data or arguments missing');
             return future.reject('invalidResponse');
@@ -2893,19 +2903,31 @@ export class WebClientService {
             return future.reject('invalidResponse');
         }
         if (this.config.ARP_LOG_TRACE) {
-            this.logChatMessages(message.type, message.subType, type, id, mode, data);
+            this.logChatMessages(wireMessage.type, wireMessage.subType, type, id, mode, data);
         }
         const receiver: threema.BaseReceiver = {type: type, id: id};
 
         // React depending on mode
         let notify = false;
-        for (const msg of data) {
+        for (const message of data) {
+            // Pop special future to be resolved if the message has been
+            // identified as sent.
+            if (this.messageService.isSentOrSendingFailed(message)) {
+                const sentId = `${message.id}-sent`;
+                const sentFuture = this.wireMessageFutures.get(sentId);
+                if (sentFuture !== undefined) {
+                    this.wireMessageFutures.delete(sentId);
+                    this.arpLogV.debug(`Removed special wire message future: ${sentId}`);
+                    sentFuture.resolve();
+                }
+            }
+
             switch (mode) {
                 case WebClientService.ARGUMENT_MODE_NEW:
                     // It's possible that this message already exists (placeholder message on send).
                     // Try to update it first. If not, add it as a new msg.
-                    if (!this.messages.update(receiver, msg)) {
-                        this.messages.addNewer(receiver, [msg]);
+                    if (!this.messages.update(receiver, message)) {
+                        this.messages.addNewer(receiver, [message]);
 
                         // If we have received a new message, it is highly unlikely that the contact is still typing
                         this.typing.unsetTyping(receiver);
@@ -2913,8 +2935,8 @@ export class WebClientService {
                     notify = true;
                     break;
                 case WebClientService.ARGUMENT_MODE_MODIFIED:
-                    if (!this.messages.update(receiver, msg)) {
-                        const log = `Received message update for unknown message (id ${msg.id})`;
+                    if (!this.messages.update(receiver, message)) {
+                        const log = `Received message update for unknown message (id ${message.id})`;
                         this.arpLog.error(log);
                         if (this.config.ARP_LOG_TRACE) {
                             this.messages.addStatusMessage(receiver, 'Warning: ' + log);
@@ -2923,8 +2945,8 @@ export class WebClientService {
                     }
                     break;
                 case WebClientService.ARGUMENT_MODE_REMOVED:
-                    if (!this.messages.remove(receiver, msg.id)) {
-                        this.arpLog.error(`Received message deletion for unknown message (id ${msg.id})`);
+                    if (!this.messages.remove(receiver, message.id)) {
+                        this.arpLog.error(`Received message deletion for unknown message (id ${message.id})`);
                     }
                     notify = true;
                     break;

+ 3 - 3
src/threema.d.ts

@@ -59,8 +59,8 @@ declare namespace threema {
 
     type MessageType = 'text' | 'image' | 'video' | 'audio' | 'location' | 'contact' |
                        'status' | 'ballot' | 'file' | 'voipStatus' | 'unknown';
-    type MessageState = 'delivered' | 'read' | 'send-failed' | 'sent' | 'user-ack' |
-                        'user-dec' | 'pending' | 'timeout' | 'sending';
+    type MessageState = 'pending' | 'sending' | 'send-failed' | 'sent' | 'delivered' |
+                        'read' | 'user-ack' | 'user-dec';
 
     const enum InitializationStep {
         ClientInfo = 'client info',
@@ -874,7 +874,7 @@ declare namespace threema {
             setThumbnail(receiver: BaseReceiver, messageId: string, thumbnailImage: ArrayBuffer): boolean;
             remove(receiver: BaseReceiver, messageId: string): boolean;
             removeTemporary(receiver: BaseReceiver, temporaryMessageId: string): boolean;
-            bindTemporaryToMessageId(receiver: BaseReceiver, temporaryId: string, messageId: string): boolean;
+            bindTemporaryToMessageId(receiver: BaseReceiver, temporaryId: string, messageId: string): Message | null;
             notify(receiver: BaseReceiver, $scope: ng.IScope): void;
             register(receiver: BaseReceiver, $scope: ng.IScope, callback: any): Message[];
             updateFirstUnreadMessage(receiver: BaseReceiver);

+ 19 - 9
src/threema/container.ts

@@ -694,6 +694,7 @@ class Messages implements threema.Container.Messages {
         }
         return false;
     }
+
     /**
      * Remove a message.
      *
@@ -711,24 +712,33 @@ class Messages implements threema.Container.Messages {
         return false;
     }
 
-    public bindTemporaryToMessageId(receiver: threema.BaseReceiver, temporaryId: string, messageId: string): boolean {
+    /**
+     * Look up a message with a specific temporary id. If it has been found,
+     * replaces the temporary id with the message id and returns the message
+     * instance.
+     */
+    public bindTemporaryToMessageId(
+        receiver: threema.BaseReceiver,
+        temporaryId: string,
+        messageId: string,
+    ): threema.Message | null {
         const list = this.getList(receiver);
-        for (const item of list) {
-            if (item.temporaryId === temporaryId) {
-                if (item.id !== undefined) {
+        for (const message of list) {
+            if (message.temporaryId === temporaryId) {
+                if (message.id !== undefined) {
                     // do not bind to a new message id
-                    return false;
+                    return message;
                 }
 
                 // reset temporary id
-                item.temporaryId = null;
+                message.temporaryId = null;
 
                 // assign to "real" message id
-                item.id = messageId;
-                return true;
+                message.id = messageId;
+                return message;
             }
         }
-        return false;
+        return null;
     }
 
     /**