From 925c1bdb331196e3375127a2dd3931e24f8c2133 Mon Sep 17 00:00:00 2001
From: lilia <liliakai@gmail.com>
Date: Thu, 14 Jan 2016 13:40:26 -0800
Subject: [PATCH] Add SyncRequest class

Similar in function to an xhr request, a textsecure.SyncRequest object
is initialized from a message sender and receiver pair and initiates a
request for sync from the master device. It later fires a success event
when both contacts and groups are done syncing, or a timeout event after
one minute.

// FREEBIE
---
 Gruntfile.js                  |   1 +
 js/background.js              |  17 +++--
 js/libtextsecure.js           | 119 ++++++++++++++++++++++++++++++++++
 js/options.js                 |   2 -
 libtextsecure/sync_request.js | 118 +++++++++++++++++++++++++++++++++
 5 files changed, 246 insertions(+), 11 deletions(-)
 create mode 100644 libtextsecure/sync_request.js

diff --git a/Gruntfile.js b/Gruntfile.js
index ec914d56d..ea0ec37ff 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -59,6 +59,7 @@ module.exports = function(grunt) {
           'libtextsecure/message_receiver.js',
           'libtextsecure/outgoing_message.js',
           'libtextsecure/sendmessage.js',
+          'libtextsecure/sync_request.js',
           'libtextsecure/contacts_parser.js',
         ],
         dest: 'js/libtextsecure.js',
diff --git a/js/background.js b/js/background.js
index f4d5510e4..8aa6e0dd8 100644
--- a/js/background.js
+++ b/js/background.js
@@ -103,21 +103,20 @@
         messageReceiver.addEventListener('sent', onSentMessage);
         messageReceiver.addEventListener('error', onError);
 
