From 6e63153d83aa3d2639f367e3ac093eb5ae1f70a0 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 2 Nov 2015 18:01:20 +0000 Subject: [PATCH 01/46] Add unsupported suffix on Ongoing conf notification if conf calls are unsupported in the browser --- src/skins/vector/views/organisms/RoomView.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/skins/vector/views/organisms/RoomView.js b/src/skins/vector/views/organisms/RoomView.js index 2dc63cacc5..4f15ea6182 100644 --- a/src/skins/vector/views/organisms/RoomView.js +++ b/src/skins/vector/views/organisms/RoomView.js @@ -230,9 +230,13 @@ module.exports = React.createClass({ var conferenceCallNotification = null; if (this.state.displayConfCallNotification) { + var supportedText; + if (!MatrixClientPeg.get().supportsVoip()) { + supportedText = " (unsupported)"; + } conferenceCallNotification = (
- Ongoing conference call + Ongoing conference call {supportedText}
); } From 4dac9bc1b88cc5389b8bda11a94e7bcbce43ab95 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 2 Nov 2015 23:47:04 +0000 Subject: [PATCH 02/46] Use relative rather than absolute paths for icons --- vector/index.html | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/vector/index.html b/vector/index.html index d893523ee3..d86466797d 100644 --- a/vector/index.html +++ b/vector/index.html @@ -4,26 +4,26 @@ Vector - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - + + From b1b2704bed5e0f6f78db58d076ff19fe1cff1777 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 3 Nov 2015 14:35:39 +0000 Subject: [PATCH 03/46] avoid racey NPE on first login --- src/skins/vector/views/molecules/RoomTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index 82616b5a59..bdaa621d19 100644 --- a/src/skins/vector/views/molecules/RoomTile.js +++ b/src/skins/vector/views/molecules/RoomTile.js @@ -43,12 +43,13 @@ module.exports = React.createClass({ render: function() { var myUserId = MatrixClientPeg.get().credentials.userId; + var me = this.props.room.currentState.members[myUserId]; var classes = classNames({ 'mx_RoomTile': true, 'mx_RoomTile_selected': this.props.selected, 'mx_RoomTile_unread': this.props.unread, 'mx_RoomTile_highlight': this.props.highlight, - 'mx_RoomTile_invited': this.props.room.currentState.members[myUserId].membership == 'invite' + 'mx_RoomTile_invited': (me && me.membership == 'invite'), }); var name; From 27cf9cf561bfa0e26c005194ac2e1075c5721643 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 3 Nov 2015 14:35:48 +0000 Subject: [PATCH 04/46] put invites above recents --- src/skins/vector/views/organisms/RoomList.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/skins/vector/views/organisms/RoomList.js b/src/skins/vector/views/organisms/RoomList.js index dc958a4ecd..1d4ee86b75 100644 --- a/src/skins/vector/views/organisms/RoomList.js +++ b/src/skins/vector/views/organisms/RoomList.js @@ -62,11 +62,10 @@ module.exports = React.createClass({
{ expandButton } { callElement } + { invites }

Favourites

- { invites } -

{ recentsLabel }

{this.makeRoomTiles(this.state.roomList, false)} From 8b9b268ec047c418504972a2b6f8517fb4e4950f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 3 Nov 2015 14:35:55 +0000 Subject: [PATCH 05/46] make our state explicit --- src/controllers/organisms/RoomList.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index 964a26481a..514ae7747b 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -28,6 +28,14 @@ var CallHandler = require("matrix-react-sdk/lib/CallHandler"); var HIDE_CONFERENCE_CHANS = true; module.exports = { + getInitialState: function() { + return { + activityMap: null, + inviteList: [], + roomList: [], + } + }, + componentWillMount: function() { var cli = MatrixClientPeg.get(); cli.on("Room", this.onRoom); From 7fe7af6026b0e121984ba95ea465edf202ab6ce2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 4 Nov 2015 00:19:37 +0000 Subject: [PATCH 06/46] refactor out the sections of the RoomList into RoomSubLists. Start wiring up tags --- package.json | 3 +- src/controllers/organisms/RoomList.js | 93 ++++++------- src/skins/vector/css/organisms/LeftPanel.css | 4 + src/skins/vector/css/organisms/RoomList.css | 18 --- .../vector/css/organisms/RoomSubList.css | 32 +++++ src/skins/vector/skindex.js | 3 + src/skins/vector/views/organisms/LeftPanel.js | 49 +++++++ src/skins/vector/views/organisms/RoomList.js | 71 +++++----- .../vector/views/organisms/RoomSubList.js | 124 ++++++++++++++++++ 9 files changed, 295 insertions(+), 102 deletions(-) create mode 100644 src/skins/vector/css/organisms/RoomSubList.css create mode 100644 src/skins/vector/views/organisms/RoomSubList.js diff --git a/package.json b/package.json index eb9c3aff95..96176843b3 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "q": "^1.4.1", "react": "^0.13.3", "react-loader": "^1.4.0", - "sanitize-html": "^1.11.1" + "react-dnd": "^1.1.8", + "sanitize-html": "^1.0.0" }, "devDependencies": { "babel": "^5.8.23", diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index 514ae7747b..38a0c652c4 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -23,7 +23,6 @@ var dis = require("matrix-react-sdk/lib/dispatcher"); var sdk = require('matrix-react-sdk'); var VectorConferenceHandler = require("../../modules/VectorConferenceHandler"); -var CallHandler = require("matrix-react-sdk/lib/CallHandler"); var HIDE_CONFERENCE_CHANS = true; @@ -31,8 +30,7 @@ module.exports = { getInitialState: function() { return { activityMap: null, - inviteList: [], - roomList: [], + lists: {}, } }, @@ -41,6 +39,7 @@ module.exports = { cli.on("Room", this.onRoom); cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.name", this.onRoomName); + cli.on("Room.tags", this.onRoomTags); cli.on("RoomState.events", this.onRoomStateEvents); cli.on("RoomMember.name", this.onRoomMemberName); @@ -55,11 +54,6 @@ module.exports = { onAction: function(payload) { switch (payload.action) { - // listen for call state changes to prod the render method, which - // may hide the global CallView if the call it is tracking is dead - case 'call_state': - this._recheckCallElement(this.props.selectedRoom); - break; case 'view_tooltip': this.tooltip = payload.tooltip; this._repositionTooltip(); @@ -80,7 +74,6 @@ module.exports = { componentWillReceiveProps: function(newProps) { this.state.activityMap[newProps.selectedRoom] = undefined; - this._recheckCallElement(newProps.selectedRoom); this.setState({ activityMap: this.state.activityMap }); @@ -117,6 +110,10 @@ module.exports = { this.refreshRoomList(); }, + onRoomTags: function(room) { + this.refreshRoomList(); + }, + onRoomStateEvents: function(ev, state) { setTimeout(this.refreshRoomList, 0); }, @@ -125,26 +122,31 @@ module.exports = { setTimeout(this.refreshRoomList, 0); }, - refreshRoomList: function() { + // TODO: rather than bluntly regenerating and re-sorting everything + // every time we see any kind of room change from the JS SDK + // we could do incremental updates on our copy of the state + // based on the room which has actually changed. This would stop + // us re-rendering all the sublists every time anything changes anywhere + // in the state of the client. this.setState(this.getRoomLists()); }, getRoomLists: function() { - var s = {}; - var inviteList = []; - s.roomList = RoomListSorter.mostRecentActivityFirst( - MatrixClientPeg.get().getRooms().filter(function(room) { - var me = room.getMember(MatrixClientPeg.get().credentials.userId); + var s = { lists: {} }; - if (me && me.membership == "invite") { - inviteList.push(room); - return false; - } + MatrixClientPeg.get().getRooms().forEach(function(room) { + var me = room.getMember(MatrixClientPeg.get().credentials.userId); + if (me && me.membership == "invite") { + s.lists["invites"] = s.lists["invites"] || []; + s.lists["invites"].push(room); + } + else { var shouldShowRoom = ( me && (me.membership == "join") ); + // hiding conf rooms only ever toggles shouldShowRoom to false if (shouldShowRoom && HIDE_CONFERENCE_CHANS) { // we want to hide the 1:1 conf<->user room and not the group chat @@ -159,23 +161,27 @@ module.exports = { } } } - return shouldShowRoom; - }) - ); - s.inviteList = RoomListSorter.mostRecentActivityFirst(inviteList); - return s; - }, - _recheckCallElement: function(selectedRoomId) { - // if we aren't viewing a room with an ongoing call, but there is an - // active call, show the call element - we need to do this to make - // audio/video not crap out - var activeCall = CallHandler.getAnyActiveCall(); - var callForRoom = CallHandler.getCallForRoom(selectedRoomId); - var showCall = (activeCall && !callForRoom); - this.setState({ - show_call_element: showCall + if (shouldShowRoom) { + var tagNames = Object.keys(room.tags); + if (tagNames.length) { + for (var i = 0; i < tagNames.length; i++) { + var tagName = tagNames[i]; + s.lists[tagName] = s.lists[tagName] || []; + s.lists[tagNames[i]].push(room); + } + } + else { + s.lists["recents"] = s.lists["recents"] || []; + s.lists["recents"].push(room); + } + } + } }); + + // we actually apply the sorting to this when receiving the prop in RoomSubLists. + + return s; }, _repositionTooltip: function(e) { @@ -184,23 +190,4 @@ module.exports = { this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.scrollTop) + "px"; } }, - - makeRoomTiles: function(list, isInvite) { - var self = this; - var RoomTile = sdk.getComponent("molecules.RoomTile"); - return list.map(function(room) { - var selected = room.roomId == self.props.selectedRoom; - return ( - - ); - }); - } }; diff --git a/src/skins/vector/css/organisms/LeftPanel.css b/src/skins/vector/css/organisms/LeftPanel.css index 67f00c358d..d755b2164e 100644 --- a/src/skins/vector/css/organisms/LeftPanel.css +++ b/src/skins/vector/css/organisms/LeftPanel.css @@ -34,6 +34,10 @@ limitations under the License. cursor: pointer; } +.mx_LeftPanel_callView { + +} + .mx_LeftPanel .mx_RoomList { -webkit-box-ordinal-group: 1; -moz-box-ordinal-group: 1; diff --git a/src/skins/vector/css/organisms/RoomList.css b/src/skins/vector/css/organisms/RoomList.css index 34ebd1dbfb..9512354469 100644 --- a/src/skins/vector/css/organisms/RoomList.css +++ b/src/skins/vector/css/organisms/RoomList.css @@ -18,27 +18,9 @@ limitations under the License. padding-top: 24px; } -.mx_RoomList_invites, -.mx_RoomList_recents { - display: table; - table-layout: fixed; - width: 100%; -} - .mx_RoomList_expandButton { margin-left: 8px; cursor: pointer; padding-left: 12px; padding-right: 12px; } - -.mx_RoomList h2 { - text-transform: uppercase; - color: #3d3b39; - font-weight: 600; - font-size: 14px; - padding-left: 12px; - padding-right: 12px; - margin-top: 8px; - margin-bottom: 4px; -} diff --git a/src/skins/vector/css/organisms/RoomSubList.css b/src/skins/vector/css/organisms/RoomSubList.css new file mode 100644 index 0000000000..43f453fb54 --- /dev/null +++ b/src/skins/vector/css/organisms/RoomSubList.css @@ -0,0 +1,32 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_RoomSubList { + display: table; + table-layout: fixed; + width: 100%; +} + +.mx_RoomSubList_label { + text-transform: uppercase; + color: #3d3b39; + font-weight: 600; + font-size: 14px; + padding-left: 12px; + padding-right: 12px; + margin-top: 8px; + margin-bottom: 4px; +} diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index e715656c0e..cf279c872d 100644 --- a/src/skins/vector/skindex.js +++ b/src/skins/vector/skindex.js @@ -30,6 +30,7 @@ skin['atoms.LogoutButton'] = require('./views/atoms/LogoutButton'); skin['atoms.MemberAvatar'] = require('./views/atoms/MemberAvatar'); skin['atoms.MessageTimestamp'] = require('./views/atoms/MessageTimestamp'); skin['atoms.RoomAvatar'] = require('./views/atoms/RoomAvatar'); +skin['atoms.Spinner'] = require('./views/atoms/Spinner'); skin['atoms.create_room.CreateRoomButton'] = require('./views/atoms/create_room/CreateRoomButton'); skin['atoms.create_room.Presets'] = require('./views/atoms/create_room/Presets'); skin['atoms.create_room.RoomAlias'] = require('./views/atoms/create_room/RoomAlias'); @@ -80,9 +81,11 @@ skin['organisms.QuestionDialog'] = require('./views/organisms/QuestionDialog'); skin['organisms.RightPanel'] = require('./views/organisms/RightPanel'); skin['organisms.RoomDirectory'] = require('./views/organisms/RoomDirectory'); skin['organisms.RoomList'] = require('./views/organisms/RoomList'); +skin['organisms.RoomSubList'] = require('./views/organisms/RoomSubList'); skin['organisms.RoomView'] = require('./views/organisms/RoomView'); skin['organisms.UserSettings'] = require('./views/organisms/UserSettings'); skin['organisms.ViewSource'] = require('./views/organisms/ViewSource'); +skin['pages.CompatibilityPage'] = require('./views/pages/CompatibilityPage'); skin['pages.MatrixChat'] = require('./views/pages/MatrixChat'); skin['templates.Login'] = require('./views/templates/Login'); skin['templates.Register'] = require('./views/templates/Register'); diff --git a/src/skins/vector/views/organisms/LeftPanel.js b/src/skins/vector/views/organisms/LeftPanel.js index ec25f9342a..8f4b510a87 100644 --- a/src/skins/vector/views/organisms/LeftPanel.js +++ b/src/skins/vector/views/organisms/LeftPanel.js @@ -20,9 +20,51 @@ var React = require('react'); var sdk = require('matrix-react-sdk') var dis = require('matrix-react-sdk/lib/dispatcher'); +var CallHandler = require("matrix-react-sdk/lib/CallHandler"); + module.exports = React.createClass({ displayName: 'LeftPanel', + getInitialState: function() { + return { + showCallElement: null, + }; + }, + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + + componentWillReceiveProps: function(newProps) { + this._recheckCallElement(newProps.selectedRoom); + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + onAction: function(payload) { + switch (payload.action) { + // listen for call state changes to prod the render method, which + // may hide the global CallView if the call it is tracking is dead + case 'call_state': + this._recheckCallElement(this.props.selectedRoom); + break; + } + }, + + _recheckCallElement: function(selectedRoomId) { + // if we aren't viewing a room with an ongoing call, but there is an + // active call, show the call element - we need to do this to make + // audio/video not crap out + var activeCall = CallHandler.getAnyActiveCall(); + var callForRoom = CallHandler.getCallForRoom(selectedRoomId); + var showCall = (activeCall && !callForRoom); + this.setState({ + showCallElement: showCall + }); + }, + onHideClick: function() { dis.dispatch({ action: 'hide_left_panel', @@ -44,10 +86,17 @@ module.exports = React.createClass({ // collapseButton = < } + var callPreview; + if (this.state.showCallElement) { + var CallView = sdk.getComponent('molecules.voip.CallView'); + callPreview = + } + return ( diff --git a/src/skins/vector/views/organisms/RoomList.js b/src/skins/vector/views/organisms/RoomList.js index 1d4ee86b75..81d23867d3 100644 --- a/src/skins/vector/views/organisms/RoomList.js +++ b/src/skins/vector/views/organisms/RoomList.js @@ -33,46 +33,57 @@ module.exports = React.createClass({ }, render: function() { - var CallView = sdk.getComponent('molecules.voip.CallView'); - var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget'); - - var callElement; - if (this.state.show_call_element) { - callElement = - } - var expandButton = this.props.collapsed ? > : null; - var invitesLabel = this.props.collapsed ? null : "Invites"; - var recentsLabel = this.props.collapsed ? null : "Recent"; - - var invites; - if (this.state.inviteList.length) { - invites =
-

{ invitesLabel }

-
- {this.makeRoomTiles(this.state.inviteList, true)} -
-
- } + var RoomSubList = sdk.getComponent('organisms.RoomSubList'); return (
{ expandButton } - { callElement } - { invites } -

Favourites

- -

{ recentsLabel }

-
- {this.makeRoomTiles(this.state.roomList, false)} -
+ -

Archive

- + + + + + + +
); } diff --git a/src/skins/vector/views/organisms/RoomSubList.js b/src/skins/vector/views/organisms/RoomSubList.js new file mode 100644 index 0000000000..b8747ecf22 --- /dev/null +++ b/src/skins/vector/views/organisms/RoomSubList.js @@ -0,0 +1,124 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +var React = require('react'); +var sdk = require('matrix-react-sdk') +var dis = require('matrix-react-sdk/lib/dispatcher'); + +module.exports = React.createClass({ + displayName: 'RoomSubList', + + propTypes: { + list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + label: React.PropTypes.string.isRequired, + tagname: React.PropTypes.string, + editable: React.PropTypes.bool, + order: React.PropTypes.string.isRequired, + selectedRoom: React.PropTypes.string.isRequired, + activityMap: React.PropTypes.object.isRequired, + collapsed: React.PropTypes.bool.isRequired + }, + + getInitialState: function() { + return { + sortedList: [], + }; + }, + + componentWillMount: function() { + this.sortList(this.props.list, this.props.order); + }, + + componentWillReceiveProps: function(newProps) { + // order the room list appropriately before we re-render + this.sortList(newProps.list, newProps.order); + }, + + tsOfNewestEvent: function(room) { + if (room.timeline.length) { + return room.timeline[room.timeline.length - 1].getTs(); + } + else { + return Number.MAX_SAFE_INTEGER; + } + }, + + // TODO: factor the comparators back out into a generic comparator + // so that view_prev_room and view_next_room can do the right thing + + recentsComparator: function(roomA, roomB) { + return this.tsOfNewestEvent(roomB) - this.tsOfNewestEvent(roomA); + }, + + manualComparator: function(roomA, roomB) { + var a = roomA.tags[this.props.tagname].order; + var b = roomB.tags[this.props.tagname].order; + return a == b ? this.recentsComparator(roomA, roomB) : ( a > b ? 1 : -1); + }, + + sortList: function(list, order) { + var comparator; + list = list || []; + if (order === "manual") comparator = this.manualComparator; + if (order === "recent") comparator = this.recentsComparator; + + this.setState({ sortedList: list.sort(comparator) }); + }, + + makeRoomTiles: function() { + var self = this; + var RoomTile = sdk.getComponent("molecules.RoomTile"); + return this.state.sortedList.map(function(room) { + var selected = room.roomId == self.props.selectedRoom; + return ( + + ); + }); + }, + + render: function() { + var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget'); + + var label = this.props.collapsed ? null : this.props.label; + + if (this.state.sortedList.length > 0 || this.props.editable) { + return ( +
+

{ this.props.label }

+
+ { this.makeRoomTiles() } +
+
+ ); + } + else { + return ( +
+
+ ); + } + } +}); + From 61e55b3ca3a3f3d8dc0740384c3b7a007fb536c1 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 4 Nov 2015 02:25:08 +0000 Subject: [PATCH 07/46] implement most of drag & drop. --- src/skins/vector/views/molecules/RoomTile.js | 114 +++++++++++++++++- .../vector/views/organisms/RoomSubList.js | 89 +++++++++++++- src/skins/vector/views/pages/MatrixChat.js | 6 +- 3 files changed, 202 insertions(+), 7 deletions(-) diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index bdaa621d19..383c08d161 100644 --- a/src/skins/vector/views/molecules/RoomTile.js +++ b/src/skins/vector/views/molecules/RoomTile.js @@ -17,6 +17,8 @@ limitations under the License. 'use strict'; var React = require('react'); +var DragSource = require('react-dnd').DragSource; +var DropTarget = require('react-dnd').DropTarget; var classNames = require('classnames'); var RoomTileController = require('matrix-react-sdk/lib/controllers/molecules/RoomTile') @@ -25,10 +27,89 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var sdk = require('matrix-react-sdk') -module.exports = React.createClass({ +/** + * Specifies the drag source contract. + * Only `beginDrag` function is required. + */ +var roomTileSource = { + beginDrag: function (props) { + // Return the data describing the dragged item + var item = { + room: props.room, + originalList: props.roomSubList, + originalIndex: props.roomSubList.findRoomTile(props.room).index, + targetList: props.roomSubList, // at first target is same as original + }; + + console.log("roomTile beginDrag for " + item.room.roomId); + + return item; + }, + + endDrag: function (props, monitor, component) { + var item = monitor.getItem(); + var dropResult = monitor.getDropResult(); + + console.log("roomTile endDrag for " + item.room.roomId + " with didDrop=" + monitor.didDrop()); + + if (!monitor.didDrop() || !item.targetList.props.editable) { + props.roomSubList.moveRoomTile(item.room, item.originalIndex); + if (item.targetList && item.targetList !== item.originalList) { + item.targetList.removeRoomTile(item.room); + } + return; + } + else { + // if it's not manual ordering, we'll need to position the tile correctly here according to the right ordering + + // When dropped on a compatible target, actually set the right tags for the new ordering + // persistNewOrder(item.room, dropResult.listId); + } + } +}; + +var roomTileTarget = { + canDrop: function() { + return false; + }, + + hover: function(props, monitor) { + var item = monitor.getItem(); + console.log("hovering on room " + props.room.roomId + ", isOver=" + monitor.isOver()); + + //console.log("item.targetList=" + item.targetList + ", roomSubList=" + props.roomSubList); + + if (item.targetList !== props.roomSubList) { + // we've switched target, so remove the tile from the previous target. + // n.b. the previous target might actually be the source list. + item.targetList.removeRoomTile(item.room); + item.targetList = props.roomSubList; + } + + if (item.targetList.props.order === 'manual' && item.room.roomId !== props.room.roomId) { + var roomTile = props.roomSubList.findRoomTile(props.room); + props.roomSubList.moveRoomTile(item.room, roomTile.index); + } + }, +}; + +var RoomTile = React.createClass({ displayName: 'RoomTile', mixins: [RoomTileController], + propTypes: { + connectDragSource: React.PropTypes.func.isRequired, + connectDropTarget: React.PropTypes.func.isRequired, + isDragging: React.PropTypes.bool.isRequired, + room: React.PropTypes.object.isRequired, + collapsed: React.PropTypes.bool.isRequired, + selected: React.PropTypes.bool.isRequired, + unread: React.PropTypes.bool.isRequired, + highlight: React.PropTypes.bool.isRequired, + isInvite: React.PropTypes.bool.isRequired, + roomSubList: React.PropTypes.object.isRequired, + }, + getInitialState: function() { return( { hover : false }); }, @@ -92,7 +173,14 @@ module.exports = React.createClass({ } var RoomAvatar = sdk.getComponent('atoms.RoomAvatar'); - return ( + + // These props are injected by React DnD, + // as defined by your `collect` function above: + var isDragging = this.props.isDragging; + var connectDragSource = this.props.connectDragSource; + var connectDropTarget = this.props.connectDropTarget; + + return connectDragSource(connectDropTarget(
@@ -100,6 +188,26 @@ module.exports = React.createClass({
{ label }
- ); + )); } }); + +// Export the wrapped version, inlining the 'collect' functions +// to more closely resemble the ES7 +module.exports = +DropTarget('RoomTile', roomTileTarget, function(connect) { + return { + // Call this function inside render() + // to let React DnD handle the drag events: + connectDropTarget: connect.dropTarget(), + } +})( +DragSource('RoomTile', roomTileSource, function(connect, monitor) { + return { + // Call this function inside render() + // to let React DnD handle the drag events: + connectDragSource: connect.dragSource(), + // You can ask the monitor about the current drag state: + isDragging: monitor.isDragging() + }; +})(RoomTile)); \ No newline at end of file diff --git a/src/skins/vector/views/organisms/RoomSubList.js b/src/skins/vector/views/organisms/RoomSubList.js index b8747ecf22..68d55c7b90 100644 --- a/src/skins/vector/views/organisms/RoomSubList.js +++ b/src/skins/vector/views/organisms/RoomSubList.js @@ -17,10 +17,32 @@ limitations under the License. 'use strict'; var React = require('react'); +var DropTarget = require('react-dnd').DropTarget; var sdk = require('matrix-react-sdk') var dis = require('matrix-react-sdk/lib/dispatcher'); -module.exports = React.createClass({ +var roomListTarget = { + canDrop: function() { + return true; + }, + + hover: function(props, monitor, component) { + var item = monitor.getItem(); + + if (component.state.sortedList.length == 0 && props.editable) { + console.log("hovering on sublist " + props.label + ", isOver=" + monitor.isOver()); + + if (item.targetList !== component) { + item.targetList.removeRoomTile(item.room); + item.targetList = component; + } + + component.moveRoomTile(item.room, 0); + } + }, +}; + +var RoomSubList = React.createClass({ displayName: 'RoomSubList', propTypes: { @@ -80,14 +102,64 @@ module.exports = React.createClass({ this.setState({ sortedList: list.sort(comparator) }); }, + moveRoomTile: function(room, atIndex) { + console.log("moveRoomTile: id " + room.roomId + ", atIndex " + atIndex); + //console.log("moveRoomTile before: " + JSON.stringify(this.state.rooms)); + var found = this.findRoomTile(room); + var rooms = this.state.sortedList; + if (found.room) { + console.log("removing at index " + found.index + " and adding at index " + atIndex); + rooms.splice(found.index, 1); + rooms.splice(atIndex, 0, found.room); + } + else { + console.log("Adding at index " + atIndex); + rooms.splice(atIndex, 0, room); + } + this.setState({ sortedList: rooms }); + // console.log("moveRoomTile after: " + JSON.stringify(this.state.rooms)); + }, + + // XXX: this isn't invoked via a property method but indirectly via + // the roomList property method. Unsure how evil this is. + removeRoomTile: function(room) { + console.log("remove room " + room.roomId); + var found = this.findRoomTile(room); + var rooms = this.state.sortedList; + if (found.room) { + rooms.splice(found.index, 1); + } + else { + console.log*("Can't remove room " + room.roomId + " - can't find it"); + } + this.setState({ sortedList: rooms }); + }, + + findRoomTile: function(room) { + var index = this.state.sortedList.indexOf(room); + if (index >= 0) { + console.log("found: room: " + room + " with id " + room.roomId); + } + else { + console.log("didn't find room"); + room = null; + } + return ({ + room: room, + index: index, + }); + }, + makeRoomTiles: function() { var self = this; var RoomTile = sdk.getComponent("molecules.RoomTile"); return this.state.sortedList.map(function(room) { var selected = room.roomId == self.props.selectedRoom; + // XXX: is it evil to pass in self as a prop to RoomTile? return ( 0 || this.props.editable) { - return ( + return connectDropTarget(
-

{ this.props.label }

+

{ this.props.collapsed ? '' : this.props.label }

{ this.makeRoomTiles() }
@@ -122,3 +197,11 @@ module.exports = React.createClass({ } }); +// Export the wrapped version, inlining the 'collect' functions +// to more closely resemble the ES7 +module.exports = +DropTarget('RoomTile', roomListTarget, function(connect) { + return { + connectDropTarget: connect.dropTarget(), + } +})(RoomSubList); diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index 0cf754c23a..f34b6d4fd5 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -17,6 +17,8 @@ limitations under the License. 'use strict'; var React = require('react'); +var DragDropContext = require('react-dnd').DragDropContext; +var HTML5Backend = require('react-dnd/modules/backends/HTML5'); var sdk = require('matrix-react-sdk') var MatrixChatController = require('matrix-react-sdk/lib/controllers/pages/MatrixChat') @@ -28,7 +30,7 @@ var dis = require('matrix-react-sdk/lib/dispatcher'); var Matrix = require("matrix-js-sdk"); var ContextualMenu = require("../../../../ContextualMenu"); -module.exports = React.createClass({ +var MatrixChat = React.createClass({ displayName: 'MatrixChat', mixins: [MatrixChatController], @@ -172,3 +174,5 @@ module.exports = React.createClass({ } } }); + +module.exports = DragDropContext(HTML5Backend)(MatrixChat); \ No newline at end of file From 942659df0dbe5754f53fe6084af4302d3ae0ff4f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 Nov 2015 14:15:57 +0000 Subject: [PATCH 08/46] Work around the bug where some channels have no name from the js sdk which was causing vector to exception and never load. --- src/skins/vector/views/molecules/RoomTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index 82616b5a59..ece4804321 100644 --- a/src/skins/vector/views/molecules/RoomTile.js +++ b/src/skins/vector/views/molecules/RoomTile.js @@ -56,7 +56,8 @@ module.exports = React.createClass({ name = this.props.room.getMember(MatrixClientPeg.get().credentials.userId).events.member.getSender(); } else { - name = this.props.room.name; + // XXX: We should never display raw room IDs, but sometimes the room name js sdk gives is undefined + name = this.props.room.name || this.props.room.roomId; } name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon From 8842147ec3e54088fd655d1713d0f675acb64095 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 Nov 2015 11:21:45 +0000 Subject: [PATCH 09/46] skin RoomDropTarget correctly --- src/skins/vector/css/hide.css | 1 - .../vector/css/molecules/RoomDropTarget.css | 33 ++++++++++++++----- src/skins/vector/css/molecules/RoomTile.css | 14 ++++---- src/skins/vector/css/organisms/LeftPanel.css | 8 +++-- src/skins/vector/css/organisms/RoomList.css | 1 + .../vector/views/molecules/RoomDropTarget.js | 9 ++--- src/skins/vector/views/molecules/RoomTile.js | 21 +++++++++--- src/skins/vector/views/organisms/RoomList.js | 8 +++-- .../vector/views/organisms/RoomSubList.js | 10 +++++- 9 files changed, 72 insertions(+), 33 deletions(-) diff --git a/src/skins/vector/css/hide.css b/src/skins/vector/css/hide.css index 7d8ee30294..f84a35b313 100644 --- a/src/skins/vector/css/hide.css +++ b/src/skins/vector/css/hide.css @@ -1,4 +1,3 @@ -.mx_RoomDropTarget, .mx_RoomSettings_encrypt, .mx_CreateRoom_encrypt, .mx_RightPanel_filebutton diff --git a/src/skins/vector/css/molecules/RoomDropTarget.css b/src/skins/vector/css/molecules/RoomDropTarget.css index c42d44995d..549fa609ec 100644 --- a/src/skins/vector/css/molecules/RoomDropTarget.css +++ b/src/skins/vector/css/molecules/RoomDropTarget.css @@ -16,12 +16,29 @@ limitations under the License. .mx_RoomDropTarget { font-size: 14px; - text-align: center; - margin-left: 8px; - margin-right: 8px; - padding-top: 16px; - padding-bottom: 16px; - background-color: #fbfbfb; - border: 1px dashed #d7d7d7; - border-radius: 8px; + margin-left: 10px; + margin-right: 15px; + padding-top: 5px; + padding-bottom: 5px; + border: 1px dashed #76cfa6; + color: #454545; + background-color: rgba(255,255,255,0.5); + border-radius: 4px; +} + +.mx_RoomDropTarget_avatar { + background-color: #fff; + border-radius: 24px; + width: 24px; + height: 24px; + float: left; + margin-left: 7px; + margin-right: 8px; +} + +.mx_RoomDropTarget_label { + position: relative; + margin-top: 3px; + line-height: 21px; + z-index: 1; } diff --git a/src/skins/vector/css/molecules/RoomTile.css b/src/skins/vector/css/molecules/RoomTile.css index f2c1daadb3..b62552252b 100644 --- a/src/skins/vector/css/molecules/RoomTile.css +++ b/src/skins/vector/css/molecules/RoomTile.css @@ -22,7 +22,6 @@ limitations under the License. .mx_RoomTile_avatar { display: table-cell; - background: #eaf5f0; padding-right: 8px; padding-top: 4px; padding-bottom: 2px; @@ -43,13 +42,11 @@ limitations under the License. overflow: hidden; text-overflow: ellipsis; padding-right: 16px; - color: #454545; - opacity: 0.8; + color: rgba(69, 69, 69, 0.8); } .mx_RoomTile_invite { - opacity: 0.5; - font-weight: normal; + color: rgba(69, 69, 69, 0.5); } .collapsed .mx_RoomTile_name { @@ -106,15 +103,16 @@ limitations under the License. .mx_RoomTile_unread, .mx_RoomTile_highlight, -.mx_RoomTile_invited +.mx_RoomTile_selected { font-weight: bold; } -.mx_RoomTile_selected { +.mx_RoomTile_selected .mx_RoomTile_name { + color: #76cfa6 ! important; } -.mx_RoomTile.mx_RoomTile_selected { +.mx_RoomTile.mx_RoomTile_selected .mx_RoomTile_name { background: url('img/selected.png'); background-repeat: no-repeat; background-position: right center; diff --git a/src/skins/vector/css/organisms/LeftPanel.css b/src/skins/vector/css/organisms/LeftPanel.css index d755b2164e..738b07829d 100644 --- a/src/skins/vector/css/organisms/LeftPanel.css +++ b/src/skins/vector/css/organisms/LeftPanel.css @@ -57,8 +57,10 @@ limitations under the License. -webkit-order: 3; order: 3; - -webkit-flex: 0 0 126px; - flex: 0 0 126px; + -webkit-flex: 0 0 140px; + flex: 0 0 140px; + + background-color: rgba(118,207,166,0.19); } .mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile { @@ -66,7 +68,7 @@ limitations under the License. } .mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options { - margin-top: 12px; + margin-top: 17px; width: 100%; } diff --git a/src/skins/vector/css/organisms/RoomList.css b/src/skins/vector/css/organisms/RoomList.css index 9512354469..7f5e2272cd 100644 --- a/src/skins/vector/css/organisms/RoomList.css +++ b/src/skins/vector/css/organisms/RoomList.css @@ -16,6 +16,7 @@ limitations under the License. .mx_RoomList { padding-top: 24px; + padding-bottom: 12px; } .mx_RoomList_expandButton { diff --git a/src/skins/vector/views/molecules/RoomDropTarget.js b/src/skins/vector/views/molecules/RoomDropTarget.js index b1e1507796..c1a2a9548f 100644 --- a/src/skins/vector/views/molecules/RoomDropTarget.js +++ b/src/skins/vector/views/molecules/RoomDropTarget.js @@ -18,15 +18,16 @@ limitations under the License. var React = require('react'); -//var RoomDropTargetController = require('matrix-react-sdk/lib/controllers/molecules/RoomDropTargetController') - module.exports = React.createClass({ displayName: 'RoomDropTarget', - // mixins: [RoomDropTargetController], + render: function() { return (
- {this.props.text} +
+
+ { this.props.label } +
); } diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index 383c08d161..ad198694e2 100644 --- a/src/skins/vector/views/molecules/RoomTile.js +++ b/src/skins/vector/views/molecules/RoomTile.js @@ -32,6 +32,10 @@ var sdk = require('matrix-react-sdk') * Only `beginDrag` function is required. */ var roomTileSource = { + canDrag: function(props, monitor) { + return props.roomSubList.props.editable; + }, + beginDrag: function (props) { // Return the data describing the dragged item var item = { @@ -60,8 +64,6 @@ var roomTileSource = { return; } else { - // if it's not manual ordering, we'll need to position the tile correctly here according to the right ordering - // When dropped on a compatible target, actually set the right tags for the new ordering // persistNewOrder(item.room, dropResult.listId); } @@ -86,9 +88,18 @@ var roomTileTarget = { item.targetList = props.roomSubList; } - if (item.targetList.props.order === 'manual' && item.room.roomId !== props.room.roomId) { - var roomTile = props.roomSubList.findRoomTile(props.room); - props.roomSubList.moveRoomTile(item.room, roomTile.index); + if (item.targetList.props.order === 'manual') { + if (item.room.roomId !== props.room.roomId) { + var roomTile = props.roomSubList.findRoomTile(props.room); + props.roomSubList.moveRoomTile(item.room, roomTile.index); + } + } + else { + if (!props.roomSubList.findRoomTile(item.room).room) { + // add to the list in the right place + props.roomSubList.moveRoomTile(item.room, 0); + props.roomSubList.sortList(); + } } }, }; diff --git a/src/skins/vector/views/organisms/RoomList.js b/src/skins/vector/views/organisms/RoomList.js index 81d23867d3..2e46c42fbc 100644 --- a/src/skins/vector/views/organisms/RoomList.js +++ b/src/skins/vector/views/organisms/RoomList.js @@ -54,6 +54,7 @@ module.exports = React.createClass({ - ; + } + if (this.state.sortedList.length > 0 || this.props.editable) { return connectDropTarget(

{ this.props.collapsed ? '' : this.props.label }

+ { target } { this.makeRoomTiles() }
From 804af341ac4f929329d51e60d5c35fa2ba6391f1 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 5 Nov 2015 14:52:44 +0000 Subject: [PATCH 10/46] Add a 'connection lost' bar. --- src/controllers/organisms/RoomView.js | 9 ++++++++ src/skins/vector/css/organisms/RoomView.css | 24 ++++++++++++++++++++ src/skins/vector/views/organisms/RoomView.js | 19 +++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index f5a8d28f26..15d67b1555 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -41,6 +41,7 @@ module.exports = { draggingFile: false, searching: false, searchResults: null, + syncState: MatrixClientPeg.get().getSyncState() } }, @@ -50,6 +51,7 @@ module.exports = { MatrixClientPeg.get().on("Room.name", this.onRoomName); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().on("sync", this.onSyncStateChange); this.atBottom = true; }, @@ -67,6 +69,7 @@ module.exports = { MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange); } }, @@ -102,6 +105,12 @@ module.exports = { } }, + onSyncStateChange: function(state) { + this.setState({ + syncState: state + }); + }, + // MatrixRoom still showing the messages from the old room? // Set the key to the room_id. Sadly you can no longer get at // the key from inside the component, or we'd check this in code. diff --git a/src/skins/vector/css/organisms/RoomView.css b/src/skins/vector/css/organisms/RoomView.css index d564b08629..961a79453a 100644 --- a/src/skins/vector/css/organisms/RoomView.css +++ b/src/skins/vector/css/organisms/RoomView.css @@ -185,6 +185,30 @@ limitations under the License. vertical-align: middle; } +.mx_RoomView_connectionLostBar { + margin-top: 5px; +} + +.mx_RoomView_connectionLostBar img { + padding-left: 10px; + padding-right: 22px; + vertical-align: middle; + float: left; +} + +.mx_RoomView_connectionLostBar_textArea { + float: left; +} + +.mx_RoomView_connectionLostBar_title { + color: #f00; +} + +.mx_RoomView_connectionLostBar_desc { + color: #ddd; + font-size: 12px; +} + .mx_RoomView_typingBar { margin-top: 10px; margin-left: 54px; diff --git a/src/skins/vector/views/organisms/RoomView.js b/src/skins/vector/views/organisms/RoomView.js index 4f15ea6182..86b200cf25 100644 --- a/src/skins/vector/views/organisms/RoomView.js +++ b/src/skins/vector/views/organisms/RoomView.js @@ -197,9 +197,26 @@ module.exports = React.createClass({ } else { var typingString = this.getWhoIsTypingString(); var unreadMsgs = this.getUnreadMessagesString(); + // no conn bar trumps unread count since you can't get unread messages + // without a connection! (technically may already have some but meh) + if (this.state.syncState === "ERROR") { + statusBar = ( +
+ +
+ + Internet connection has been lost. + +
+ Sent messages will be stored until your connection has resumed. +
+
+
+ ); + } // unread count trumps who is typing since the unread count is only // set when you've scrolled up - if (unreadMsgs) { + else if (unreadMsgs) { statusBar = (
From 3a8c263e8e1927caf1aa4609fe08c0f93156853d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 5 Nov 2015 15:59:03 +0000 Subject: [PATCH 11/46] Add resending bar (and resend all option) Factor out resend logic which was in the context menu into a separate Resend file (it shouldn't be in the skin, but it also isn't really suitable for a controller given 2 different views invoke it..) --- src/Resend.js | 24 +++++++++++++++ src/controllers/organisms/RoomView.js | 30 +++++++++++++++++-- src/skins/vector/css/organisms/RoomView.css | 4 +++ .../views/molecules/MessageContextMenu.js | 16 ++-------- src/skins/vector/views/organisms/RoomView.js | 20 +++++++++++++ 5 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 src/Resend.js diff --git a/src/Resend.js b/src/Resend.js new file mode 100644 index 0000000000..52b7c93654 --- /dev/null +++ b/src/Resend.js @@ -0,0 +1,24 @@ +var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); +var dis = require('matrix-react-sdk/lib/dispatcher'); + +module.exports = { + resend: function(event) { + MatrixClientPeg.get().resendEvent( + event, MatrixClientPeg.get().getRoom(event.getRoomId()) + ).done(function() { + dis.dispatch({ + action: 'message_sent', + event: event + }); + }, function() { + dis.dispatch({ + action: 'message_send_failed', + event: event + }); + }); + dis.dispatch({ + action: 'message_resend_started', + event: event + }); + }, +}; \ No newline at end of file diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index 15d67b1555..1324e21ffd 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -24,6 +24,7 @@ var Modal = require("matrix-react-sdk/lib/Modal"); var sdk = require('matrix-react-sdk/lib/index'); var CallHandler = require('matrix-react-sdk/lib/CallHandler'); var VectorConferenceHandler = require('../../modules/VectorConferenceHandler'); +var Resend = require("../../Resend"); var dis = require("matrix-react-sdk/lib/dispatcher"); @@ -32,8 +33,9 @@ var INITIAL_SIZE = 20; module.exports = { getInitialState: function() { + var room = this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null; return { - room: this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null, + room: room, messageCap: INITIAL_SIZE, editingRoomSettings: false, uploadingRoomSettings: false, @@ -41,7 +43,8 @@ module.exports = { draggingFile: false, searching: false, searchResults: null, - syncState: MatrixClientPeg.get().getSyncState() + syncState: MatrixClientPeg.get().getSyncState(), + hasUnsentMessages: this._hasUnsentMessages(room) } }, @@ -77,6 +80,9 @@ module.exports = { switch (payload.action) { case 'message_send_failed': case 'message_sent': + this.setState({ + hasUnsentMessages: this._hasUnsentMessages(this.state.room) + }); case 'message_resend_started': this.setState({ room: MatrixClientPeg.get().getRoom(this.props.roomId) @@ -182,6 +188,19 @@ module.exports = { this._updateConfCallNotification(); }, + _hasUnsentMessages: function(room) { + return this._getUnsentMessages(room).length > 0; + }, + + _getUnsentMessages: function(room) { + if (!room) { return []; } + // TODO: It would be nice if the JS SDK provided nicer constant-time + // constructs rather than O(N) (N=num msgs) on this. + return room.timeline.filter(function(ev) { + return ev.status === Matrix.EventStatus.NOT_SENT; + }); + }, + _updateConfCallNotification: function() { var room = MatrixClientPeg.get().getRoom(this.props.roomId); if (!room) return; @@ -274,6 +293,13 @@ module.exports = { return false; }, + onResendAllClick: function() { + var eventsToResend = this._getUnsentMessages(this.state.room); + eventsToResend.forEach(function(event) { + Resend.resend(event); + }); + }, + onJoinButtonClicked: function(ev) { var self = this; MatrixClientPeg.get().joinRoom(this.props.roomId).then(function() { diff --git a/src/skins/vector/css/organisms/RoomView.css b/src/skins/vector/css/organisms/RoomView.css index 961a79453a..0379a2aaef 100644 --- a/src/skins/vector/css/organisms/RoomView.css +++ b/src/skins/vector/css/organisms/RoomView.css @@ -209,6 +209,10 @@ limitations under the License. font-size: 12px; } +.mx_RoomView_resend_link { + cursor: pointer; +} + .mx_RoomView_typingBar { margin-top: 10px; margin-left: 54px; diff --git a/src/skins/vector/views/molecules/MessageContextMenu.js b/src/skins/vector/views/molecules/MessageContextMenu.js index 995c2c4b51..b36d4ccbbf 100644 --- a/src/skins/vector/views/molecules/MessageContextMenu.js +++ b/src/skins/vector/views/molecules/MessageContextMenu.js @@ -22,25 +22,13 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var dis = require('matrix-react-sdk/lib/dispatcher'); var sdk = require('matrix-react-sdk') var Modal = require('matrix-react-sdk/lib/Modal'); +var Resend = require("../../../../Resend"); module.exports = React.createClass({ displayName: 'MessageContextMenu', onResendClick: function() { - MatrixClientPeg.get().resendEvent( - this.props.mxEvent, MatrixClientPeg.get().getRoom( - this.props.mxEvent.getRoomId() - ) - ).done(function() { - dis.dispatch({ - action: 'message_sent' - }); - }, function() { - dis.dispatch({ - action: 'message_send_failed' - }); - }); - dis.dispatch({action: 'message_resend_started'}); + Resend.resend(this.props.mxEvent); if (this.props.onFinished) this.props.onFinished(); }, diff --git a/src/skins/vector/views/organisms/RoomView.js b/src/skins/vector/views/organisms/RoomView.js index 86b200cf25..1eda7b7ffd 100644 --- a/src/skins/vector/views/organisms/RoomView.js +++ b/src/skins/vector/views/organisms/RoomView.js @@ -199,6 +199,8 @@ module.exports = React.createClass({ var unreadMsgs = this.getUnreadMessagesString(); // no conn bar trumps unread count since you can't get unread messages // without a connection! (technically may already have some but meh) + // It also trumps the "some not sent" msg since you can't resend without + // a connection! if (this.state.syncState === "ERROR") { statusBar = (
@@ -214,6 +216,24 @@ module.exports = React.createClass({
); } + else if (this.state.hasUnsentMessages) { + statusBar = ( +
+ +
+ + Some of your messages have not been sent. + +
+ + Resend all now + or select individual messages to re-send. +
+
+
+ ); + } // unread count trumps who is typing since the unread count is only // set when you've scrolled up else if (unreadMsgs) { From fdf79d709eacf1176be7a9ff87867dd0c1ae7b76 Mon Sep 17 00:00:00 2001 From: Steven Hammerton Date: Fri, 6 Nov 2015 11:22:59 +0000 Subject: [PATCH 12/46] Replace CAS login with token login --- src/vector/index.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vector/index.js b/src/vector/index.js index 298861973e..45d69001b3 100644 --- a/src/vector/index.js +++ b/src/vector/index.js @@ -65,14 +65,21 @@ function parseQsFromFragment(location) { return {}; } +function parseQs(location) { + return qs.parse(location.search.substring(1)); +} + // Here, we do some crude URL analysis to allow // deep-linking. We only support registration // deep-links in this example. function routeUrl(location) { - if (location.hash.indexOf('#/register') == 0) { + var params = parseQs(location); + var loginToken = params.loginToken; + if (loginToken) { + window.matrixChat.showScreen('token_login', parseQs(location)); + } + else if (location.hash.indexOf('#/register') == 0) { window.matrixChat.showScreen('register', parseQsFromFragment(location)); - } else if (location.hash.indexOf('#/login/cas') == 0) { - window.matrixChat.showScreen('cas_login', parseQsFromFragment(location)); } else { window.matrixChat.showScreen(location.hash.substring(2)); } From c884c5fc33bd85610e076c9ced216051b993d2c8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 6 Nov 2015 20:54:07 +0100 Subject: [PATCH 13/46] actually manage manual ordering; support arbitrary tags; bug fixes --- src/controllers/organisms/RoomView.js | 2 +- src/skins/vector/views/molecules/RoomTile.js | 50 ++++++++++++-- src/skins/vector/views/organisms/RoomList.js | 69 ++++++++++++------- .../vector/views/organisms/RoomSubList.js | 52 ++++++++++++-- 4 files changed, 135 insertions(+), 38 deletions(-) diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index f5a8d28f26..28b15a261e 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -351,7 +351,7 @@ module.exports = { upload: undefined }); }).done(undefined, function() { - // display error message + // TODO: display error message }); }, diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index ad198694e2..5be7ac4c57 100644 --- a/src/skins/vector/views/molecules/RoomTile.js +++ b/src/skins/vector/views/molecules/RoomTile.js @@ -56,16 +56,50 @@ var roomTileSource = { console.log("roomTile endDrag for " + item.room.roomId + " with didDrop=" + monitor.didDrop()); - if (!monitor.didDrop() || !item.targetList.props.editable) { + if (monitor.didDrop() && item.targetList.props.editable) { + // if we moved lists, remove the old tag + if (item.targetList !== item.originalList) { + // commented out attempts to set a spinner on our target component as component is actually + // the original source component being dragged, not our target. To fix we just need to + // move all of this to endDrop in the target instead. FIXME later. + + //component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 }); + MatrixClientPeg.get().deleteRoomTag(item.room.roomId, item.originalList.props.tagName).finally(function() { + //component.state.set({ spinner: component.state.spinner-- }); + }).fail(function(err) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Failed to remove tag " + item.originalList.props.tagName + " from room", + description: err.toString() + }); + }); + } + + var newOrder= {}; + if (item.targetList.props.order === 'manual') { + newOrder['order' = item.targetList.calcManualOrderTagData(item.room); + } + + // if we moved lists or the ordering changed, add the new tag + if (item.targetList.props.tagName && item.targetList !== item.originalList || newOrder) { + //component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 }); + MatrixClientPeg.get().setRoomTag(item.room.roomId, item.targetList.props.tagName, newOrder).finally(function() { + //component.state.set({ spinner: component.state.spinner-- }); + }).fail(function(err) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Failed to add tag " + item.targetList.props.tagName + " to room", + description: err.toString() + }); + }); + } + } + else { + // cancel the drop and reset our original position props.roomSubList.moveRoomTile(item.room, item.originalIndex); if (item.targetList && item.targetList !== item.originalList) { item.targetList.removeRoomTile(item.room); } - return; - } - else { - // When dropped on a compatible target, actually set the right tags for the new ordering - // persistNewOrder(item.room, dropResult.listId); } } }; @@ -88,6 +122,8 @@ var roomTileTarget = { item.targetList = props.roomSubList; } + if (!item.targetList.props.editable) return; + if (item.targetList.props.order === 'manual') { if (item.room.roomId !== props.room.roomId) { var roomTile = props.roomSubList.findRoomTile(props.room); @@ -146,7 +182,7 @@ var RoomTile = React.createClass({ var name; if (this.props.isInvite) { - name = this.props.room.getMember(MatrixClientPeg.get().credentials.userId).events.member.getSender(); + name = this.props.room.getMember(myUserId).events.member.getSender(); } else { name = this.props.room.name; diff --git a/src/skins/vector/views/organisms/RoomList.js b/src/skins/vector/views/organisms/RoomList.js index 2e46c42fbc..98b1da5419 100644 --- a/src/skins/vector/views/organisms/RoomList.js +++ b/src/skins/vector/views/organisms/RoomList.js @@ -38,54 +38,71 @@ module.exports = React.createClass({ null; var RoomSubList = sdk.getComponent('organisms.RoomSubList'); + var self = this; return ( -
+
{ expandButton } - + activityMap={ self.state.activityMap } + selectedRoom={ self.props.selectedRoom } + collapsed={ self.props.collapsed } /> - + activityMap={ self.state.activityMap } + selectedRoom={ self.props.selectedRoom } + collapsed={ self.props.collapsed } /> - + activityMap={ self.state.activityMap } + selectedRoom={ self.props.selectedRoom } + collapsed={ self.props.collapsed } /> - + activityMap={ self.state.activityMap } + selectedRoom={ self.props.selectedRoom } + collapsed={ self.props.collapsed } /> - + + } + }) } + + + activityMap={ self.state.activityMap } + selectedRoom={ self.props.selectedRoom } + collapsed={ self.props.collapsed } />
); } diff --git a/src/skins/vector/views/organisms/RoomSubList.js b/src/skins/vector/views/organisms/RoomSubList.js index b5655e3739..27cfb2ae52 100644 --- a/src/skins/vector/views/organisms/RoomSubList.js +++ b/src/skins/vector/views/organisms/RoomSubList.js @@ -48,7 +48,7 @@ var RoomSubList = React.createClass({ propTypes: { list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, label: React.PropTypes.string.isRequired, - tagname: React.PropTypes.string, + tagName: React.PropTypes.string, editable: React.PropTypes.bool, order: React.PropTypes.string.isRequired, selectedRoom: React.PropTypes.string.isRequired, @@ -88,8 +88,9 @@ var RoomSubList = React.createClass({ }, manualComparator: function(roomA, roomB) { - var a = roomA.tags[this.props.tagname].order; - var b = roomB.tags[this.props.tagname].order; + if (!roomA.tags[this.props.tagName] || !roomB.tags[this.props.tagName]) return 0; + var a = roomA.tags[this.props.tagName].order; + var b = roomB.tags[this.props.tagName].order; return a == b ? this.recentsComparator(roomA, roomB) : ( a > b ? 1 : -1); }, @@ -150,7 +151,50 @@ var RoomSubList = React.createClass({ room: room, index: index, }); - }, + }, + + calcManualOrderTagData: function(room) { + var index = this.state.sortedList.indexOf(room); + + // we sort rooms by the lexicographic ordering of the 'order' metadata on their tags. + // for convenience, we calculate this for now a floating point number between 0.0 and 1.0. + + var orderA = 0.0; // by default we're next to the beginning of the list + if (index > 0) { + var prevTag = this.state.sortedList[index - 1].tags[this.props.tagName]; + if (!prevTag) { + console.error("Previous room in sublist is not tagged to be in this list. This should never happen.") + } + else if (prevTag.order === undefined) { + console.error("Previous room in sublist has no ordering metadata. This should never happen."); + } + else { + orderA = prevTag.order; + } + } + + var orderB = 1.0; // by default we're next to the end of the list too + if (index < this.state.sortedList.length - 1) { + var nextTag = this.state.sortedList[index + 1].tags[this.props.tagName]; + if (!nextTag) { + console.error("Next room in sublist is not tagged to be in this list. This should never happen.") + } + else if (nextTag.order === undefined) { + console.error("Next room in sublist has no ordering metadata. This should never happen."); + } + else { + orderB = nextTag.order; + } + } + + var order = (orderA + orderB) / 2.0; + if (order === orderA || order === orderB) { + console.error("Cannot describe new list position. This should be incredibly unlikely."); + // TODO: renumber the list + } + + return order; + }, makeRoomTiles: function() { var self = this; From 886ffbf158d2a843fc26a11213317b220a34c2da Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 6 Nov 2015 21:00:34 +0100 Subject: [PATCH 14/46] switch to m.* prefixes for tags before it's too late --- src/controllers/organisms/RoomList.js | 8 ++++---- src/skins/vector/views/molecules/RoomTile.js | 2 +- src/skins/vector/views/organisms/RoomList.js | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index 38a0c652c4..3a03fb0439 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -139,8 +139,8 @@ module.exports = { var me = room.getMember(MatrixClientPeg.get().credentials.userId); if (me && me.membership == "invite") { - s.lists["invites"] = s.lists["invites"] || []; - s.lists["invites"].push(room); + s.lists["m.invite"] = s.lists["m.invite"] || []; + s.lists["m.invite"].push(room); } else { var shouldShowRoom = ( @@ -172,8 +172,8 @@ module.exports = { } } else { - s.lists["recents"] = s.lists["recents"] || []; - s.lists["recents"].push(room); + s.lists["m.recent"] = s.lists["m.recent"] || []; + s.lists["m.recent"].push(room); } } } diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index 5be7ac4c57..27c6bc124e 100644 --- a/src/skins/vector/views/molecules/RoomTile.js +++ b/src/skins/vector/views/molecules/RoomTile.js @@ -77,7 +77,7 @@ var roomTileSource = { var newOrder= {}; if (item.targetList.props.order === 'manual') { - newOrder['order' = item.targetList.calcManualOrderTagData(item.room); + newOrder['order'] = item.targetList.calcManualOrderTagData(item.room); } // if we moved lists or the ordering changed, add the new tag diff --git a/src/skins/vector/views/organisms/RoomList.js b/src/skins/vector/views/organisms/RoomList.js index 98b1da5419..9c67d905f1 100644 --- a/src/skins/vector/views/organisms/RoomList.js +++ b/src/skins/vector/views/organisms/RoomList.js @@ -44,7 +44,7 @@ module.exports = React.createClass({
{ expandButton } - - - - { Object.keys(self.state.lists).map(function(tagName) { - if (!tagName.match(/^(invites|favourite|recents|lowpriority|archived)$/)) { + if (!tagName.match(/^m\.(invite|favourite|recent|lowpriority|archived)$/)) { return Date: Fri, 6 Nov 2015 21:25:20 +0100 Subject: [PATCH 15/46] collapsible sublists --- .../vector/css/organisms/RoomSubList.css | 4 ++++ .../vector/views/organisms/RoomSubList.js | 23 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/skins/vector/css/organisms/RoomSubList.css b/src/skins/vector/css/organisms/RoomSubList.css index 43f453fb54..0580f574e3 100644 --- a/src/skins/vector/css/organisms/RoomSubList.css +++ b/src/skins/vector/css/organisms/RoomSubList.css @@ -30,3 +30,7 @@ limitations under the License. margin-top: 8px; margin-bottom: 4px; } + +.mx_RoomSubList_chevron { + padding-left: 5px; +} \ No newline at end of file diff --git a/src/skins/vector/views/organisms/RoomSubList.js b/src/skins/vector/views/organisms/RoomSubList.js index 27cfb2ae52..47e412f3e2 100644 --- a/src/skins/vector/views/organisms/RoomSubList.js +++ b/src/skins/vector/views/organisms/RoomSubList.js @@ -58,6 +58,7 @@ var RoomSubList = React.createClass({ getInitialState: function() { return { + hidden: false, sortedList: [], }; }, @@ -71,6 +72,10 @@ var RoomSubList = React.createClass({ this.sortList(newProps.list, newProps.order); }, + onClick: function(ev) { + this.setState({ hidden : !this.state.hidden }); + }, + tsOfNewestEvent: function(room) { if (room.timeline.length) { return room.timeline[room.timeline.length - 1].getTs(); @@ -229,14 +234,22 @@ var RoomSubList = React.createClass({ target = ; } + var subList; + if (!this.state.hidden) { + subList =
+ { target } + { this.makeRoomTiles() } +
; + } + + if (this.state.sortedList.length > 0 || this.props.editable) { return connectDropTarget(
-

{ this.props.collapsed ? '' : this.props.label }

-
- { target } - { this.makeRoomTiles() } -
+

{ this.props.collapsed ? '' : this.props.label } + +

+ { subList }
); } From f102e3b3b7367ef45ddfb7c957ae3a8a6af80748 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 6 Nov 2015 21:25:35 +0100 Subject: [PATCH 16/46] collapsible sublist graphics --- src/skins/vector/img/list-close.png | Bin 0 -> 1033 bytes src/skins/vector/img/list-open.png | Bin 0 -> 1059 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/skins/vector/img/list-close.png create mode 100644 src/skins/vector/img/list-open.png diff --git a/src/skins/vector/img/list-close.png b/src/skins/vector/img/list-close.png new file mode 100644 index 0000000000000000000000000000000000000000..82b322f9d4e5b93eff00e47263effa03721c2271 GIT binary patch literal 1033 zcmaJ=J8aWH7n;T~ekG1;t7%E>q>Y55D2>z%2ypCcW3_%@Uy0iR z6^5enSWrP7VL)O)2v`|-gbJ$83^1au6$}hWAeD2Pqzo;V&Ug3g|GxkGAGcC0j7NI< zdN2%&#7T|v~E`co;TGQPy(vHaCHNWU|49- zC|6)bxgct`NveSl>6;EhW7x=;@2J`=fOr+m7*>k-{`w7p8+wYE;uKnOBv3OZ7F{s8 zSSV|Yvznk2W25+pFCqdHKo$4RIm;9M6w%}r(K#?v1m1LkvngU%R7EM`lI;SVBa<vggo zCvA6zVgx~;XqIBx81jgD3l>!UnB@($8D!vTuHit#w(x*at=jW2MIfcSDVR=&*790q zLItCI)u9-Y4pM46D$4&|O|#?e!4lZhyDzaLH-QD)SbYzhI}jTTgz2EjGH{J+K+n0hi8m7$jeWFm znFPZLxr6yySbZNYO>CN`57a^-F;vjrf7;zPp&1H}o#927&iDfh&Af|-yK%X98$FDA zUQU<&uhl<0J7XcPt2gxE-P0SZp@;YHY@Rt}29+~1Tbnd%rP)eXu!0BE9mK;xP&_C&(9NUsG8lLj4|?z@%$Kh1JXixS@4Y|&-|zqRdrR5L2~Yn} zKS2(peAQ22rwrrp*~tm2F7GRkH{|T9->O2W55s)+QV!h#s$& zFQ9^y;S^J+WY>qP=@v#4#OPSfl9dWXpa@Hv5hH)Rc}oIKjgi;F5+hkWENiK{4X5jq zd8J-aSd|>R0!C{bCeR_0K~0}E9Ih54o4g!8yJngMO&3&&k^4y%q%7b~8-g$u@hMCs z3Zg9K4@aY+=p_(f{9&32(m}=-2y!8Y3k5*yBXKlao#AqV*ouX3F|v%1#nE)NTBWK% z%Ct+gpJiE^3DALn4}18Wc>~EcpWzI*83gDkwq_yCG=R$}7tJ{oBXOqtCFoX%)^J*F z!VRNqvPJtT#x1GsC`tc!)%A|IgL3d7-+vN2`FRV{Ip~;kwt@#YGwg=4INpXbGVQ!+ z&bGUlEt|-6%BBT)zS%Y4m1IRTT;s(iN|Ly=;UL*iU|NWgn1|9dl?x|A{xB;FQ9c>* z`;%cdEJoQVBSbF8`CuX-wy}b#%<0fTZLE5LO?Jh)g3v7-S%9{78>*sh>Y!OTr*+RI z(Uq@-RlDcHcg4~;8QLB1U*m3@u!h`YN4)sbkv}xB=4~wQ!!N%K;D@oA7UKEZPVx8N z-dGRkef=tPJhfW_ME%z@X5+E7wtH-2`KU_V=-ql)@eZ70*N;LHOszl72y5pzvm>N$ zWG6dvV&Sfrf09}e78V1mtJ^<^uHJ7j{HfgMcN61hrhGl|MfU5~_sn)^CEs&oNL+3R wgW~gsa9UhxTo@ETjz93cFM;ce4PyMxAtLvU?ETR9>5= Date: Fri, 6 Nov 2015 21:33:28 +0100 Subject: [PATCH 17/46] precedence fail :( --- src/skins/vector/views/molecules/RoomTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index 27c6bc124e..9f7806959d 100644 --- a/src/skins/vector/views/molecules/RoomTile.js +++ b/src/skins/vector/views/molecules/RoomTile.js @@ -81,7 +81,7 @@ var roomTileSource = { } // if we moved lists or the ordering changed, add the new tag - if (item.targetList.props.tagName && item.targetList !== item.originalList || newOrder) { + if (item.targetList.props.tagName && (item.targetList !== item.originalList || newOrder)) { //component.state.set({ spinner: component.state.spinner ? component.state.spinner++ : 1 }); MatrixClientPeg.get().setRoomTag(item.room.roomId, item.targetList.props.tagName, newOrder).finally(function() { //component.state.set({ spinner: component.state.spinner-- }); From fe442f5c24e49b571d2a0924432f6dc63f63187d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 6 Nov 2015 23:30:57 +0100 Subject: [PATCH 18/46] fix various edge cases when dragging stuff back to the conversations list --- src/controllers/organisms/RoomList.js | 8 ++++++-- src/skins/vector/views/molecules/RoomTile.js | 11 ++++++++--- src/skins/vector/views/organisms/RoomList.js | 1 + src/skins/vector/views/organisms/RoomSubList.js | 7 ++++--- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index 3a03fb0439..792fa04f79 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -135,11 +135,16 @@ module.exports = { getRoomLists: function() { var s = { lists: {} }; + s.lists["m.invite"] = []; + s.lists["m.favourite"] = []; + s.lists["m.recent"] = []; + s.lists["m.lowpriority"] = []; + s.lists["m.archived"] = []; + MatrixClientPeg.get().getRooms().forEach(function(room) { var me = room.getMember(MatrixClientPeg.get().credentials.userId); if (me && me.membership == "invite") { - s.lists["m.invite"] = s.lists["m.invite"] || []; s.lists["m.invite"].push(room); } else { @@ -172,7 +177,6 @@ module.exports = { } } else { - s.lists["m.recent"] = s.lists["m.recent"] || []; s.lists["m.recent"].push(room); } } diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index 9f7806959d..a8f646aeab 100644 --- a/src/skins/vector/views/molecules/RoomTile.js +++ b/src/skins/vector/views/molecules/RoomTile.js @@ -96,6 +96,7 @@ var roomTileSource = { } else { // cancel the drop and reset our original position + console.log("cancelling drop & drag"); props.roomSubList.moveRoomTile(item.room, item.originalIndex); if (item.targetList && item.targetList !== item.originalList) { item.targetList.removeRoomTile(item.room); @@ -111,13 +112,16 @@ var roomTileTarget = { hover: function(props, monitor) { var item = monitor.getItem(); - console.log("hovering on room " + props.room.roomId + ", isOver=" + monitor.isOver()); + //console.log("hovering on room " + props.room.roomId + ", isOver=" + monitor.isOver()); //console.log("item.targetList=" + item.targetList + ", roomSubList=" + props.roomSubList); + var switchedTarget = false; if (item.targetList !== props.roomSubList) { // we've switched target, so remove the tile from the previous target. // n.b. the previous target might actually be the source list. + console.log("switched target"); + switchedTarget = true; item.targetList.removeRoomTile(item.room); item.targetList = props.roomSubList; } @@ -130,12 +134,13 @@ var roomTileTarget = { props.roomSubList.moveRoomTile(item.room, roomTile.index); } } - else { + else if (switchedTarget) { if (!props.roomSubList.findRoomTile(item.room).room) { // add to the list in the right place props.roomSubList.moveRoomTile(item.room, 0); - props.roomSubList.sortList(); } + // we have to sort the list whatever to recalculate it + props.roomSubList.sortList(); } }, }; diff --git a/src/skins/vector/views/organisms/RoomList.js b/src/skins/vector/views/organisms/RoomList.js index 9c67d905f1..8ee456e9b4 100644 --- a/src/skins/vector/views/organisms/RoomList.js +++ b/src/skins/vector/views/organisms/RoomList.js @@ -65,6 +65,7 @@ module.exports = React.createClass({ Date: Sat, 7 Nov 2015 02:57:56 +0000 Subject: [PATCH 19/46] sacrifice a small mountainside of goats to make placeholder-based work correctly --- .../vector/css/molecules/RoomDropTarget.css | 5 ++ src/skins/vector/css/molecules/RoomTile.css | 4 +- .../vector/views/molecules/RoomDropTarget.js | 22 +++++--- src/skins/vector/views/molecules/RoomTile.js | 55 +++++++++++++++++-- .../vector/views/organisms/RoomSubList.js | 23 +++++--- 5 files changed, 88 insertions(+), 21 deletions(-) diff --git a/src/skins/vector/css/molecules/RoomDropTarget.css b/src/skins/vector/css/molecules/RoomDropTarget.css index 549fa609ec..5e3facc4e2 100644 --- a/src/skins/vector/css/molecules/RoomDropTarget.css +++ b/src/skins/vector/css/molecules/RoomDropTarget.css @@ -26,6 +26,11 @@ limitations under the License. border-radius: 4px; } +.mx_RoomDropTarget_placeholder { + padding-top: 1px; + padding-bottom: 1px; +} + .mx_RoomDropTarget_avatar { background-color: #fff; border-radius: 24px; diff --git a/src/skins/vector/css/molecules/RoomTile.css b/src/skins/vector/css/molecules/RoomTile.css index b62552252b..4bc71cb801 100644 --- a/src/skins/vector/css/molecules/RoomTile.css +++ b/src/skins/vector/css/molecules/RoomTile.css @@ -16,7 +16,8 @@ limitations under the License. .mx_RoomTile { cursor: pointer; - display: table-row; + /* This fixes wrapping of long room names, but breaks drag & drop previews */ + /* display: table-row; */ font-size: 14px; } @@ -38,6 +39,7 @@ limitations under the License. .mx_RoomTile_name { display: table-cell; + width: 100%; vertical-align: middle; overflow: hidden; text-overflow: ellipsis; diff --git a/src/skins/vector/views/molecules/RoomDropTarget.js b/src/skins/vector/views/molecules/RoomDropTarget.js index c1a2a9548f..00d0546c90 100644 --- a/src/skins/vector/views/molecules/RoomDropTarget.js +++ b/src/skins/vector/views/molecules/RoomDropTarget.js @@ -22,13 +22,21 @@ module.exports = React.createClass({ displayName: 'RoomDropTarget', render: function() { - return ( -
-
-
- { this.props.label } + if (this.props.placeholder) { + return ( +
-
- ); + ); + } + else { + return ( +
+
+
+ { this.props.label } +
+
+ ); + } } }); diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index a8f646aeab..28e76a7034 100644 --- a/src/skins/vector/views/molecules/RoomTile.js +++ b/src/skins/vector/views/molecules/RoomTile.js @@ -43,10 +43,16 @@ var roomTileSource = { originalList: props.roomSubList, originalIndex: props.roomSubList.findRoomTile(props.room).index, targetList: props.roomSubList, // at first target is same as original + lastTargetRoom: null, + lastYOffset: null, + lastYDelta: null, }; console.log("roomTile beginDrag for " + item.room.roomId); + // doing this 'correctly' with state causes react-dnd to break seemingly due to the state transitions + props.room._dragging = true; + return item; }, @@ -56,6 +62,11 @@ var roomTileSource = { console.log("roomTile endDrag for " + item.room.roomId + " with didDrop=" + monitor.didDrop()); + props.room._dragging = false; + if (monitor.didDrop()) { + monitor.getDropResult().component.forceUpdate(); // as we're not using state + } + if (monitor.didDrop() && item.targetList.props.editable) { // if we moved lists, remove the old tag if (item.targetList !== item.originalList) { @@ -112,7 +123,8 @@ var roomTileTarget = { hover: function(props, monitor) { var item = monitor.getItem(); - //console.log("hovering on room " + props.room.roomId + ", isOver=" + monitor.isOver()); + var off = monitor.getClientOffset(); + // console.log("hovering on room " + props.room.roomId + ", isOver=" + monitor.isOver()); //console.log("item.targetList=" + item.targetList + ", roomSubList=" + props.roomSubList); @@ -120,7 +132,7 @@ var roomTileTarget = { if (item.targetList !== props.roomSubList) { // we've switched target, so remove the tile from the previous target. // n.b. the previous target might actually be the source list. - console.log("switched target"); + console.log("switched target sublist"); switchedTarget = true; item.targetList.removeRoomTile(item.room); item.targetList = props.roomSubList; @@ -129,10 +141,35 @@ var roomTileTarget = { if (!item.targetList.props.editable) return; if (item.targetList.props.order === 'manual') { - if (item.room.roomId !== props.room.roomId) { + if (item.room.roomId !== props.room.roomId && props.room !== item.lastTargetRoom) { + // find the offset of the target tile in the list. var roomTile = props.roomSubList.findRoomTile(props.room); + // shuffle the list to add our tile to that position. props.roomSubList.moveRoomTile(item.room, roomTile.index); } + + // stop us from flickering between our droptarget and the previous room. + // whenever the cursor changes direction we have to reset the flicker-damping. + + var yDelta = off.y - item.lastYOffset; + + if ((yDelta > 0 && item.lastYDelta < 0) || + (yDelta < 0 && item.lastYDelta > 0)) + { + // the cursor changed direction - forget our previous room + item.lastTargetRoom = null; + } + else { + // track the last room we were hovering over so we can stop + // bouncing back and forth if the droptarget is narrower than + // the other list items. The other way to do this would be + // to reduce the size of the hittarget on the list items, but + // can't see an easy way to do that. + item.lastTargetRoom = props.room; + } + + if (yDelta) item.lastYDelta = yDelta; + item.lastYOffset = off.y; } else if (switchedTarget) { if (!props.roomSubList.findRoomTile(item.room).room) { @@ -175,6 +212,15 @@ var RoomTile = React.createClass({ }, render: function() { + // if (this.props.clientOffset) { + // //console.log("room " + this.props.room.roomId + " has dropTarget clientOffset " + this.props.clientOffset.x + "," + this.props.clientOffset.y); + // } + + if (this.props.room._dragging) { + var RoomDropTarget = sdk.getComponent("molecules.RoomDropTarget"); + return ; + } + var myUserId = MatrixClientPeg.get().credentials.userId; var me = this.props.room.currentState.members[myUserId]; var classes = classNames({ @@ -247,11 +293,12 @@ var RoomTile = React.createClass({ // Export the wrapped version, inlining the 'collect' functions // to more closely resemble the ES7 module.exports = -DropTarget('RoomTile', roomTileTarget, function(connect) { +DropTarget('RoomTile', roomTileTarget, function(connect, monitor) { return { // Call this function inside render() // to let React DnD handle the drag events: connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), } })( DragSource('RoomTile', roomTileSource, function(connect, monitor) { diff --git a/src/skins/vector/views/organisms/RoomSubList.js b/src/skins/vector/views/organisms/RoomSubList.js index b88bc3c70a..8dcccc733a 100644 --- a/src/skins/vector/views/organisms/RoomSubList.js +++ b/src/skins/vector/views/organisms/RoomSubList.js @@ -26,6 +26,11 @@ var roomListTarget = { return true; }, + drop: function(props, monitor, component) { + console.log("dropped on sublist") + return { component: component }; + }, + hover: function(props, monitor, component) { var item = monitor.getItem(); @@ -147,7 +152,7 @@ var RoomSubList = React.createClass({ findRoomTile: function(room) { var index = this.state.sortedList.indexOf(room); if (index >= 0) { - //console.log("found: room: " + room + " with id " + room.roomId); + // console.log("found: room: " + room.roomId + " with index " + index); } else { console.log("didn't find room"); @@ -210,14 +215,14 @@ var RoomSubList = React.createClass({ // XXX: is it evil to pass in self as a prop to RoomTile? return ( + room={ room } + roomSubList={ self } + key={ room.roomId } + collapsed={ self.props.collapsed } + selected={ selected } + unread={ self.props.activityMap[room.roomId] === 1 } + highlight={ self.props.activityMap[room.roomId] === 2 } + isInvite={ self.props.label === 'Invites' } /> ); }); }, From 4e170a2831fa23fdc7ab5911b50afcfd2f8122a1 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 7 Nov 2015 20:16:05 +0000 Subject: [PATCH 20/46] fix onRoomTags signature --- src/controllers/organisms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/organisms/RoomList.js b/src/controllers/organisms/RoomList.js index 792fa04f79..35d1774ac8 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -110,7 +110,7 @@ module.exports = { this.refreshRoomList(); }, - onRoomTags: function(room) { + onRoomTags: function(event, room) { this.refreshRoomList(); }, From ed4c5b9f73841e23e692648fbc7baf1785d8031d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sat, 7 Nov 2015 20:16:44 +0000 Subject: [PATCH 21/46] switch initial spinner to mx_Spinner --- src/skins/vector/css/atoms/Spinner.css | 25 ++++++++++++++++++++++ src/skins/vector/views/atoms/Spinner.js | 2 +- src/skins/vector/views/pages/MatrixChat.js | 8 +++---- 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 src/skins/vector/css/atoms/Spinner.css diff --git a/src/skins/vector/css/atoms/Spinner.css b/src/skins/vector/css/atoms/Spinner.css new file mode 100644 index 0000000000..1c8aa97d02 --- /dev/null +++ b/src/skins/vector/css/atoms/Spinner.css @@ -0,0 +1,25 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_Spinner { + display: -webkit-flex; + display: flex; + -webkit-align-items: center; + -webkit-justify-content: center; + align-items: center; + justify-content: center; + height: 100%; +} \ No newline at end of file diff --git a/src/skins/vector/views/atoms/Spinner.js b/src/skins/vector/views/atoms/Spinner.js index 908f267857..6dfd0c41aa 100644 --- a/src/skins/vector/views/atoms/Spinner.js +++ b/src/skins/vector/views/atoms/Spinner.js @@ -26,7 +26,7 @@ module.exports = React.createClass({ var h = this.props.h || 32; var imgClass = this.props.imgClassName || ""; return ( -
+
); diff --git a/src/skins/vector/views/pages/MatrixChat.js b/src/skins/vector/views/pages/MatrixChat.js index f34b6d4fd5..2d6a351697 100644 --- a/src/skins/vector/views/pages/MatrixChat.js +++ b/src/skins/vector/views/pages/MatrixChat.js @@ -23,9 +23,6 @@ var sdk = require('matrix-react-sdk') var MatrixChatController = require('matrix-react-sdk/lib/controllers/pages/MatrixChat') -// should be atomised -var Loader = require("react-loader"); - var dis = require('matrix-react-sdk/lib/dispatcher'); var Matrix = require("matrix-js-sdk"); var ContextualMenu = require("../../../../ContextualMenu"); @@ -156,8 +153,9 @@ var MatrixChat = React.createClass({ ); } } else if (this.state.logged_in) { + var Spinner = sdk.getComponent('atoms.Spinner'); return ( - + ); } else if (this.state.screen == 'register') { return ( @@ -175,4 +173,4 @@ var MatrixChat = React.createClass({ } }); -module.exports = DragDropContext(HTML5Backend)(MatrixChat); \ No newline at end of file +module.exports = DragDropContext(HTML5Backend)(MatrixChat); From 69899e37184a2b8457e8f40bb0e3207e1ac38860 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 8 Nov 2015 11:44:13 +0000 Subject: [PATCH 22/46] position sublist chevron better when collapsed --- src/skins/vector/css/organisms/RoomSubList.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/skins/vector/css/organisms/RoomSubList.css b/src/skins/vector/css/organisms/RoomSubList.css index 0580f574e3..b971f65c77 100644 --- a/src/skins/vector/css/organisms/RoomSubList.css +++ b/src/skins/vector/css/organisms/RoomSubList.css @@ -33,4 +33,8 @@ limitations under the License. .mx_RoomSubList_chevron { padding-left: 5px; -} \ No newline at end of file +} + +.collapsed .mx_RoomSubList_chevron { + padding-left: 13px; +} From c60e8736c15d58f48b827d1155557c969fb854aa Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 8 Nov 2015 11:50:15 +0000 Subject: [PATCH 23/46] handle collapsed drop-targets better --- src/skins/vector/css/molecules/RoomDropTarget.css | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/skins/vector/css/molecules/RoomDropTarget.css b/src/skins/vector/css/molecules/RoomDropTarget.css index 5e3facc4e2..344c31a9c1 100644 --- a/src/skins/vector/css/molecules/RoomDropTarget.css +++ b/src/skins/vector/css/molecules/RoomDropTarget.css @@ -26,6 +26,10 @@ limitations under the License. border-radius: 4px; } +.collapsed .mx_RoomDropTarget { + margin-right: 10px; +} + .mx_RoomDropTarget_placeholder { padding-top: 1px; padding-bottom: 1px; @@ -47,3 +51,11 @@ limitations under the License. line-height: 21px; z-index: 1; } + +.collapsed .mx_RoomDropTarget_avatar { + float: none; +} + +.collapsed .mx_RoomDropTarget_label { + display: none; +} From e289235e1727b5bf7474d26b8dbbc8e86597add5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 8 Nov 2015 12:02:26 +0000 Subject: [PATCH 24/46] fix tooltip positioning when collapsed --- src/skins/vector/css/molecules/RoomTooltip.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/skins/vector/css/molecules/RoomTooltip.css b/src/skins/vector/css/molecules/RoomTooltip.css index 604c6a56f1..4e831d48c1 100644 --- a/src/skins/vector/css/molecules/RoomTooltip.css +++ b/src/skins/vector/css/molecules/RoomTooltip.css @@ -21,7 +21,6 @@ limitations under the License. border-radius: 8px; background-color: #fff; z-index: 1000; - margin-top: 6px; left: 64px; padding: 6px; } From 36da1acccaa5cdcd3fc82a9a90b5ebd32c1ca43e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 8 Nov 2015 12:14:10 +0000 Subject: [PATCH 25/46] click on CallView preview to jump to call --- src/skins/vector/views/molecules/voip/CallView.js | 2 +- src/skins/vector/views/molecules/voip/VideoView.js | 2 +- src/skins/vector/views/organisms/LeftPanel.js | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/skins/vector/views/molecules/voip/CallView.js b/src/skins/vector/views/molecules/voip/CallView.js index 07987bd379..52297bbc1d 100644 --- a/src/skins/vector/views/molecules/voip/CallView.js +++ b/src/skins/vector/views/molecules/voip/CallView.js @@ -34,7 +34,7 @@ module.exports = React.createClass({ render: function(){ var VideoView = sdk.getComponent('molecules.voip.VideoView'); return ( - + ); } }); diff --git a/src/skins/vector/views/molecules/voip/VideoView.js b/src/skins/vector/views/molecules/voip/VideoView.js index 4e0fb913e6..e19f570424 100644 --- a/src/skins/vector/views/molecules/voip/VideoView.js +++ b/src/skins/vector/views/molecules/voip/VideoView.js @@ -78,7 +78,7 @@ module.exports = React.createClass({ render: function() { var VideoFeed = sdk.getComponent('atoms.voip.VideoFeed'); return ( -
+