diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index d2f4de11ca..ed720acaf4 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -918,7 +918,7 @@ celerity_register_resource_map(array( ), 'conpherence-menu-css' => array( - 'uri' => '/res/c955650e/rsrc/css/application/conpherence/menu.css', + 'uri' => '/res/60f38fbd/rsrc/css/application/conpherence/menu.css', 'type' => 'css', 'requires' => array( @@ -927,7 +927,7 @@ celerity_register_resource_map(array( ), 'conpherence-message-pane-css' => array( - 'uri' => '/res/383af93e/rsrc/css/application/conpherence/message-pane.css', + 'uri' => '/res/d9e90066/rsrc/css/application/conpherence/message-pane.css', 'type' => 'css', 'requires' => array( @@ -945,7 +945,7 @@ celerity_register_resource_map(array( ), 'conpherence-widget-pane-css' => array( - 'uri' => '/res/6f836b19/rsrc/css/application/conpherence/widget-pane.css', + 'uri' => '/res/b218398a/rsrc/css/application/conpherence/widget-pane.css', 'type' => 'css', 'requires' => array( @@ -1294,25 +1294,24 @@ celerity_register_resource_map(array( ), 'javelin-behavior-conpherence-menu' => array( - 'uri' => '/res/7181099a/rsrc/js/application/conpherence/behavior-menu.js', + 'uri' => '/res/478fc4f3/rsrc/js/application/conpherence/behavior-menu.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', 2 => 'javelin-util', - 3 => 'javelin-request', - 4 => 'javelin-stratcom', - 5 => 'javelin-workflow', - 6 => 'javelin-behavior-device', - 7 => 'javelin-history', - 8 => 'javelin-vector', + 3 => 'javelin-stratcom', + 4 => 'javelin-workflow', + 5 => 'javelin-behavior-device', + 6 => 'javelin-history', + 7 => 'javelin-vector', ), 'disk' => '/rsrc/js/application/conpherence/behavior-menu.js', ), 'javelin-behavior-conpherence-pontificate' => array( - 'uri' => '/res/88ac3361/rsrc/js/application/conpherence/behavior-pontificate.js', + 'uri' => '/res/d6c5860f/rsrc/js/application/conpherence/behavior-pontificate.js', 'type' => 'js', 'requires' => array( @@ -1326,7 +1325,7 @@ celerity_register_resource_map(array( ), 'javelin-behavior-conpherence-widget-pane' => array( - 'uri' => '/res/3d426c01/rsrc/js/application/conpherence/behavior-widget-pane.js', + 'uri' => '/res/232893cf/rsrc/js/application/conpherence/behavior-widget-pane.js', 'type' => 'js', 'requires' => array( diff --git a/src/applications/conpherence/controller/ConpherenceController.php b/src/applications/conpherence/controller/ConpherenceController.php index 95bf05f613..348507011b 100644 --- a/src/applications/conpherence/controller/ConpherenceController.php +++ b/src/applications/conpherence/controller/ConpherenceController.php @@ -50,7 +50,16 @@ abstract class ConpherenceController extends PhabricatorController { ->setHref($this->getApplicationURI('update/'.$conpherence->getID().'/')) ->setWorkflow(true)); - return $crumbs; + return hsprintf( + '%s', + array( + phutil_tag( + 'div', + array( + 'class' => 'header-loading-mask' + ), + ''), + $crumbs)); } protected function renderConpherenceTransactions( diff --git a/src/applications/conpherence/controller/ConpherenceListController.php b/src/applications/conpherence/controller/ConpherenceListController.php index 9ff71e83f9..1116ac332d 100644 --- a/src/applications/conpherence/controller/ConpherenceListController.php +++ b/src/applications/conpherence/controller/ConpherenceListController.php @@ -154,7 +154,13 @@ final class ConpherenceListController ->setThreadView($thread_view) ->setRole('list'); if ($conpherence) { + $layout->setHeader($this->buildHeaderPaneContent($conpherence)); $layout->setThread($conpherence); + } else { + $layout->setHeader( + $this->buildHeaderPaneContent( + id(new ConpherenceThread()) + ->makeEphemeral())); } $response = $this->buildApplicationPage( $layout, diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 347445d4bc..3d5d9b2f50 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -70,7 +70,7 @@ final class ConpherenceViewController extends $form = null; $content = array('messages' => $messages); } else { - $header = $this->renderHeaderPaneContent(); + $header = $this->buildHeaderPaneContent($conpherence); $form = $this->renderFormContent($data['latest_transaction_id']); $content = array( 'header' => $header, @@ -103,19 +103,10 @@ final class ConpherenceViewController extends )); } - private function renderHeaderPaneContent() { - $conpherence = $this->getConpherence(); - $header = $this->buildHeaderPaneContent($conpherence); - return hsprintf('%s', $header); - } - - private function renderMessagePaneContent( array $transactions, $oldest_transaction_id) { - require_celerity_resource('conpherence-message-pane-css'); - $scrollbutton = ''; if ($oldest_transaction_id) { $scrollbutton = javelin_tag( diff --git a/src/applications/conpherence/controller/ConpherenceWidgetController.php b/src/applications/conpherence/controller/ConpherenceWidgetController.php index 7415cb8407..764088bac1 100644 --- a/src/applications/conpherence/controller/ConpherenceWidgetController.php +++ b/src/applications/conpherence/controller/ConpherenceWidgetController.php @@ -61,7 +61,6 @@ final class ConpherenceWidgetController extends } private function renderWidgetPaneContent() { - require_celerity_resource('conpherence-widget-pane-css'); require_celerity_resource('sprite-conpherence-css'); $conpherence = $this->getConpherence(); @@ -73,7 +72,7 @@ final class ConpherenceWidgetController extends ), id(new PhabricatorActionHeaderView()) ->setHeaderColor(PhabricatorActionHeaderView::HEADER_GREY) - ->setHeaderTitle('') + ->setHeaderTitle(pht('Participants')) ->setHeaderHref('#') ->setDropdown(true) ->addHeaderSigil('widgets-selector')); diff --git a/src/applications/conpherence/view/ConpherenceLayoutView.php b/src/applications/conpherence/view/ConpherenceLayoutView.php index d6d31bf820..7509295402 100644 --- a/src/applications/conpherence/view/ConpherenceLayoutView.php +++ b/src/applications/conpherence/view/ConpherenceLayoutView.php @@ -51,14 +51,23 @@ final class ConpherenceLayoutView extends AphrontView { public function render() { require_celerity_resource('conpherence-menu-css'); + require_celerity_resource('conpherence-message-pane-css'); + require_celerity_resource('conpherence-widget-pane-css'); $layout_id = celerity_generate_unique_node_id(); + $selected_id = null; + $selected_thread_id = null; + if ($this->thread) { + $selected_id = $this->thread->getPHID() . '-nav-item'; + $selected_thread_id = $this->thread->getID(); + } Javelin::initBehavior('conpherence-menu', array( - 'base_uri' => $this->baseURI, + 'baseURI' => $this->baseURI, 'layoutID' => $layout_id, - 'selectedID' => ($this->thread ? $this->thread->getID() : null), + 'selectedID' => $selected_id, + 'selectedThreadID' => $selected_thread_id, 'role' => $this->role, 'hasThreadList' => (bool)$this->threadView, 'hasThread' => (bool)$this->messages, @@ -92,6 +101,9 @@ final class ConpherenceLayoutView extends AphrontView { ), ))); + + $icon_48 = celerity_get_resource_uri('/rsrc/image/loading/loading_48.gif'); + $loading_style = 'background-image: url('.$icon_48.');'; return javelin_tag( 'div', array( @@ -114,7 +126,14 @@ final class ConpherenceLayoutView extends AphrontView { 'class' => 'conpherence-menu-pane phabricator-side-menu', 'sigil' => 'conpherence-menu-pane', ), - nonempty($this->threadView, '')), + nonempty( + $this->threadView, + phutil_tag( + 'div', + array( + 'class' => 'menu-loading-icon', + 'style' => $loading_style), + ''))), javelin_tag( 'div', array( @@ -159,12 +178,32 @@ final class ConpherenceLayoutView extends AphrontView { 'id' => 'conpherence-widget-pane', 'sigil' => 'conpherence-widget-pane', ), - ''), + array( + phutil_tag( + 'div', + array( + 'class' => 'widgets-loading-mask' + ), + ''), + phutil_tag( + 'div', + array( + 'class' => 'widgets-loading-icon', + 'style' => $loading_style, + ), + ''), + javelin_tag( + 'div', + array( + 'sigil' => 'conpherence-widgets-holder' + ), + ''))), javelin_tag( 'div', array( 'class' => 'conpherence-message-pane', - 'id' => 'conpherence-message-pane' + 'id' => 'conpherence-message-pane', + 'sigil' => 'conpherence-message-pane' ), array( javelin_tag( @@ -175,6 +214,18 @@ final class ConpherenceLayoutView extends AphrontView { 'sigil' => 'conpherence-messages', ), nonempty($this->messages, '')), + phutil_tag( + 'div', + array( + 'class' => 'messages-loading-mask', + ), + ''), + phutil_tag( + 'div', + array( + 'class' => 'messages-loading-icon', + 'style' => $loading_style, + )), javelin_tag( 'div', array( diff --git a/src/applications/conpherence/view/ConpherenceThreadListView.php b/src/applications/conpherence/view/ConpherenceThreadListView.php index abe66876d4..37bc55124a 100644 --- a/src/applications/conpherence/view/ConpherenceThreadListView.php +++ b/src/applications/conpherence/view/ConpherenceThreadListView.php @@ -84,6 +84,7 @@ final class ConpherenceThreadListView extends AphrontView { $unread_count = $data['unread_count']; $epoch = $data['epoch']; $image = $data['image']; + $dom_id = $thread->getPHID().'-nav-item'; return id(new ConpherenceMenuItemView()) ->setUser($user) @@ -98,7 +99,8 @@ final class ConpherenceThreadListView extends AphrontView { ->setMetadata( array( 'title' => $data['js_title'], - 'id' => $thread->getID(), + 'id' => $dom_id, + 'threadID' => $thread->getID(), )); } diff --git a/webroot/rsrc/css/application/conpherence/menu.css b/webroot/rsrc/css/application/conpherence/menu.css index 4501e56727..96841a14bb 100644 --- a/webroot/rsrc/css/application/conpherence/menu.css +++ b/webroot/rsrc/css/application/conpherence/menu.css @@ -21,7 +21,13 @@ margin: 0px 0px 16px 0px; } -.conpherence-menu-pane { +.conpherence-menu-pane .menu-loading-icon { + background-repeat: no-repeat; + background-position: center center; +} + +.conpherence-menu-pane, +.loading .menu-loading-icon { width: 100%; position: absolute; overflow-x: hidden; @@ -30,7 +36,8 @@ bottom: 0; } .device-desktop .conpherence-layout .conpherence-menu-pane, -.device-desktop .conpherence-layout .phabricator-nav-column-background { +.device-desktop .conpherence-layout .phabricator-nav-column-background, +.device-desktop .loading .menu-loading-icon { width: 280px; } .device .conpherence-menu-pane { @@ -63,10 +70,16 @@ } .conpherence-content-pane { + display: none; margin-left: 0px; position: relative; } +.device-desktop .conpherence-content-pane, +.device .conpherence-role-thread .conpherence-content-pane { + display: block; +} + .conpherence-menu .conpherence-menu-item-view { display: block; height: 55px; @@ -85,6 +98,10 @@ border-left: 2px solid #66CCFF; } +.conpherence-menu .loading { + font-style: italic; +} + .device-desktop .conpherence-menu .conpherence-menu-item-view:hover { background-image: url('/rsrc/image/texture/dark-menu-hover.png'); } diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index 821151c51e..ff39a8cb08 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -2,23 +2,26 @@ * @provides conpherence-message-pane-css */ -.conpherence-message-pane { +.conpherence-message-pane, +.loading .messages-loading-mask, +.loading .messages-loading-icon { position: fixed; left: 280px; right: 241px; top: 76px; + bottom: 0px; min-width: 300px; width: auto; - height: 100%; } -.device .conpherence-message-pane { +.device .conpherence-message-pane, +.device .loading .messages-loading-mask, +.device .loading .messages-loading-icon { left: 0; right: 0; width: 100%; } - .conpherence-show-older-messages { display: block; background: #e0e3ec; @@ -28,6 +31,10 @@ color: #18559D; } +.conpherence-show-older-messages-loading { + font-style: italic; +} + .conpherence-message-pane .conpherence-messages { position: fixed; left: 280px; @@ -47,6 +54,31 @@ box-shadow: none; } +.conpherence-message-pane .messages-loading-mask { + opacity: .22; + background: #222; + display: none; +} +.conpherence-message-pane .messages-loading-icon { + background-repeat: no-repeat; + background-position: center center; +} + +.loading .messages-loading-mask, +.loading .messages-loading-icon { + display: block; + z-index: 500; +} + +.loading .header-loading-mask { + height: 31px; + position: absolute; + width: 100%; + z-index: 5; + background: #222; + opacity: .22; +} + .conpherence-message-pane .phabricator-form-view { border-width: 0; background: none; diff --git a/webroot/rsrc/css/application/conpherence/widget-pane.css b/webroot/rsrc/css/application/conpherence/widget-pane.css index 2862828a3b..4f755efab7 100644 --- a/webroot/rsrc/css/application/conpherence/widget-pane.css +++ b/webroot/rsrc/css/application/conpherence/widget-pane.css @@ -2,12 +2,14 @@ * @provides conpherence-widget-pane-css */ -.conpherence-widget-pane { +.conpherence-widget-pane, +.loading .widgets-loading-mask, +.loading .widgets-loading-icon { position: fixed; right: 0px; top: 74px; + bottom: 0px; width: 240px; - height: 100%; border-width: 0 0 0 1px; border-color: #CCC; border-style: solid; @@ -15,11 +17,29 @@ -webkit-overflow-scrolling: touch; } -.device .conpherence-widget-pane { +.device .conpherence-widget-pane, +.device .loading .widgets-loading-mask, +.device .loading .widgets-loading-icon { top: 44px; width: 100%; } +.conpherence-widget-pane .widgets-loading-mask { + opacity: .22; + background: #222; + display: none; +} +.conpherence-widget-pane .widgets-loading-icon { + background-repeat: no-repeat; + background-position: center center; +} + +.loading .widgets-loading-mask, +.loading .widgets-loading-icon { + display: block; + z-index: 500; +} + .conpherence-widget-pane .aphront-form-input { margin: 0; width: 100%; @@ -80,6 +100,10 @@ text-align: center; color: #555; } +.device .conpherence-widget-pane #widgets-files .no-files { + width: 60px; + margin: 0px auto 0px auto; +} .conpherence-widget-pane #widgets-files .file-entry { padding: 10px 0; margin: 0 5px 0 10px; diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js index 3bbdc2b2bf..b24dd683cf 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-menu.js +++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js @@ -3,7 +3,6 @@ * @requires javelin-behavior * javelin-dom * javelin-util - * javelin-request * javelin-stratcom * javelin-workflow * javelin-behavior-device @@ -13,27 +12,66 @@ JX.behavior('conpherence-menu', function(config) { - var thread = { + /** + * State for displayed thread. + */ + var _thread = { selected: null, - node: null, - visible: null + visible: null, + node: null }; - function selectthreadid(id, updatePageData) { - var threads = JX.DOM.scry(document.body, 'a', 'conpherence-menu-click'); - for (var ii = 0; ii < threads.length; ii++) { - var data = JX.Stratcom.getData(threads[ii]); - if (data.id == id) { - selectthread(threads[ii], updatePageData); - return; - } + /** + * Current role of this behavior. The two possible roles are to show a 'list' + * of threads or a specific 'thread'. On devices, this behavior stays in the + * 'list' role indefinitely, treating clicks normally and the next page + * loads the behavior with role = 'thread'. On desktop, this behavior + * auto-loads a thread as part of the 'list' role. As the thread loads the + * role is changed to 'thread'. + */ + var _currentRole = null; + + /** + * When _oldDevice is null the code is executing for the first time. + */ + var _oldDevice = null; + + /** + * Initializes this behavior based on all the configuraton jonx and the + * result of JX.Device.getDevice(); + */ + function init() { + _currentRole = config.role; + + if (_currentRole == 'thread') { + markThreadsLoading(true); + } else { + markThreadLoading(true); } + markWidgetLoading(true); + onDeviceChange(); + } + init(); + + /** + * Selecting threads + */ + JX.Stratcom.listen( + 'conpherence-selectthread', + null, + function (e) { + selectThreadByID(e.getData().id); + } + ); + + function selectThreadByID(id, update_page_data) { + var thread = JX.$(id); + selectThread(thread, update_page_data); } - function selectthread(node, updatePageData) { - - if (thread.node) { - JX.DOM.alterClass(thread.node, 'conpherence-selected', false); + function selectThread(node, update_page_data) { + if (_thread.node) { + JX.DOM.alterClass(_thread.node, 'conpherence-selected', false); // keep the unread-count hidden still. big TODO once we ajax in updates // to threads to make this work right and move threads between read / // unread @@ -42,37 +80,28 @@ JX.behavior('conpherence-menu', function(config) { JX.DOM.alterClass(node, 'conpherence-selected', true); JX.DOM.alterClass(node, 'hide-unread-count', true); - thread.node = node; + _thread.node = node; var data = JX.Stratcom.getData(node); - thread.selected = data.id; + _thread.selected = data.threadID; - if (updatePageData) { - updatepagedata(data); + if (update_page_data) { + updatePageData(data); } - redrawthread(); + redrawThread(); } - JX.Stratcom.listen( - 'conpherence-selectthread', - null, - function (e) { - var node = JX.$(e.getData().id); - selectthread(node); - } - ); - - function updatepagedata(data) { - var uri_suffix = thread.selected + '/'; + function updatePageData(data) { + var uri_suffix = _thread.selected + '/'; if (data.use_base_uri) { uri_suffix = ''; } - JX.History.replace(config.base_uri + uri_suffix); + JX.History.replace(config.baseURI + uri_suffix); if (data.title) { document.title = data.title; - } else if (thread.node) { - var threadData = JX.Stratcom.getData(thread.node); + } else if (_thread.node) { + var threadData = JX.Stratcom.getData(_thread.node); document.title = threadData.title; } } @@ -81,60 +110,110 @@ JX.behavior('conpherence-menu', function(config) { 'conpherence-update-page-data', null, function (e) { - updatepagedata(e.getData()); + updatePageData(e.getData()); } ); - function redrawthread() { - if (!thread.node) { + function redrawThread() { + if (!_thread.node) { return; } - if (thread.visible == thread.selected) { + if (_thread.visible == _thread.selected) { return; } - var data = JX.Stratcom.getData(thread.node); + var data = JX.Stratcom.getData(_thread.node); - if (thread.visible !== null || !config.hasThread) { - var uri = config.base_uri + data.id + '/'; + if (_thread.visible !== null || !config.hasThread) { + markThreadLoading(true); + var uri = config.baseURI + data.threadID + '/'; new JX.Workflow(uri, {}) - .setHandler(onloadthreadresponse) + .setHandler(JX.bind(null, onLoadThreadResponse, data.threadID)) .start(); + } else if (config.hasThread) { + _scrollMessageWindow(); } else { - didredrawthread(); + didRedrawThread(); } - if (thread.visible !== null || !config.hasWidgets) { - var widget_uri = config.base_uri + 'widget/' + data.id + '/'; + if (_thread.visible !== null || !config.hasWidgets) { + markWidgetLoading(true); + var widget_uri = config.baseURI + 'widget/' + data.threadID + '/'; new JX.Workflow(widget_uri, {}) - .setHandler(onwidgetresponse) + .setHandler(JX.bind(null, onWidgetResponse, data.threadID)) .start(); } else { - updatetoggledwidget(); - } - - thread.visible = thread.selected; - } - - function onwidgetresponse(response) { - var root = JX.DOM.find(document, 'div', 'conpherence-layout'); - var widgetsRoot = JX.DOM.find(root, 'div', 'conpherence-widget-pane'); - JX.DOM.setContent(widgetsRoot, JX.$H(response.widgets)); - updatetoggledwidget(); - } - - function updatetoggledwidget(no_toggle) { - JX.Stratcom.invoke( - 'conpherence-toggle-widget', + JX.Stratcom.invoke( + 'conpherence-update-widgets', null, { - widget : getdefaultwidget(), - no_toggle : no_toggle + widget : getDefaultWidget(), + buildSelectors : false, + toggleWidget : true, + threadID : _thread.selected }); + } + + _thread.visible = _thread.selected; } - function getdefaultwidget() { + function markThreadsLoading(loading) { + var root = JX.DOM.find(document, 'div', 'conpherence-layout'); + var menu = JX.DOM.find(root, 'div', 'conpherence-menu-pane'); + JX.DOM.alterClass(menu, 'loading', loading); + } + + function markThreadLoading(loading) { + var root = JX.DOM.find(document, 'div', 'conpherence-layout'); + var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane'); + var messages_root = JX.DOM.find(root, 'div', 'conpherence-message-pane'); + var form_root = JX.DOM.find(root, 'div', 'conpherence-form'); + + JX.DOM.alterClass(header_root, 'loading', loading); + JX.DOM.alterClass(messages_root, 'loading', loading); + JX.DOM.alterClass(form_root, 'loading', loading); + + try { + var textarea = JX.DOM.find(form, 'textarea'); + textarea.disabled = loading; + var button = JX.DOM.find(form, 'button'); + button.disabled = loading; + } catch (ex) { + // haven't loaded it yet! + } + } + + function markWidgetLoading(loading) { + var root = JX.DOM.find(document, 'div', 'conpherence-layout'); + var widgets_root = JX.DOM.find(root, 'div', 'conpherence-widget-pane'); + + JX.DOM.alterClass(widgets_root, 'loading', loading); + } + + function onWidgetResponse(thread_id, response) { + // we got impatient and this is no longer the right answer :/ + if (_thread.selected != thread_id) { + return; + } + var root = JX.DOM.find(document, 'div', 'conpherence-layout'); + var widgets_root = JX.DOM.find(root, 'div', 'conpherence-widgets-holder'); + JX.DOM.setContent(widgets_root, JX.$H(response.widgets)); + + JX.Stratcom.invoke( + 'conpherence-update-widgets', + null, + { + widget : getDefaultWidget(), + buildSelectors : true, + toggleWidget : true, + threadID : _thread.selected + }); + + markWidgetLoading(false); + } + + function getDefaultWidget() { var device = JX.Device.getDevice(); var widget = 'conpherence-message-pane'; if (device == 'desktop') { @@ -143,48 +222,55 @@ JX.behavior('conpherence-menu', function(config) { return widget; } - function onloadthreadresponse(response) { + function onLoadThreadResponse(thread_id, response) { + // we got impatient and this is no longer the right answer :/ + if (_thread.selected != thread_id) { + return; + } var header = JX.$H(response.header); var messages = JX.$H(response.messages); var form = JX.$H(response.form); var root = JX.DOM.find(document, 'div', 'conpherence-layout'); - var headerRoot = JX.DOM.find(root, 'div', 'conpherence-header-pane'); - var messagesRoot = JX.DOM.find(root, 'div', 'conpherence-messages'); - var formRoot = JX.DOM.find(root, 'div', 'conpherence-form'); - JX.DOM.setContent(headerRoot, header); - JX.DOM.setContent(messagesRoot, messages); - JX.DOM.setContent(formRoot, form); + var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane'); + var messages_root = JX.DOM.find(root, 'div', 'conpherence-messages'); + var form_root = JX.DOM.find(root, 'div', 'conpherence-form'); + JX.DOM.setContent(header_root, header); + JX.DOM.setContent(messages_root, messages); + JX.DOM.setContent(form_root, form); - didredrawthread(); + markThreadLoading(false); + + didRedrawThread(true); } - function didredrawthread() { + /** + * This function is a wee bit tricky. Internally, we want to scroll the + * message window and let other stuff - notably widgets - redraw / build if + * necessary. Externally, we want a hook to scroll the message window + * - notably when the widget selector is used to invoke the message pane. + * The following three functions get 'er done. + */ + function didRedrawThread(build_device_widget_selector) { + _scrollMessageWindow(); + JX.Stratcom.invoke( + 'conpherence-did-redraw-thread', + null, + { + widget : getDefaultWidget(), + threadID : _thread.selected, + buildDeviceWidgetSelector : build_device_widget_selector + }); + } + function _scrollMessageWindow() { var root = JX.DOM.find(document, 'div', 'conpherence-layout'); - var messagesRoot = JX.DOM.find(root, 'div', 'conpherence-messages'); - messagesRoot.scrollTop = messagesRoot.scrollHeight; - - try { - var device = JX.Device.getDevice(); - var deviceWidgetSelector = JX.DOM.find( - root, - 'a', - 'device-widgets-selector'); - if (device != 'desktop') { - JX.DOM.show(deviceWidgetSelector); - updatetoggledwidget(true); - } else { - JX.DOM.hide(deviceWidgetSelector); - } - } catch (ex) { - // not here yet - } + var messages_root = JX.DOM.find(root, 'div', 'conpherence-messages'); + messages_root.scrollTop = messages_root.scrollHeight; } - JX.Stratcom.listen( 'conpherence-redraw-thread', null, function (e) { - didredrawthread(); + _scrollMessageWindow(); } ); @@ -202,7 +288,7 @@ JX.behavior('conpherence-menu', function(config) { } e.kill(); - selectthread(e.getNode('conpherence-menu-click'), true); + selectThread(e.getNode('conpherence-menu-click'), true); }); JX.Stratcom.listen('click', 'conpherence-edit-metadata', function (e) { @@ -241,126 +327,145 @@ JX.behavior('conpherence-menu', function(config) { .start(); }); + var _loadingTransactionID = null; JX.Stratcom.listen('click', 'show-older-messages', function(e) { e.kill(); var data = e.getNodeData('show-older-messages'); - var oldest_transaction_id = data.oldest_transaction_id; - var conf_id = thread.selected; - JX.DOM.remove(e.getNode('show-older-messages')); + if (data.oldest_transaction_id == _loadingTransactionID) { + return; + } + _loadingTransactionID = data.oldest_transaction_id; + var node = e.getNode('show-older-messages'); + JX.DOM.setContent(node, 'Loading...'); + JX.DOM.alterClass(node, 'conpherence-show-older-messages-loading', true); + + var conf_id = _thread.selected; var root = JX.DOM.find(document, 'div', 'conpherence-layout'); var messages_root = JX.DOM.find(root, 'div', 'conpherence-messages'); - new JX.Request(config.base_uri + conf_id + '/', function(r) { + new JX.Workflow(config.baseURI + conf_id + '/', data) + .setHandler(function(r) { + JX.DOM.remove(node); var messages = JX.$H(r.messages); JX.DOM.prependContent( messages_root, JX.$H(messages)); - }).setData({ oldest_transaction_id : oldest_transaction_id }).send(); + }).start(); }); - // On mobile, we just show a thread list, so we don't want to automatically - // select or load any threads. On Desktop, we automatically select the first - // thread. - var old_device = null; - function ondevicechange() { + /** + * On devices, we just show a thread list, so we don't want to automatically + * select or load any threads. On desktop, we automatically select the first + * thread, changing the _currentRole from list to thread. + */ + function onDeviceChange() { var new_device = JX.Device.getDevice(); - if (new_device === old_device) { + if (new_device === _oldDevice) { return; } - if (old_device === null) { - old_device = new_device; - if (config.role == 'list') { + if (_oldDevice === null) { + _oldDevice = new_device; + if (_currentRole == 'list') { if (new_device != 'desktop') { return; } } else { - loadthreads(); + loadThreads(); return; } } var update_toggled_widget = - new_device == 'desktop' || old_device == 'desktop'; - old_device = new_device; + new_device == 'desktop' || _oldDevice == 'desktop'; + _oldDevice = new_device; - if (thread.visible !== null && update_toggled_widget) { - updatetoggledwidget(); + if (_thread.visible !== null && update_toggled_widget) { + JX.Stratcom.invoke( + 'conpherence-did-redraw-thread', + null, + { + widget : getDefaultWidget(), + threadID : _thread.selected + }); } - if (config.role == 'list') { - didloadthreads(); - config.role = 'thread'; + if (_currentRole == 'list' && new_device == 'desktop') { + // this selects a thread and loads it + didLoadThreads(); + _currentRole = 'thread'; var root = JX.DOM.find(document, 'div', 'conpherence-layout'); JX.DOM.alterClass(root, 'conpherence-role-list', false); JX.DOM.alterClass(root, 'conpherence-role-thread', true); } } + JX.Stratcom.listen('phabricator-device-change', null, onDeviceChange); - JX.Stratcom.listen('phabricator-device-change', null, ondevicechange); - ondevicechange(); - - function loadthreads() { - var uri = config.base_uri + 'thread/' + config.selectedID + '/'; + function loadThreads() { + markThreadsLoading(true); + var uri = config.baseURI + 'thread/' + config.selectedThreadID + '/'; new JX.Workflow(uri) - .setHandler(onloadthreadsresponse) + .setHandler(onLoadThreadsResponse) .start(); } - function onloadthreadsresponse(r) { + function onLoadThreadsResponse(r) { var layout = JX.$(config.layoutID); var menu = JX.DOM.find(layout, 'div', 'conpherence-menu-pane'); JX.DOM.setContent(menu, JX.$H(r)); - config.selectedID && selectthreadid(config.selectedID); + config.selectedID && selectThreadByID(config.selectedID); - thread.node.scrollIntoView(); + _thread.node.scrollIntoView(); + + markThreadsLoading(false); } - function didloadthreads() { + function didLoadThreads() { // If there's no thread selected yet, select the current thread or the // first thread. - if (!thread.selected) { + if (!_thread.selected) { if (config.selectedID) { - selectthreadid(config.selectedID, true); + selectThreadByID(config.selectedID, true); } else { var layout = JX.$(config.layoutID); var threads = JX.DOM.scry(layout, 'a', 'conpherence-menu-click'); if (threads.length) { - selectthread(threads[0]); + selectThread(threads[0]); } else { var nothreads = JX.DOM.find(layout, 'div', 'conpherence-no-threads'); nothreads.style.display = 'block'; } } } - redrawthread(); } - var handlethreadscrollers = function (e) { + var handleThreadScrollers = function (e) { e.kill(); var data = e.getNodeData('conpherence-menu-scroller'); var scroller = e.getNode('conpherence-menu-scroller'); + JX.DOM.alterClass(scroller, 'loading', true); + JX.DOM.setContent(scroller.firstChild, 'Loading...'); new JX.Workflow(scroller.href, data) .setHandler( - JX.bind(null, threadscrollerresponse, scroller, data.direction)) + JX.bind(null, threadScrollerResponse, scroller, data.direction)) .start(); }; - var threadscrollerresponse = function (scroller, direction, r) { + var threadScrollerResponse = function (scroller, direction, r) { var html = JX.$H(r.html); - var threadPhids = r.phids; - var reselectId = null; + var thread_phids = r.phids; + var reselect_id = null; // remove any threads that are in the list that we just got back // in the result set; things have changed and they'll be in the // right place soon - for (var ii = 0; ii < threadPhids.length; ii++) { + for (var ii = 0; ii < thread_phids.length; ii++) { try { - var nodeId = threadPhids[ii] + '-nav-item'; - var node = JX.$(nodeId); - var nodeData = JX.Stratcom.getData(node); - if (nodeData.id == thread.selected) { - reselectId = nodeId; + var node_id = thread_phids[ii] + '-nav-item'; + var node = JX.$(node_id); + var node_data = JX.Stratcom.getData(node); + if (node_data.id == _thread.selected) { + reselect_id = node_id; } JX.DOM.remove(node); } catch (ex) { @@ -369,8 +474,8 @@ JX.behavior('conpherence-menu', function(config) { } var root = JX.DOM.find(document, 'div', 'conpherence-layout'); - var menuRoot = JX.DOM.find(root, 'div', 'conpherence-menu-pane'); - var scrollY = 0; + var menu_root = JX.DOM.find(root, 'div', 'conpherence-menu-pane'); + var scroll_y = 0; // we have to do some hyjinx in the up case to make the menu scroll to // where it should if (direction == 'up') { @@ -382,16 +487,16 @@ JX.behavior('conpherence-menu', function(config) { document.body.appendChild(test_size); var html_size = JX.Vector.getDim(test_size); JX.DOM.remove(test_size); - scrollY = html_size.y; + scroll_y = html_size.y; } JX.DOM.replace(scroller, html); - menuRoot.scrollTop += scrollY; + menu_root.scrollTop += scroll_y; - if (reselectId) { + if (reselect_id) { JX.Stratcom.invoke( 'conpherence-selectthread', null, - { id : reselectId } + { id : reselect_id } ); } }; @@ -399,7 +504,7 @@ JX.behavior('conpherence-menu', function(config) { JX.Stratcom.listen( ['click'], 'conpherence-menu-scroller', - handlethreadscrollers + handleThreadScrollers ); }); diff --git a/webroot/rsrc/js/application/conpherence/behavior-pontificate.js b/webroot/rsrc/js/application/conpherence/behavior-pontificate.js index e39251f403..f8068679e5 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-pontificate.js +++ b/webroot/rsrc/js/application/conpherence/behavior-pontificate.js @@ -14,13 +14,19 @@ JX.behavior('conpherence-pontificate', function(config) { var form = e.getNode('tag:form'); var root = e.getNode('conpherence-layout'); - var messages = JX.DOM.find(root, 'div', 'conpherence-messages'); + var messages_root = JX.DOM.find(root, 'div', 'conpherence-message-pane'); + var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane'); + var form_root = JX.DOM.find(root, 'div', 'conpherence-form'); + var messages = JX.DOM.find(messages_root, 'div', 'conpherence-messages'); var fileWidget = null; try { fileWidget = JX.DOM.find(root, 'div', 'widgets-files'); } catch (ex) { // Ignore; maybe no files widget } + JX.DOM.alterClass(header_root, 'loading', true); + JX.DOM.alterClass(messages_root, 'loading', true); + JX.DOM.alterClass(form_root, 'loading', true); JX.Workflow.newFromForm(form) .setHandler(JX.bind(this, function(r) { @@ -49,7 +55,11 @@ JX.behavior('conpherence-pontificate', function(config) { 'conpherence-selectthread', null, { id : r.conpherence_phid + '-nav-item' } - ); + ); + + JX.DOM.alterClass(header_root, 'loading', false); + JX.DOM.alterClass(messages_root, 'loading', false); + JX.DOM.alterClass(form_root, 'loading', false); })) .start(); }; diff --git a/webroot/rsrc/js/application/conpherence/behavior-widget-pane.js b/webroot/rsrc/js/application/conpherence/behavior-widget-pane.js index a273dc2fa4..d9ea8afa30 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-widget-pane.js +++ b/webroot/rsrc/js/application/conpherence/behavior-widget-pane.js @@ -13,80 +13,173 @@ JX.behavior('conpherence-widget-pane', function(config) { - var build_widget_selector = function (data) { - var widgets = config.widgetRegistry; + /** + * There can be race conditions around loading the messages or the widgets + * first. Keep track of what widgets we've loaded with this variable. + */ + var _loadedWidgetsID = null; + + /** + * At any given time there can be only one selected widget. Keep track of + * which one it is by the user-facing name for ease of use with + * PhabricatorDropdownMenuItems. + */ + var _selectedWidgetName = null; + + /** + * This is potentially built each time the user switches conpherence threads + * or when the result JX.Device.getDevice() changes from desktop to some + * other value. + */ + var buildDeviceWidgetSelector = function (data) { + var device_header = _getDeviceWidgetHeader(); + if (!device_header) { + return; + } + JX.DOM.show(device_header); + var device_menu = new JX.PhabricatorDropdownMenu(device_header); + data.deviceMenu = true; + _buildWidgetSelector(device_menu, data); + }; + + /** + * This is potentially built each time the user switches conpherence threads + * or when the result JX.Device.getDevice() changes from mobile or tablet to + * desktop. + */ + var buildDesktopWidgetSelector = function (data) { var root = JX.DOM.find(document, 'div', 'conpherence-layout'); - var widgetPane = JX.DOM.find(root, 'div', 'conpherence-widget-pane'); - var widgetHeader = JX.DOM.find(widgetPane, 'a', 'widgets-selector'); - var mobileWidgetHeader = null; + var widget_pane = JX.DOM.find(root, 'div', 'conpherence-widget-pane'); + var widget_header = JX.DOM.find(widget_pane, 'a', 'widgets-selector'); + var menu = new JX.PhabricatorDropdownMenu(widget_header); + menu.toggleAlignDropdownRight(false); + data.deviceMenu = false; + _buildWidgetSelector(menu, data); + }; + + /** + * Workhorse that actually builds the widget selector. Note some fancy bits + * where we listen for the "open" event and enable / disable widgets as + * appropos. + */ + var _buildWidgetSelector = function (menu, data) { + _loadedWidgetsID = data.threadID; + var widgets = config.widgetRegistry; + for (var widget in widgets) { + var widget_data = widgets[widget]; + if (widget_data.deviceOnly && data.deviceMenu === false) { + continue; + } + menu.addItem(new JX.PhabricatorMenuItem( + widget_data.name, + JX.bind(null, toggleWidget, { widget : widget }), + '#' + ).setDisabled(widget == data.widget)); + } + + menu.listen( + 'open', + JX.bind(menu, function () { + for (var ii = 0; ii < this._items.length; ii++) { + var item = this._items[ii]; + var name = item.getName(); + if (name == _selectedWidgetName) { + item.setDisabled(true); + } else { + item.setDisabled(false); + } + } + })); + }; + + /** + * Since this is not always on the page, avoid having a repeat + * try / catch block and consolidate into this helper function. + */ + var _getDeviceWidgetHeader = function () { + var root = JX.DOM.find(document, 'div', 'conpherence-layout'); + var device_header = null; try { - mobileWidgetHeader = JX.DOM.find( + device_header = JX.DOM.find( root, 'a', 'device-widgets-selector'); } catch (ex) { - // is okay - no mobileWidgetHeader yet... + // is okay - no deviceWidgetHeader yet... but bail time } - var widgetData = widgets[data.widget]; - JX.DOM.setContent( - widgetHeader, - widgetData.name); - JX.DOM.appendContent( - widgetHeader, - JX.$N('span', { className : 'caret' })); - if (mobileWidgetHeader) { - // this is fragile but adding a sigil to this element is awkward - var mobileWidgetHeaderSpans = JX.DOM.scry(mobileWidgetHeader, 'span'); - var mobileWidgetHeaderSpan = mobileWidgetHeaderSpans[1]; - JX.DOM.setContent( - mobileWidgetHeaderSpan, - widgetData.name); - } - - var menu = new JX.PhabricatorDropdownMenu(widgetHeader); - menu.toggleAlignDropdownRight(false); - var deviceMenu = null; - if (mobileWidgetHeader) { - deviceMenu = new JX.PhabricatorDropdownMenu(mobileWidgetHeader); - } - - for (var widget in widgets) { - widgetData = widgets[widget]; - if (mobileWidgetHeader) { - deviceMenu.addItem(new JX.PhabricatorMenuItem( - widgetData.name, - JX.bind(null, build_widget_selector, { widget : widget }), - '#' - ).setDisabled(widget == data.widget)); - } - if (widgetData.deviceOnly) { - continue; - } - menu.addItem(new JX.PhabricatorMenuItem( - widgetData.name, - JX.bind(null, build_widget_selector, { widget : widget }), - '#' - ).setDisabled(widget == data.widget)); - } - if (data.no_toggle) { - return; - } - toggle_widget(data); + return device_header; }; - var toggle_widget = function (data) { + /** + * Responder to the 'conpherence-did-redraw-thread' event, this bad boy + * hides or shows the device widget selector as appropros. + */ + var _didRedrawThread = function (data) { + if (_loadedWidgetsID === null || _loadedWidgetsID != data.threadID) { + return; + } + var device = JX.Device.getDevice(); + var device_selector = _getDeviceWidgetHeader(); + if (device == 'desktop') { + JX.DOM.hide(device_selector); + } else { + JX.DOM.show(device_selector); + } + if (data.buildDeviceWidgetSelector) { + buildDeviceWidgetSelector(data); + } + toggleWidget(data); + }; + JX.Stratcom.listen( + 'conpherence-did-redraw-thread', + null, + function (e) { + _didRedrawThread(e.getData()); + } + ); + + /** + * Toggling a widget involves showing / hiding the appropriate widget + * bodies as well as updating the selectors to have the label on the + * newly selected widget. + */ + var toggleWidget = function (data) { var widgets = config.widgetRegistry; - var widgetData = widgets[data.widget]; + var widget_data = widgets[data.widget]; var device = JX.Device.getDevice(); var is_desktop = device == 'desktop'; - if (widgetData.deviceOnly && is_desktop) { + if (widget_data.deviceOnly && is_desktop) { return; } + _selectedWidgetName = widget_data.name; + + var device_header = _getDeviceWidgetHeader(); + if (device_header) { + // this is fragile but adding a sigil to this element is awkward + var device_header_spans = JX.DOM.scry(device_header, 'span'); + var device_header_span = device_header_spans[1]; + JX.DOM.setContent( + device_header_span, + widget_data.name); + } + + // don't update the non-device selector with device only widget stuff + if (!widget_data.deviceOnly) { + var root = JX.DOM.find(document, 'div', 'conpherence-layout'); + var widget_pane = JX.DOM.find(root, 'div', 'conpherence-widget-pane'); + var widget_header = JX.DOM.find(widget_pane, 'a', 'widgets-selector'); + JX.DOM.setContent( + widget_header, + widget_data.name); + JX.DOM.appendContent( + widget_header, + JX.$N('span', { className : 'caret' })); + } for (var widget in config.widgetRegistry) { - widgetData = widgets[widget]; - if (widgetData.deviceOnly && is_desktop) { + widget_data = widgets[widget]; + if (widget_data.deviceOnly && is_desktop) { // some one off code for conpherence messages which are device-only // as a widget, but shown always on the desktop if (widget == 'conpherence-message-pane') { @@ -109,13 +202,18 @@ JX.behavior('conpherence-widget-pane', function(config) { }; JX.Stratcom.listen( - 'conpherence-toggle-widget', + 'conpherence-update-widgets', null, function (e) { - build_widget_selector(e.getData()); - } - ); - + var data = e.getData(); + if (data.buildSelectors) { + buildDesktopWidgetSelector(data); + buildDeviceWidgetSelector(data); + } + if (data.toggleWidget) { + toggleWidget(data); + } + }); /* people widget */ JX.Stratcom.listen( @@ -126,19 +224,19 @@ JX.behavior('conpherence-widget-pane', function(config) { var root = e.getNode('conpherence-layout'); var form = e.getNode('tag:form'); var data = e.getNodeData('add-person'); - var peopleRoot = e.getNode('widgets-people'); + var people_root = e.getNode('widgets-people'); var messages = null; try { messages = JX.DOM.find(root, 'div', 'conpherence-messages'); } catch (ex) { } - var latestTransactionData = JX.Stratcom.getData( + var latest_transaction_data = JX.Stratcom.getData( JX.DOM.find( root, 'input', 'latest-transaction-id' )); - data.latest_transaction_id = latestTransactionData.id; + data.latest_transaction_id = latest_transaction_data.id; JX.Workflow.newFromForm(form, data) .setHandler(JX.bind(this, function (r) { if (messages) { @@ -148,7 +246,7 @@ JX.behavior('conpherence-widget-pane', function(config) { // update the people widget JX.DOM.setContent( - peopleRoot, + people_root, JX.$H(r.people_widget) ); })) @@ -160,7 +258,7 @@ JX.behavior('conpherence-widget-pane', function(config) { ['touchstart', 'mousedown'], 'remove-person', function (e) { - var peopleRoot = e.getNode('widgets-people'); + var people_root = e.getNode('widgets-people'); var form = JX.DOM.find(peopleRoot, 'form'); var data = e.getNodeData('remove-person'); // we end up re-directing to conpherence home