-        messageReceiver.addEventListener('contactsync', onContactSyncComplete);
-
         window.textsecure.messaging = new textsecure.MessageSender(SERVER_URL, USERNAME, PASSWORD, ATTACHMENT_SERVER_URL);
         if (firstRun === true && textsecure.storage.user.getDeviceId() != '1') {
-            textsecure.messaging.sendRequestContactSyncMessage().then(function() {
-                textsecure.messaging.sendRequestGroupSyncMessage();
+            var syncRequest = new textsecure.SyncRequest(textsecure.messaging, messageReceiver);
+            syncRequest.addEventListener('success', function() {
+                console.log('sync successful');
+                window.dispatchEvent(new Event('textsecure:contactsync'));
+            });
+            syncRequest.addEventListener('timeout', function() {
+                console.log('sync timed out');
+                window.dispatchEvent(new Event('textsecure:contactsync'));
             });
         }
     }
 
-    function onContactSyncComplete() {
-        console.log('Contact sync complete');
-        window.dispatchEvent(new Event('textsecure:contactsync'));
-    }
-
     function onContactReceived(ev) {
         var contactDetails = ev.contactDetails;
         ConversationController.create({
diff --git a/js/libtextsecure.js b/js/libtextsecure.js
index c9451fe1b..79fe86a64 100644
--- a/js/libtextsecure.js
+++ b/js/libtextsecure.js
@@ -37677,6 +37677,125 @@ textsecure.MessageSender.prototype = {
     constructor: textsecure.MessageSender
 };
 
+/*
+ * vim: ts=4:sw=4:expandtab
+ */
+
+
+;(function () {
+    'use strict';
+    window.textsecure = window.textsecure || {};
+
+    function SyncRequest(sender, receiver) {
+        this.receiver = receiver;
+
+        this.oncontact = this.onContactSyncComplete.bind(this);
+        receiver.addEventListener('contactsync', this.oncontact);
+
+        this.ongroup = this.onGroupSyncComplete.bind(this);
+        receiver.addEventListener('groupsync', this.ongroup);
+
+        sender.sendRequestContactSyncMessage().then(function() {
+            sender.sendRequestGroupSyncMessage();
+        });
+        this.timeout = setTimout(this.onTimeout.bind(this), 60000);
+    }
+
+    SyncRequest.prototype = {
+        constructor: SyncRequest,
+        onContactSyncComplete: function() {
+            this.contactSync = true;
+            this.update();
+        },
+        onGroupSyncComplete: function() {
+            this.groupSync = true;
+            this.update();
+        },
+        update: function() {
+            if (this.contactSync && this.groupSync) {
+                this.dispatchEvent(new Event('success'));
+                this.cleanup();
+            }
+        },
+        onTimeout: function() {
+            this.dispatchEvent(new Event('timeout'));
+            this.cleanup();
+        },
+        cleanup: function() {
+            clearTimeout(this.timeout);
+            this.receiver.removeEventListener('contactsync', this.oncontact);
+            this.receiver.removeEventListener('groupSync', this.ongroup);
+            delete this.listeners;
+        },
+
+        /* Implements EventTarget */  /// TODO: Dedupe this same code in MessageReceiver
+        dispatchEvent: function(ev) {
+            if (!(ev instanceof Event)) {
+                throw new Error('Expects an event');
+            }
+            if (this.listeners === null || typeof this.listeners !== 'object') {
+                this.listeners = {};
+            }
+            var listeners = this.listeners[ev.type];
+            if (typeof listeners === 'object') {
+                for (var i=0; i < listeners.length; ++i) {
+                    if (typeof listeners[i] === 'function') {
+                        listeners[i].call(null, ev);
+                    }
+                }
+            }
+        },
+        addEventListener: function(eventName, callback) {
+            if (typeof eventName !== 'string') {
+                throw new Error('First argument expects a string');
+            }
+            if (typeof callback !== 'function') {
+                throw new Error('Second argument expects a function');
+            }
+            if (this.listeners === null || typeof this.listeners !== 'object') {
+                this.listeners = {};
+            }
+            var listeners = this.listeners[eventName];
+            if (typeof listeners !== 'object') {
+                listeners = [];
+            }
+            listeners.push(callback);
+            this.listeners[eventName] = listeners;
+        },
+        removeEventListener: function(eventName, callback) {
+            if (typeof eventName !== 'string') {
+                throw new Error('First argument expects a string');
+            }
+            if (typeof callback !== 'function') {
+                throw new Error('Second argument expects a function');
+            }
+            if (this.listeners === null || typeof this.listeners !== 'object') {
+                this.listeners = {};
+            }
+            var listeners = this.listeners[eventName];
+            for (var i=0; i < listeners.length; ++ i) {
+                if (listeners[i] === callback) {
+                    listeners.splice(i, 1);
+                    return;
+                }
+            }
+            this.listeners[eventName] = listeners;
+        }
+    };
+
+    textsecure.SyncRequest = function(sender, receiver) {
+        var syncRequest = new SyncRequest(sender, receiver);
+        this.addEventListener    = syncRequest.addEventListener.bind(syncRequest);
+        this.removeEventListener = syncRequest.removeEventListener.bind(syncRequest);
+    };
+
+    textsecure.SyncRequest.prototype = {
+        constructor: textsecure.SyncRequest
+    };
+
+
+}());
+
 /*
  * vim: ts=4:sw=4:expandtab
  */
diff --git a/js/options.js b/js/options.js
index dd7c6dd58..b43401a16 100644
--- a/js/options.js
+++ b/js/options.js
@@ -38,10 +38,8 @@
                         var launch = function() {
                             bg.openInbox();
                             bg.removeEventListener('textsecure:contactsync', launch);
-                            clearTimeout(timeout);
                             window.close();
                         };
-                        var timeout = setTimeout(launch, 60000);
                         bg.addEventListener('textsecure:contactsync', launch);
                         view.showSync();
                     }).catch(function(e) {
diff --git a/libtextsecure/sync_request.js b/libtextsecure/sync_request.js
new file mode 100644
index 000000000..240eadac0
--- /dev/null
+++ b/libtextsecure/sync_request.js
@@ -0,0 +1,118 @@
+/*
+ * vim: ts=4:sw=4:expandtab
+ */
+
+
+;(function () {
+    'use strict';
+    window.textsecure = window.textsecure || {};
+
+    function SyncRequest(sender, receiver) {
+        this.receiver = receiver;
+
+        this.oncontact = this.onContactSyncComplete.bind(this);
+        receiver.addEventListener('contactsync', this.oncontact);
+
+        this.ongroup = this.onGroupSyncComplete.bind(this);
+        receiver.addEventListener('groupsync', this.ongroup);
+
+        sender.sendRequestContactSyncMessage().then(function() {
+            sender.sendRequestGroupSyncMessage();
+        });
+        this.timeout = setTimout(this.onTimeout.bind(this), 60000);
+    }
+
+    SyncRequest.prototype = {
+        constructor: SyncRequest,
+        onContactSyncComplete: function() {
+            this.contactSync = true;
+            this.update();
+        },
+        onGroupSyncComplete: function() {
+            this.groupSync = true;
+            this.update();
+        },
+        update: function() {
+            if (this.contactSync && this.groupSync) {
+                this.dispatchEvent(new Event('success'));
+                this.cleanup();
+            }
+        },
+        onTimeout: function() {
+            this.dispatchEvent(new Event('timeout'));
+            this.cleanup();
+        },
+        cleanup: function() {
+            clearTimeout(this.timeout);
+            this.receiver.removeEventListener('contactsync', this.oncontact);
+            this.receiver.removeEventListener('groupSync', this.ongroup);
+            delete this.listeners;
+        },
+
+        /* Implements EventTarget */  /// TODO: Dedupe this same code in MessageReceiver
+        dispatchEvent: function(ev) {
+            if (!(ev instanceof Event)) {
+                throw new Error('Expects an event');
+            }
+            if (this.listeners === null || typeof this.listeners !== 'object') {
+                this.listeners = {};
+            }
+            var listeners = this.listeners[ev.type];
+            if (typeof listeners === 'object') {
+                for (var i=0; i < listeners.length; ++i) {
+                    if (typeof listeners[i] === 'function') {
+                        listeners[i].call(null, ev);
+                    }
+                }
+            }
+        },
+        addEventListener: function(eventName, callback) {
+            if (typeof eventName !== 'string') {
+                throw new Error('First argument expects a string');
+            }
+            if (typeof callback !== 'function') {
+                throw new Error('Second argument expects a function');
+            }
+            if (this.listeners === null || typeof this.listeners !== 'object') {
+                this.listeners = {};
+            }
+            var listeners = this.listeners[eventName];
+            if (typeof listeners !== 'object') {
+                listeners = [];
+            }
+            listeners.push(callback);
+            this.listeners[eventName] = listeners;
+        },
+        removeEventListener: function(eventName, callback) {
+            if (typeof eventName !== 'string') {
+                throw new Error('First argument expects a string');
+            }
+            if (typeof callback !== 'function') {
+                throw new Error('Second argument expects a function');
+            }
+            if (this.listeners === null || typeof this.listeners !== 'object') {
+                this.listeners = {};
+            }
+            var listeners = this.listeners[eventName];
+            for (var i=0; i < listeners.length; ++ i) {
+                if (listeners[i] === callback) {
+                    listeners.splice(i, 1);
+                    return;
+                }
+            }
+            this.listeners[eventName] = listeners;
+        }
+    };
+
+    textsecure.SyncRequest = function(sender, receiver) {
+        var syncRequest = new SyncRequest(sender, receiver);
+        this.addEventListener    = syncRequest.addEventListener.bind(syncRequest);
+        this.removeEventListener = syncRequest.removeEventListener.bind(syncRequest);
+    };
+
+    textsecure.SyncRequest.prototype = {
+        constructor: textsecure.SyncRequest
+    };
+
+
+}());