/** * @provides conpherence-thread-manager * @requires javelin-dom * javelin-util * javelin-stratcom * javelin-install * javelin-aphlict * javelin-workflow * javelin-router * javelin-behavior-device * javelin-vector */ JX.install('ConpherenceThreadManager', { construct : function() { if (__DEV__) { if (JX.ConpherenceThreadManager._instance) { JX.$E('ConpherenceThreadManager object is a singleton.'); } } JX.ConpherenceThreadManager._instance = this; return this; }, members: { _loadThreadURI: null, _loadedThreadID: null, _loadedThreadPHID: null, _latestTransactionID: null, _transactionIDMap: null, _transactionCache: null, _canEditLoadedThread: null, _updating: null, _messagesRootCallback: JX.bag, _willLoadThreadCallback: JX.bag, _didLoadThreadCallback: JX.bag, _didUpdateThreadCallback: JX.bag, _willSendMessageCallback: JX.bag, _didSendMessageCallback: JX.bag, _willUpdateWorkflowCallback: JX.bag, _didUpdateWorkflowCallback: JX.bag, setLoadThreadURI: function(uri) { this._loadThreadURI = uri; return this; }, getLoadThreadURI: function() { return this._loadThreadURI; }, isThreadLoaded: function() { return Boolean(this._loadedThreadID); }, isThreadIDLoaded: function(thread_id) { return this._loadedThreadID == thread_id; }, getLoadedThreadID: function() { return this._loadedThreadID; }, setLoadedThreadID: function(id) { this._loadedThreadID = id; return this; }, getLoadedThreadPHID: function() { return this._loadedThreadPHID; }, setLoadedThreadPHID: function(phid) { this._loadedThreadPHID = phid; return this; }, getLatestTransactionID: function() { return this._latestTransactionID; }, setLatestTransactionID: function(id) { this._latestTransactionID = id; return this; }, _updateTransactionIDMap: function(transactions) { var loaded_id = this.getLoadedThreadID(); if (!this._transactionIDMap[loaded_id]) { this._transactionIDMap[this._loadedThreadID] = {}; } var loaded_transaction_ids = this._transactionIDMap[loaded_id]; var transaction; for (var ii = 0; ii < transactions.length; ii++) { transaction = transactions[ii]; loaded_transaction_ids[JX.Stratcom.getData(transaction).id] = 1; } this._transactionIDMap[this._loadedThreadID] = loaded_transaction_ids; return this; }, _updateTransactionCache: function(transactions) { var transaction; for (var ii = 0; ii < transactions.length; ii++) { transaction = transactions[ii]; this._transactionCache[JX.Stratcom.getData(transaction).id] = transaction; } return this; }, _getLoadedTransactions: function() { var loaded_id = this.getLoadedThreadID(); var loaded_tx_ids = JX.keys(this._transactionIDMap[loaded_id]); loaded_tx_ids.sort(function (a, b) { var x = parseFloat(a); var y = parseFloat(b); if (x > y) { return 1; } if (x < y) { return -1; } return 0; }); var transactions = []; for (var ii = 0; ii < loaded_tx_ids.length; ii++) { transactions.push(this._transactionCache[loaded_tx_ids[ii]]); } return transactions; }, _deleteTransactionCaches: function(id) { delete this._transactionCache[id]; delete this._transactionIDMap[this._loadedThreadID][id]; return this; }, setCanEditLoadedThread: function(bool) { this._canEditLoadedThread = bool; return this; }, getCanEditLoadedThread: function() { if (this._canEditLoadedThread === null) { return false; } return this._canEditLoadedThread; }, setMessagesRootCallback: function(callback) { this._messagesRootCallback = callback; return this; }, setWillLoadThreadCallback: function(callback) { this._willLoadThreadCallback = callback; return this; }, setDidLoadThreadCallback: function(callback) { this._didLoadThreadCallback = callback; return this; }, setDidUpdateThreadCallback: function(callback) { this._didUpdateThreadCallback = callback; return this; }, setWillSendMessageCallback: function(callback) { this._willSendMessageCallback = callback; return this; }, setDidSendMessageCallback: function(callback) { this._didSendMessageCallback = callback; return this; }, setWillUpdateWorkflowCallback: function(callback) { this._willUpdateWorkflowCallback = callback; return this; }, setDidUpdateWorkflowCallback: function(callback) { this._didUpdateWorkflowCallback = callback; return this; }, _getParams: function(base_params) { if (this._latestTransactionID) { base_params.latest_transaction_id = this._latestTransactionID; } return base_params; }, start: function() { this._transactionIDMap = {}; this._transactionCache = {}; JX.Stratcom.listen( 'aphlict-server-message', null, JX.bind(this, function(e) { var message = e.getData(); if (message.type != 'message') { // Not a message event. return; } if (message.threadPHID != this._loadedThreadPHID) { // Message event for some thread other than the visible one. return; } if (message.messageID <= this._latestTransactionID) { // Message event for something we already know about. return; } // If this notification tells us about a message which is newer than // the newest one we know to exist, update our latest knownID so we // can properly update later. if (this._updating && this._updating.threadPHID == this._loadedThreadPHID) { if (message.messageID > this._updating.knownID) { this._updating.knownID = message.messageID; // We're currently updating, so wait for the update to complete. // this.syncWorkflow has us covered in this case. if (this._updating.active) { return; } } } this._updateThread(); })); // If we see a reconnect, always update the thread state. JX.Stratcom.listen( 'aphlict-reconnect', null, JX.bind(this, function() { if (!this._loadedThreadPHID) { return; } this._updateThread(); })); JX.Stratcom.listen( 'click', 'show-older-messages', JX.bind(this, function(e) { e.kill(); var data = e.getNodeData('show-older-messages'); var node = e.getNode('show-older-messages'); JX.DOM.setContent(node, 'Loading...'); JX.DOM.alterClass( node, 'conpherence-show-more-messages-loading', true); new JX.Workflow(this._getMoreMessagesURI(), data) .setHandler(JX.bind(this, function(r) { this._deleteTransactionCaches(JX.Stratcom.getData(node).id); JX.DOM.remove(node); this._updateTransactions(r); })).start(); })); JX.Stratcom.listen( 'click', 'show-newer-messages', JX.bind(this, function(e) { e.kill(); var data = e.getNodeData('show-newer-messages'); var node = e.getNode('show-newer-messages'); JX.DOM.setContent(node, 'Loading...'); JX.DOM.alterClass( node, 'conpherence-show-more-messages-loading', true); new JX.Workflow(this._getMoreMessagesURI(), data) .setHandler(JX.bind(this, function(r) { this._deleteTransactionCaches(JX.Stratcom.getData(node).id); JX.DOM.remove(node); this._updateTransactions(r); })).start(); })); }, _shouldUpdateDOM: function(r) { if (this._updating && this._updating.threadPHID == this._loadedThreadPHID) { if (r.non_update) { return false; } // we have a different, more current update in progress so // return early if (r.latest_transaction_id < this._updating.knownID) { return false; } } return true; }, _updateDOM: function(r) { this._updateTransactions(r); this._updating.knownID = r.latest_transaction_id; this._latestTransactionID = r.latest_transaction_id; JX.Leader.broadcast( 'conpherence.message.' + r.latest_transaction_id, { type: 'sound', data: r.sound.receive }); JX.Stratcom.invoke( 'conpherence-redraw-aphlict', null, r.aphlictDropdownData); }, _updateTransactions: function(r) { var new_transactions = JX.$H(r.transactions).getFragment().childNodes; this._updateTransactionIDMap(new_transactions); this._updateTransactionCache(new_transactions); var transactions = this._getLoadedTransactions(); JX.DOM.setContent(this._messagesRootCallback(), transactions); }, cacheCurrentTransactions: function() { var root = this._messagesRootCallback(); var transactions = JX.DOM.scry( root , 'div', 'conpherence-transaction-view'); this._updateTransactionIDMap(transactions); this._updateTransactionCache(transactions); }, _updateThread: function() { var params = this._getParams({ action: 'load', }); var workflow = new JX.Workflow(this._getUpdateURI()) .setData(params) .setHandler(JX.bind(this, function(r) { if (this._shouldUpdateDOM(r)) { this._updateDOM(r); this._didUpdateThreadCallback(r); } })); this.syncWorkflow(workflow, 'finally'); }, syncWorkflow: function(workflow, stage) { this._updating = { threadPHID: this._loadedThreadPHID, knownID: this._latestTransactionID, active: true }; workflow.listen(stage, JX.bind(this, function() { // TODO - do we need to handle if we switch threads somehow? var need_sync = this._updating && (this._updating.knownID > this._latestTransactionID); if (need_sync) { return this._updateThread(); } this._updating.active = false; })); workflow.start(); }, runUpdateWorkflowFromLink: function(link, params) { params = this._getParams(params); this._willUpdateWorkflowCallback(); var workflow = new JX.Workflow.newFromLink(link) .setData(params) .setHandler(JX.bind(this, function(r) { if (this._shouldUpdateDOM(r)) { this._updateDOM(r); this._didUpdateWorkflowCallback(r); } })); this.syncWorkflow(workflow, params.stage); }, loadThreadByID: function(thread_id, force_reload) { if (this.isThreadLoaded() && this.isThreadIDLoaded(thread_id) && !force_reload) { return; } this._willLoadThreadCallback(); var params = {}; // We pick a thread from the server if not specified if (thread_id) { params.id = thread_id; } params = this._getParams(params); var handler = JX.bind(this, function(r) { var client = JX.Aphlict.getInstance(); if (client) { var old_subs = client.getSubscriptions(); var new_subs = []; for (var ii = 0; ii < old_subs.length; ii++) { if (old_subs[ii] == this._loadedThreadPHID) { continue; } else { new_subs.push(old_subs[ii]); } } new_subs.push(r.threadPHID); client.clearSubscriptions(client.getSubscriptions()); client.setSubscriptions(new_subs); } this._loadedThreadID = r.threadID; this._loadedThreadPHID = r.threadPHID; this._latestTransactionID = r.latestTransactionID; this._canEditLoadedThread = r.canEdit; JX.Stratcom.invoke( 'conpherence-redraw-aphlict', null, r.aphlictDropdownData); this._didLoadThreadCallback(r); this.cacheCurrentTransactions(); if (force_reload) { JX.Stratcom.invoke('hashchange'); } }); // should this be sync'd too? new JX.Workflow(this.getLoadThreadURI()) .setData(params) .setHandler(handler) .start(); }, sendMessage: function(form, params) { var inputs = JX.DOM.scry(form, 'input'); var block_empty = true; for (var i = 0; i < inputs.length; i++) { if (inputs[i].type != 'hidden') { continue; } if (inputs[i].name == 'action' && inputs[i].value == 'join_room') { block_empty = false; continue; } } // don't bother sending up text if there is nothing to submit var textarea = JX.DOM.find(form, 'textarea'); if (block_empty && !textarea.value.length) { return; } params = this._getParams(params); var keep_enabled = true; var workflow = JX.Workflow.newFromForm(form, params, keep_enabled) .setHandler(JX.bind(this, function(r) { if (this._shouldUpdateDOM(r)) { this._updateDOM(r); this._didSendMessageCallback(r); } else if (r.non_update) { this._didSendMessageCallback(r, true); } })); this.syncWorkflow(workflow, 'finally'); textarea.value = ''; this._willSendMessageCallback(); }, handleDraftKeydown: function(e) { var form = e.getNode('tag:form'); var data = e.getNodeData('tag:form'); if (!data.preview) { data.preview = new JX.PhabricatorShapedRequest( this._getUpdateURI(), JX.bag, JX.bind(this, function () { var data = JX.DOM.convertFormToDictionary(form); data.action = 'draft'; data = this._getParams(data); return data; })); } data.preview.trigger(); }, _getUpdateURI: function() { return '/conpherence/update/' + this._loadedThreadID + '/'; }, _getMoreMessagesURI: function() { return '/conpherence/' + this._loadedThreadID + '/'; } }, statics: { _instance: null, getInstance: function() { var self = JX.ConpherenceThreadManager; if (!self._instance) { return null; } return self._instance; } } });