From dfecad181a1a086c391e1cedd67e53c50b9c2b54 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 18 Jan 2018 17:52:50 +0000 Subject: [PATCH 1/9] Swap RoomList to react-beautiful-dnd --- src/components/views/rooms/RoomList.js | 97 ++++++++++++++++++++++++++ src/components/views/rooms/RoomTile.js | 43 ++++-------- 2 files changed, 109 insertions(+), 31 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index d1ef6c2f2c..898dfbef8c 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -18,6 +18,7 @@ limitations under the License. 'use strict'; const React = require("react"); const ReactDOM = require("react-dom"); +import { DragDropContext } from 'react-beautiful-dnd'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; const GeminiScrollbar = require('react-gemini-scrollbar'); @@ -32,6 +33,8 @@ const Receipt = require('../../../utils/Receipt'); import TagOrderStore from '../../../stores/TagOrderStore'; import GroupStoreCache from '../../../stores/GroupStoreCache'; +import Modal from '../../../Modal'; + const HIDE_CONFERENCE_CHANS = true; function phraseForSection(section) { @@ -275,6 +278,98 @@ module.exports = React.createClass({ this.forceUpdate(); }, + onRoomTileEndDrag: function(result) { + if (!result.destination) return; + + let newTag = result.destination.droppableId.split('_')[1]; + let prevTag = result.source.droppableId.split('_')[1]; + if (newTag === 'undefined') newTag = undefined; + if (prevTag === 'undefined') prevTag = undefined; + + const roomId = result.draggableId.split('_')[1]; + const room = MatrixClientPeg.get().getRoom(roomId); + + const newIndex = result.destination.index; + + // Evil hack to get DMs behaving + if ((prevTag === undefined && newTag === 'im.vector.fake.direct') || + (prevTag === 'im.vector.fake.direct' && newTag === undefined) + ) { + Rooms.guessAndSetDMRoom( + room, newTag === 'im.vector.fake.direct', + ).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to set direct chat tag " + err); + Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { + title: _t('Failed to set direct chat tag'), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + return; + } + + const hasChangedSubLists = result.source.droppableId !== result.destination.droppableId; + + const newOrder = {}; + + // Is the tag ordered manually? + if (!newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { + const newList = Object.assign({}, this.state.lists[newTag]); + + // If the room was moved "down" (increasing index) in the same list we + // need to use the orders of the tiles with indices shifted by +1 + const offset = ( + newTag === prevTag && result.source.index < result.destination.index + ) ? 1 : 0; + + const prevOrder = newIndex === 0 ? + 0 : newList[offset + newIndex - 1].tags[newTag].order; + const nextOrder = newIndex === newList.length ? + 1 : newList[offset + newIndex].tags[newTag].order; + + newOrder['order'] = (prevOrder + nextOrder) / 2.0; + } + + // More evilness: We will still be dealing with moving to favourites/low prio, + // but we avoid ever doing a request with 'im.vector.fake.direct`. + // + // if we moved lists, remove the old tag + if (prevTag && prevTag !== 'im.vector.fake.direct' && + hasChangedSubLists + ) { + // Optimistic update of what will happen to the room tags + delete room.tags[prevTag]; + + MatrixClientPeg.get().deleteRoomTag(roomId, prevTag).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to remove tag " + prevTag + " from room: " + err); + Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { + title: _t('Failed to remove tag %(tagName)s from room', {tagName: prevTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + } + + // if we moved lists or the ordering changed, add the new tag + if (newTag && newTag !== 'im.vector.fake.direct' && + (hasChangedSubLists || newOrder) + ) { + // Optimistic update of what will happen to the room tags + room.tags[newTag] = newOrder; + + MatrixClientPeg.get().setRoomTag(roomId, newTag, newOrder).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to add tag " + newTag + " to room: " + err); + Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { + title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + } + + this.refreshRoomList(); + }, + _delayedRefreshRoomList: new rate_limited_func(function() { this.refreshRoomList(); }, 500), @@ -649,6 +744,7 @@ module.exports = React.createClass({ const self = this; return ( +
@@ -757,6 +853,7 @@ module.exports = React.createClass({ onShowMoreRooms={self.onShowMoreRooms} />
+
); }, }); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 30cc64ef7a..43bc69dea2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -35,10 +35,7 @@ module.exports = React.createClass({ displayName: 'RoomTile', propTypes: { - connectDragSource: PropTypes.func, - connectDropTarget: PropTypes.func, onClick: PropTypes.func, - isDragging: PropTypes.bool, room: PropTypes.object.isRequired, collapsed: PropTypes.bool.isRequired, @@ -256,35 +253,19 @@ module.exports = React.createClass({ directMessageIndicator = dm; } - // These props are injected by React DnD, - // as defined by your `collect` function above: - const isDragging = this.props.isDragging; - const connectDragSource = this.props.connectDragSource; - const connectDropTarget = this.props.connectDropTarget; - - - let ret = ( -
{ /* Only native elements can be wrapped in a DnD object. */ } - -
-
- - { directMessageIndicator } -
+ return +
+
+ + { directMessageIndicator }
-
- { label } - { badge } -
- { /* { incomingCallBox } */ } - { tooltip } -
- ); - - if (connectDropTarget) ret = connectDropTarget(ret); - if (connectDragSource) ret = connectDragSource(ret); - - return ret; +
+ { label } + { badge } +
+ { /* { incomingCallBox } */ } + { tooltip } +
; }, }); From 59f4661bfdd50360b6688e67eec4f19d244e7079 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 18 Jan 2018 18:01:31 +0000 Subject: [PATCH 2/9] Add comment --- src/components/views/rooms/RoomList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 898dfbef8c..6a7c86ab31 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -367,6 +367,9 @@ module.exports = React.createClass({ }); } + // Refresh to display the optimistic updates - this needs to be done in the + // same tick as the drag finishing otherwise the room will pop back to its + // previous position - hence no delayed refresh this.refreshRoomList(); }, From 6f0d7999129f1413f5f11d424811b85d0137cd88 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Thu, 18 Jan 2018 18:15:34 +0000 Subject: [PATCH 3/9] Fix indentation --- src/components/views/rooms/RoomList.js | 202 ++++++++++++------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 6a7c86ab31..9c66731b06 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -748,114 +748,114 @@ module.exports = React.createClass({ const self = this; return ( - -
- + +
+ - + - + - + - + - { Object.keys(self.state.lists).map((tagName) => { - if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - return ; - } - }) } + { Object.keys(self.state.lists).map((tagName) => { + if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { + return ; + } + }) } - + - -
-
+ +
+
); }, From 6c15bd859987b1d7698f55777fa90d0574e9ac04 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 19 Jan 2018 10:39:38 +0000 Subject: [PATCH 4/9] fix NPE when getGroupProfiles returns null --- src/components/views/elements/Flair.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index a487395a87..009817a340 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -107,7 +107,11 @@ export default class Flair extends React.Component { } const profiles = await this._getGroupProfiles(groups); if (!this.unmounted) { - this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})}); + this.setState({ + profiles: profiles.filter((profile) => { + return profile ? profile.avatarUrl : false; + }) + }); } } From 00dc077271ef48d9275a690af1f41607d9a9bc7f Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Fri, 19 Jan 2018 13:34:56 +0000 Subject: [PATCH 5/9] Remove react-dnd, revert fa14bc9 as no longer needed --- package.json | 2 -- src/components/structures/LoggedInView.js | 4 +--- src/components/structures/MatrixChat.js | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index dbc27b5a08..f81e72e556 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,6 @@ "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", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.14.1", diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index bebc109806..e97d9dd0a1 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -19,8 +19,6 @@ limitations under the License. import * as Matrix from 'matrix-js-sdk'; import React from 'react'; import PropTypes from 'prop-types'; -import { DragDropContext } from 'react-dnd'; -import HTML5Backend from 'react-dnd-html5-backend'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import Notifier from '../../Notifier'; @@ -347,4 +345,4 @@ const LoggedInView = React.createClass({ }, }); -export default DragDropContext(HTML5Backend)(LoggedInView); +export default LoggedInView; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 733007677b..a5181b55a7 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1066,10 +1066,10 @@ export default React.createClass({ // this if we are not scrolled up in the view. To find out, delegate to // the timeline panel. If the timeline panel doesn't exist, then we assume // it is safe to reset the timeline. - if (!self._loggedInView) { + if (!self._loggedInView || !self._loggedInView.child) { return true; } - return self._loggedInView.getDecoratedComponentInstance().canResetTimelineInRoom(roomId); + return self._loggedInView.child.canResetTimelineInRoom(roomId); }); cli.on('sync', function(state, prevState) { From 75a19227314ced9de11d775a5f6c9b311f443996 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Fri, 19 Jan 2018 14:07:13 +0000 Subject: [PATCH 6/9] Fix linting --- src/components/views/elements/Flair.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 009817a340..76566e8c4d 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -110,7 +110,7 @@ export default class Flair extends React.Component { this.setState({ profiles: profiles.filter((profile) => { return profile ? profile.avatarUrl : false; - }) + }), }); } } From 54f6d305d759c6911ba69e549a2cb55587951f64 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Fri, 19 Jan 2018 14:11:05 +0000 Subject: [PATCH 7/9] Null-guard for newTag --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 9c66731b06..85b4b52380 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -313,7 +313,7 @@ module.exports = React.createClass({ const newOrder = {}; // Is the tag ordered manually? - if (!newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { + if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { const newList = Object.assign({}, this.state.lists[newTag]); // If the room was moved "down" (increasing index) in the same list we From 6106b3ce44122fc858f44eeec5bbbfc141a59286 Mon Sep 17 00:00:00 2001 From: lukebarnard Date: Fri, 19 Jan 2018 14:12:21 +0000 Subject: [PATCH 8/9] newOrder defaults `null`, allows check for `newOrder` on line 357 --- src/components/views/rooms/RoomList.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 85b4b52380..ca1fccd1f5 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -310,7 +310,7 @@ module.exports = React.createClass({ const hasChangedSubLists = result.source.droppableId !== result.destination.droppableId; - const newOrder = {}; + let newOrder = null; // Is the tag ordered manually? if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { @@ -327,7 +327,9 @@ module.exports = React.createClass({ const nextOrder = newIndex === newList.length ? 1 : newList[offset + newIndex].tags[newTag].order; - newOrder['order'] = (prevOrder + nextOrder) / 2.0; + newOrder = { + order: (prevOrder + nextOrder) / 2.0, + }; } // More evilness: We will still be dealing with moving to favourites/low prio, From 5206c9d18b6d096ca4dc5dbfbd2363ac73b03a60 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 19 Jan 2018 22:27:48 +0000 Subject: [PATCH 9/9] Show a warning if the user attempts to leave a room that is invite only --- src/components/structures/MatrixChat.js | 22 +++++++++++++++++++++- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index a5181b55a7..d6d0b00c84 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -847,16 +847,36 @@ export default React.createClass({ }).close; }, + _leaveRoomWarnings: function(roomId) { + const roomToLeave = MatrixClientPeg.get().getRoom(roomId); + // Show a warning if there are additional complications. + const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', ''); + const warnings = []; + if (joinRules) { + const rule = joinRules.getContent().join_rule; + if (rule !== "public") { + warnings.push(( + + { _t("This room is not public. You will not be able to rejoin without an invite.") } + + )); + } + } + return warnings; + }, + _leaveRoom: function(roomId) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const roomToLeave = MatrixClientPeg.get().getRoom(roomId); + const warnings = this._leaveRoomWarnings(roomId); + Modal.createTrackedDialog('Leave room', '', QuestionDialog, { title: _t("Leave room"), description: ( { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) } + { warnings } ), onFinished: (shouldLeave) => { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 73e3f3a014..7efc5ff38e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -979,5 +979,6 @@ "Which officially provided instance you are using, if any": "Which officially provided instance you are using, if any", "Whether or not you're using the Richtext mode of the Rich Text Editor": "Whether or not you're using the Richtext mode of the Rich Text Editor", "Your homeserver's URL": "Your homeserver's URL", - "Your identity server's URL": "Your identity server's URL" + "Your identity server's URL": "Your identity server's URL", + "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite." }