1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-11 17:32:41 +01:00
phorge-phorge/webroot/rsrc/js/application/conpherence/behavior-durable-column.js
Bob Trahan 6c049d06ce Conpherence - change message rendering logic to eradicate possibility of duplicates
Summary:
Fixes T6713. Before this diff, we would update the DOM when various requests came back, but the logic to erase race conditions proved too tricky for me to get right. Instead, change the algorithm up and keep a set of transaction ids around per thread. When its time to update the transactions, sort the list of ids and just render the whole darn set again.

To make this work, this ends up adding transacton ids to fake transactons like "show older" and date markers. This is able to work by using a float sort and giving these transactions ids that are .5 from being an integer and in the right place numerically.

Test Plan: for durable column, clicked show older and it worked. sent a message and it worked. for main view, clicked show older and it worked. sent a message and it worked.

Reviewers: epriestley

Reviewed By: epriestley

Subscribers: Korvin, epriestley

Maniphest Tasks: T6713

Differential Revision: https://secure.phabricator.com/D12819
2015-05-13 11:06:54 -07:00

365 lines
9.9 KiB
JavaScript

/**
* @provides javelin-behavior-durable-column
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
* javelin-behavior-device
* javelin-scrollbar
* javelin-quicksand
* phabricator-keyboard-shortcut
* conpherence-thread-manager
*/
JX.behavior('durable-column', function(config, statics) {
// TODO: Currently, updating the column sends the entire column back. This
// includes the `durable-column` behavior itself, which tries to re-initialize
// the column. Detect this and bail.
//
// If ThreadManager gets separated into a UI part and a thread part (which
// seems likely), responses may no longer ship back the entire column. This
// might let us remove this check.
if (statics.initialized) {
return;
} else {
statics.initialized = true;
}
var userVisible = config.visible;
var show = null;
var loadThreadID = null;
var scrollbar = null;
var margin = JX.Scrollbar.getScrollbarControlMargin();
var columnWidth = (300 + margin);
// This is the smallest window size where we'll enable the column.
var minimumViewportWidth = (768 - margin);
var quick = JX.$('phabricator-standard-page-body');
function _getColumnNode() {
return JX.$('conpherence-durable-column');
}
function _getColumnScrollNode() {
var column = _getColumnNode();
return JX.DOM.find(column, 'div', 'conpherence-durable-column-main');
}
function _isViewportWideEnoughForColumn() {
var viewport = JX.Vector.getViewport();
if (viewport.x < minimumViewportWidth) {
return false;
} else {
return true;
}
}
function _updateColumnVisibility() {
var new_value = (userVisible && _isViewportWideEnoughForColumn());
if (new_value !== show) {
show = new_value;
_drawColumn(show);
}
}
function _toggleColumn() {
userVisible = !userVisible;
_updateColumnVisibility();
new JX.Request(config.settingsURI)
.setData({value: (show ? 1 : 0)})
.send();
}
function _drawColumn(visible) {
JX.DOM.alterClass(
document.body,
'with-durable-column',
visible);
JX.DOM.alterClass(
document.body,
'with-durable-margin',
visible && !!margin);
var column = _getColumnNode();
if (visible) {
JX.DOM.show(column);
threadManager.loadThreadByID(loadThreadID);
} else {
JX.DOM.hide(column);
}
JX.Quicksand.setFrame(visible ? quick : null);
// When we activate the column, adjust the tablet breakpoint so that we
// convert the left side of the screen to tablet mode on narrow displays.
var breakpoint;
if (visible) {
breakpoint = minimumViewportWidth + columnWidth;
} else {
breakpoint = minimumViewportWidth;
}
JX.Device.setTabletBreakpoint(breakpoint);
JX.Stratcom.invoke('resize');
}
new JX.KeyboardShortcut('\\', 'Toggle Conpherence Column')
.setHandler(_toggleColumn)
.register();
scrollbar = new JX.Scrollbar(_getColumnScrollNode());
JX.Quicksand.setFrame(userVisible ? quick : null);
JX.Quicksand.start(config.quicksandConfig);
/* Conpherence Thread Manager configuration - lots of display
* callbacks.
*/
var threadManager = new JX.ConpherenceThreadManager();
threadManager.setMinimalDisplay(true);
threadManager.setMessagesRootCallback(function() {
return _getColumnMessagesNode();
});
threadManager.setLoadThreadURI('/conpherence/columnview/');
threadManager.setWillLoadThreadCallback(function() {
_markLoading(true);
});
threadManager.setDidLoadThreadCallback(function(r) {
var column = _getColumnNode();
var new_column = JX.$H(r.content);
JX.DOM.replace(column, new_column);
if (show) {
JX.DOM.show(_getColumnNode());
} else {
JX.DOM.hide(_getColumnNode());
}
var messages = _getColumnMessagesNode();
scrollbar = new JX.Scrollbar(_getColumnScrollNode());
scrollbar.scrollTo(messages.scrollHeight);
_markLoading(false);
loadThreadID = threadManager.getLoadedThreadID();
});
threadManager.setDidUpdateThreadCallback(function(r) {
var messages = _getColumnMessagesNode();
JX.DOM.appendContent(messages, JX.$H(r.transactions));
scrollbar.scrollTo(messages.scrollHeight);
});
threadManager.setWillSendMessageCallback(function() {
// Wipe the textarea immediately so the user can start typing more text.
var textarea = _getColumnTextareaNode();
textarea.value = '';
_focusColumnTextareaNode();
});
threadManager.setDidSendMessageCallback(function(r, non_update) {
if (non_update) {
return;
}
var messages = _getColumnMessagesNode();
scrollbar.scrollTo(messages.scrollHeight);
});
threadManager.setWillUpdateWorkflowCallback(function() {
JX.Stratcom.invoke('notification-panel-close');
});
threadManager.setDidUpdateWorkflowCallback(function(r) {
var messages = _getColumnMessagesNode();
scrollbar.scrollTo(messages.scrollHeight);
JX.DOM.setContent(_getColumnTitleNode(), r.conpherence_title);
});
threadManager.start();
JX.Stratcom.listen(
'click',
'conpherence-durable-column-header-action',
function (e) {
e.kill();
var data = e.getNodeData('conpherence-durable-column-header-action');
var action = data.action;
var link = e.getNode('tag:a');
var params = null;
switch (action) {
case 'metadata':
threadManager.runUpdateWorkflowFromLink(
link,
{
action: action,
force_ajax: true,
stage: 'submit'
});
break;
case 'add_person':
threadManager.runUpdateWorkflowFromLink(
link,
{
action: action,
stage: 'submit'
});
break;
case 'go_conpherence':
JX.$U(link.href).go();
break;
case 'hide_column':
JX.Stratcom.invoke('notification-panel-close');
_toggleColumn();
break;
}
});
JX.Stratcom.listen(
'click',
'conpherence-durable-column-thread-icon',
function (e) {
e.kill();
var icons = JX.DOM.scry(
JX.$('conpherence-durable-column'),
'a',
'conpherence-durable-column-thread-icon');
var data = e.getNodeData('conpherence-durable-column-thread-icon');
var cdata = null;
for (var i = 0; i < icons.length; i++) {
cdata = JX.Stratcom.getData(icons[i]);
JX.DOM.alterClass(
icons[i],
'selected',
cdata.threadID == data.threadID);
}
JX.DOM.setContent(_getColumnTitleNode(), JX.$H(data.threadTitle));
threadManager.loadThreadByID(data.threadID);
});
JX.Stratcom.listen('resize', null, _updateColumnVisibility);
function _getColumnBodyNode() {
var column = JX.$('conpherence-durable-column');
return JX.DOM.find(
column,
'div',
'conpherence-durable-column-body');
}
function _getColumnMessagesNode() {
var column = JX.$('conpherence-durable-column');
return JX.DOM.find(
column,
'div',
'conpherence-durable-column-transactions');
}
function _getColumnTitleNode() {
var column = JX.$('conpherence-durable-column');
return JX.DOM.find(
column,
'div',
'conpherence-durable-column-header-text');
}
function _getColumnFormNode() {
var column = JX.$('conpherence-durable-column');
return JX.DOM.find(
column,
'form',
'conpherence-message-form');
}
function _getColumnTextareaNode() {
var column = JX.$('conpherence-durable-column');
return JX.DOM.find(
column,
'textarea',
'conpherence-durable-column-textarea');
}
function _focusColumnTextareaNode() {
var textarea = _getColumnTextareaNode();
setTimeout(function() { JX.DOM.focus(textarea); }, 1);
}
function _markLoading(loading) {
var column = _getColumnNode();
JX.DOM.alterClass(column, 'loading', loading);
}
function _sendMessage(e) {
e.kill();
var form = _getColumnFormNode();
threadManager.sendMessage(form, { minimal_display: true });
}
JX.Stratcom.listen(
'click',
'conpherence-send-message',
_sendMessage);
JX.Stratcom.listen(
['submit', 'didSyntheticSubmit'],
'conpherence-message-form',
_sendMessage);
// Send on enter if the shift key is not held.
JX.Stratcom.listen(
'keydown',
'conpherence-message-form',
function(e) {
if (e.getSpecialKey() != 'return') {
return;
}
var raw = e.getRawEvent();
if (raw.shiftKey) {
// If the shift key is pressed, let the browser write a newline into
// the textarea.
return;
}
// From here on, interpret this as a "send" action, not a literal
// newline.
e.kill();
_sendMessage(e);
});
JX.Stratcom.listen(
['keydown'],
'conpherence-durable-column-textarea',
function (e) {
threadManager.handleDraftKeydown(e);
});
// HTML5 placeholders are rendered as long as the input is empty, even if the
// input is currently focused. This is undesirable for the chat input,
// especially immediately after sending a message. Hide the placeholder while
// the input is focused.
JX.Stratcom.listen(
['focus', 'blur'],
'conpherence-durable-column-textarea',
function (e) {
var node = e.getTarget();
if (e.getType() == 'focus') {
if (node.placeholder) {
node.placeholderStorage = node.placeholder;
node.placeholder = '';
}
} else {
if (node.placeholderStorage) {
node.placeholder = node.placeholderStorage;
node.placeholderStorage = '';
}
}
});
JX.Stratcom.listen(
'quicksand-redraw',
null,
function (e) {
var new_data = e.getData().newResponse;
JX.Title.setTitle(new_data.title);
});
_updateColumnVisibility();
});