diff --git a/.travis.yml b/.travis.yml
index 4137d754bf..954f14a4da 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,7 +3,10 @@ dist: trusty
# we don't need sudo, so can run in a container, which makes startup much
# quicker.
-sudo: false
+#
+# unfortunately we do temporarily require sudo as a workaround for
+# https://github.com/travis-ci/travis-ci/issues/8836
+sudo: required
language: node_js
node_js:
diff --git a/package.json b/package.json
index eb2cabf854..dbc27b5a08 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,7 @@
"querystring": "^0.2.0",
"react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2",
+ "react-beautiful-dnd": "^4.0.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-dom": "^15.4.0",
diff --git a/src/DateUtils.js b/src/DateUtils.js
index 91df1e46d5..986525eec8 100644
--- a/src/DateUtils.js
+++ b/src/DateUtils.js
@@ -82,6 +82,17 @@ export function formatDate(date, showTwelveHour=false) {
return formatFullDate(date, showTwelveHour);
}
+export function formatFullDateNoTime(date) {
+ const days = getDaysArray();
+ const months = getMonthsArray();
+ return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
+ weekDayName: days[date.getDay()],
+ monthName: months[date.getMonth()],
+ day: date.getDate(),
+ fullYear: date.getFullYear(),
+ });
+}
+
export function formatFullDate(date, showTwelveHour=false) {
const days = getDaysArray();
const months = getMonthsArray();
diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js
index 60946ea7f1..608fd6c4c5 100644
--- a/src/actions/TagOrderActions.js
+++ b/src/actions/TagOrderActions.js
@@ -22,25 +22,32 @@ const TagOrderActions = {};
/**
* Creates an action thunk that will do an asynchronous request to
- * commit TagOrderStore.getOrderedTags() to account data and dispatch
- * actions to indicate the status of the request.
+ * move a tag in TagOrderStore to destinationIx.
*
* @param {MatrixClient} matrixClient the matrix client to set the
* account data on.
+ * @param {string} tag the tag to move.
+ * @param {number} destinationIx the new position of the tag.
* @returns {function} an action thunk that will dispatch actions
* indicating the status of the request.
* @see asyncAction
*/
-TagOrderActions.commitTagOrdering = function(matrixClient) {
- return asyncAction('TagOrderActions.commitTagOrdering', () => {
- // Only commit tags if the state is ready, i.e. not null
- const tags = TagOrderStore.getOrderedTags();
- if (!tags) {
- return;
- }
+TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
+ // Only commit tags if the state is ready, i.e. not null
+ let tags = TagOrderStore.getOrderedTags();
+ if (!tags) {
+ return;
+ }
+ tags = tags.filter((t) => t !== tag);
+ tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
+
+ return asyncAction('TagOrderActions.moveTag', () => {
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags});
+ }, () => {
+ // For an optimistic update
+ return {tags};
});
};
diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js
index bddfbc7c63..0238eee8c0 100644
--- a/src/actions/actionCreators.js
+++ b/src/actions/actionCreators.js
@@ -22,6 +22,9 @@ limitations under the License.
* suffix determining whether it is pending, successful or
* a failure.
* @param {function} fn a function that returns a Promise.
+ * @param {function?} pendingFn a function that returns an object to assign
+ * to the `request` key of the ${id}.pending
+ * payload.
* @returns {function} an action thunk - a function that uses its single
* argument as a dispatch function to dispatch the
* following actions:
@@ -29,9 +32,13 @@ limitations under the License.
* `${id}.success` or
* `${id}.failure`.
*/
-export function asyncAction(id, fn) {
+export function asyncAction(id, fn, pendingFn) {
return (dispatch) => {
- dispatch({action: id + '.pending'});
+ dispatch({
+ action: id + '.pending',
+ request:
+ typeof pendingFn === 'function' ? pendingFn() : undefined,
+ });
fn().then((result) => {
dispatch({action: id + '.success', result});
}).catch((err) => {
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 5523fc27a0..6a8c2e9c2e 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -325,7 +325,7 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
- const dateSeparator =
;
+ const dateSeparator = ;
ret.push(dateSeparator);
}
@@ -479,7 +479,7 @@ module.exports = React.createClass({
// do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) {
- const dateSeparator = ;
+ const dateSeparator = ;
ret.push(dateSeparator);
continuation = false;
}
diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js
index 531c247ea6..1cd3f04f9d 100644
--- a/src/components/structures/TagPanel.js
+++ b/src/components/structures/TagPanel.js
@@ -25,6 +25,8 @@ import TagOrderActions from '../../actions/TagOrderActions';
import sdk from '../../index';
import dis from '../../dispatcher';
+import { DragDropContext, Droppable } from 'react-beautiful-dnd';
+
const TagPanel = React.createClass({
displayName: 'TagPanel',
@@ -69,7 +71,9 @@ const TagPanel = React.createClass({
dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
},
- onClick() {
+ onClick(e) {
+ // Ignore clicks on children
+ if (e.target !== e.currentTarget) return;
dis.dispatch({action: 'deselect_tags'});
},
@@ -78,8 +82,20 @@ const TagPanel = React.createClass({
dis.dispatch({action: 'view_create_group'});
},
- onTagTileEndDrag() {
- dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
+ onTagTileEndDrag(result) {
+ // Dragged to an invalid destination, not onto a droppable
+ if (!result.destination) {
+ return;
+ }
+
+ // Dispatch synchronously so that the TagPanel receives an
+ // optimistic update from TagOrderStore before the previous
+ // state is shown.
+ dis.dispatch(TagOrderActions.moveTag(
+ this.context.matrixClient,
+ result.draggableId,
+ result.destination.index,
+ ), true);
},
render() {
@@ -89,16 +105,31 @@ const TagPanel = React.createClass({
const tags = this.state.orderedTags.map((tag, index) => {
return ;
});
- return
-
- { tags }
-
+ return
+
+
+ { (provided, snapshot) => (
+
+ { tags }
+ { provided.placeholder }
+
+ ) }
+
+
diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js
index 8814beace6..6cfe0babcb 100644
--- a/src/components/views/dialogs/QuestionDialog.js
+++ b/src/components/views/dialogs/QuestionDialog.js
@@ -62,7 +62,6 @@ export default React.createClass({
{ this.props.description }
@@ -70,6 +69,7 @@ export default React.createClass({
{ this.props.extraButtons }
diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js
index 539163d0dc..e17ea52976 100644
--- a/src/components/views/elements/DNDTagTile.js
+++ b/src/components/views/elements/DNDTagTile.js
@@ -15,71 +15,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { DragSource, DropTarget } from 'react-dnd';
-
import TagTile from './TagTile';
-import dis from '../../../dispatcher';
-import { findDOMNode } from 'react-dom';
-const tagTileSource = {
- canDrag: function(props, monitor) {
- return true;
- },
+import { Draggable } from 'react-beautiful-dnd';
- beginDrag: function(props) {
- // Return the data describing the dragged item
- return {
- tag: props.tag,
- };
- },
-
- endDrag: function(props, monitor, component) {
- const dropResult = monitor.getDropResult();
- if (!monitor.didDrop() || !dropResult) {
- return;
- }
- props.onEndDrag();
- },
-};
-
-const tagTileTarget = {
- canDrop(props, monitor) {
- return true;
- },
-
- hover(props, monitor, component) {
- if (!monitor.canDrop()) return;
- const draggedY = monitor.getClientOffset().y;
- const {top, bottom} = findDOMNode(component).getBoundingClientRect();
- const targetY = (top + bottom) / 2;
- dis.dispatch({
- action: 'order_tag',
- tag: monitor.getItem().tag,
- targetTag: props.tag,
- // Note: we indicate that the tag should be after the target when
- // it's being dragged over the top half of the target.
- after: draggedY < targetY,
- });
- },
-
- drop(props) {
- // Return the data to be returned by getDropResult
- return {
- tag: props.tag,
- };
- },
-};
-
-export default
- DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
- connectDropTarget: connect.dropTarget(),
- }))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
- connectDragSource: connect.dragSource(),
- }))((props) => {
- const { connectDropTarget, connectDragSource, ...otherProps } = props;
- return connectDropTarget(connectDragSource(
-
-
-
,
- ));
- }));
+export default function DNDTagTile(props) {
+ return
+
+ { (provided, snapshot) => (
+
+
+
+
+ { provided.placeholder }
+
+ ) }
+
+
;
+}
diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js
index d1e00bd302..d1ef6c2f2c 100644
--- a/src/components/views/rooms/RoomList.js
+++ b/src/components/views/rooms/RoomList.js
@@ -324,12 +324,7 @@ module.exports = React.createClass({
// Show all rooms
this._visibleRooms = MatrixClientPeg.get().getRooms();
}
-
- this.setState({
- selectedTags,
- }, () => {
- this.refreshRoomList();
- });
+ this._delayedRefreshRoomList();
},
refreshRoomList: function() {
@@ -345,6 +340,9 @@ module.exports = React.createClass({
this.setState({
lists: this.getRoomLists(),
totalRoomCount: totalRooms,
+ // Do this here so as to not render every time the selected tags
+ // themselves change.
+ selectedTags: TagOrderStore.getSelectedTags(),
});
// this._lastRefreshRoomListTs = Date.now();
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 0d686ad490..73e3f3a014 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -49,6 +49,7 @@
"AM": "AM",
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
+ "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
"Who would you like to add to this community?": "Who would you like to add to this community?",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js
index 9c9427284e..27ce6cbb73 100644
--- a/src/stores/TagOrderStore.js
+++ b/src/stores/TagOrderStore.js
@@ -78,24 +78,11 @@ class TagOrderStore extends Store {
this._updateOrderedTags();
break;
}
- // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag
- case 'order_tag': {
- if (!this._state.orderedTags ||
- !payload.tag ||
- !payload.targetTag ||
- payload.tag === payload.targetTag
- ) return;
-
- const tags = this._state.orderedTags;
-
- let orderedTags = tags.filter((t) => t !== payload.tag);
- const newIndex = orderedTags.indexOf(payload.targetTag) + (payload.after ? 1 : 0);
- orderedTags = [
- ...orderedTags.slice(0, newIndex),
- payload.tag,
- ...orderedTags.slice(newIndex),
- ];
- this._setState({orderedTags});
+ case 'TagOrderActions.moveTag.pending': {
+ // Optimistic update of a moved tag
+ this._setState({
+ orderedTags: payload.request.tags,
+ });
break;
}
case 'select_tag': {