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': {