diff --git a/package.json b/package.json index ff93588b..d71fdac5 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,15 @@ "modernizr": "^3.1.0", "matrix-js-sdk": "https://github.com/matrix-org/matrix-js-sdk.git#develop", "matrix-react-sdk": "^0.0.2", + "modernizr": "^3.1.0", "q": "^1.4.1", - "react": "^0.13.3", + "react": "^0.14.2", + "react-dnd": "^2.0.2", + "react-dnd-html5-backend": "^2.0.0", + "react-dom": "^0.14.2", + "react-gemini-scrollbar": "^2.0.1", "react-loader": "^1.4.0", - "sanitize-html": "^1.11.1" + "sanitize-html": "^1.0.0" }, "devDependencies": { "babel": "^5.8.23", diff --git a/src/ContextualMenu.js b/src/ContextualMenu.js index 7865e45a..3327aa94 100644 --- a/src/ContextualMenu.js +++ b/src/ContextualMenu.js @@ -18,6 +18,7 @@ limitations under the License. 'use strict'; var React = require('react'); +var ReactDOM = require('react-dom'); // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -74,7 +75,7 @@ module.exports = { ); - React.render(menu, this.getOrCreateContainer()); + ReactDOM.render(menu, this.getOrCreateContainer()); return {close: closeMenu}; }, diff --git a/src/Resend.js b/src/Resend.js new file mode 100644 index 00000000..52b7c936 --- /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/RoomList.js b/src/controllers/organisms/RoomList.js index 964a2648..37d4a4e4 100644 --- a/src/controllers/organisms/RoomList.js +++ b/src/controllers/organisms/RoomList.js @@ -17,22 +17,31 @@ limitations under the License. 'use strict'; var React = require("react"); +var ReactDOM = require("react-dom"); + var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg"); var RoomListSorter = require("matrix-react-sdk/lib/RoomListSorter"); 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; module.exports = { + getInitialState: function() { + return { + activityMap: null, + lists: {}, + } + }, + componentWillMount: function() { var cli = MatrixClientPeg.get(); 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); @@ -47,11 +56,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(); @@ -72,7 +76,6 @@ module.exports = { componentWillReceiveProps: function(newProps) { this.state.activityMap[newProps.selectedRoom] = undefined; - this._recheckCallElement(newProps.selectedRoom); this.setState({ activityMap: this.state.activityMap }); @@ -109,6 +112,10 @@ module.exports = { this.refreshRoomList(); }, + onRoomTags: function(event, room) { + this.refreshRoomList(); + }, + onRoomStateEvents: function(ev, state) { setTimeout(this.refreshRoomList, 0); }, @@ -117,26 +124,36 @@ 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; - } + 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"].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 @@ -151,48 +168,34 @@ 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["m.recent"].push(room); + } + } + } }); + + //console.log("calculated new roomLists; m.recent = " + s.lists["m.recent"]); + + // we actually apply the sorting to this when receiving the prop in RoomSubLists. + + return s; }, _repositionTooltip: function(e) { if (this.tooltip && this.tooltip.parentElement) { - var scroll = this.getDOMNode(); - this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.scrollTop) + "px"; + var scroll = ReactDOM.findDOMNode(this); + this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.children[2].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/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index c305a9c9..d8832fa3 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -17,6 +17,7 @@ limitations under the License. var Matrix = require("matrix-js-sdk"); var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg"); var React = require("react"); +var ReactDOM = require("react-dom"); var q = require("q"); var ContentMessages = require("matrix-react-sdk/lib//ContentMessages"); var WhoIsTyping = require("matrix-react-sdk/lib/WhoIsTyping"); @@ -24,6 +25,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 +34,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,6 +44,8 @@ module.exports = { draggingFile: false, searching: false, searchResults: null, + syncState: MatrixClientPeg.get().getSyncState(), + hasUnsentMessages: this._hasUnsentMessages(room) } }, @@ -51,12 +56,13 @@ module.exports = { MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().on("sync", this.onSyncStateChange); this.atBottom = true; }, componentWillUnmount: function() { if (this.refs.messageWrapper) { - var messageWrapper = this.refs.messageWrapper.getDOMNode(); + var messageWrapper = ReactDOM.findDOMNode(this.refs.messageWrapper); messageWrapper.removeEventListener('drop', this.onDrop); messageWrapper.removeEventListener('dragover', this.onDragOver); messageWrapper.removeEventListener('dragleave', this.onDragLeaveOrEnd); @@ -69,6 +75,7 @@ module.exports = { MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("sync", this.onSyncStateChange); } }, @@ -76,6 +83,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) @@ -92,8 +102,8 @@ module.exports = { // scroll to bottom var messageWrapper = this.refs.messageWrapper; if (messageWrapper) { - messageWrapper = messageWrapper.getDOMNode(); - messageWrapper.scrollTop = messageWrapper.scrollHeight; + var messageWrapperScroll = ReactDOM.findDOMNode(messageWrapper).children[2]; + messageWrapperScroll.scrollTop = messageWrapperScroll.scrollHeight; } } @@ -107,6 +117,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. @@ -116,7 +132,7 @@ module.exports = { onRoomTimeline: function(ev, room, toStartOfTimeline) { if (!this.isMounted()) return; - // ignore anything that comes in whilst pagingating: we get one + // ignore anything that comes in whilst paginating: we get one // event for each new matrix event so this would cause a huge // number of UI updates. Just update the UI when the paginate // call returns. @@ -128,10 +144,10 @@ module.exports = { if (room.roomId != this.props.roomId) return; if (this.refs.messageWrapper) { - var messageWrapper = this.refs.messageWrapper.getDOMNode(); + var messageWrapperScroll = ReactDOM.findDOMNode(this.refs.messageWrapper).children[2]; this.atBottom = ( - messageWrapper.scrollHeight - messageWrapper.scrollTop <= - (messageWrapper.clientHeight + 150) + messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= + (messageWrapperScroll.clientHeight + 150) ); } @@ -184,6 +200,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; @@ -208,14 +237,16 @@ module.exports = { componentDidMount: function() { if (this.refs.messageWrapper) { - var messageWrapper = this.refs.messageWrapper.getDOMNode(); + var messageWrapper = ReactDOM.findDOMNode(this.refs.messageWrapper); messageWrapper.addEventListener('drop', this.onDrop); messageWrapper.addEventListener('dragover', this.onDragOver); messageWrapper.addEventListener('dragleave', this.onDragLeaveOrEnd); messageWrapper.addEventListener('dragend', this.onDragLeaveOrEnd); - messageWrapper.scrollTop = messageWrapper.scrollHeight; + var messageWrapperScroll = messageWrapper.children[2]; + + messageWrapperScroll.scrollTop = messageWrapperScroll.scrollHeight; this.sendReadReceipt(); @@ -228,17 +259,17 @@ module.exports = { componentDidUpdate: function() { if (!this.refs.messageWrapper) return; - var messageWrapper = this.refs.messageWrapper.getDOMNode(); + var messageWrapperScroll = ReactDOM.findDOMNode(this.refs.messageWrapper).children[2]; if (this.state.paginating && !this.waiting_for_paginate) { - var heightGained = messageWrapper.scrollHeight - this.oldScrollHeight; - messageWrapper.scrollTop += heightGained; + var heightGained = messageWrapperScroll.scrollHeight - this.oldScrollHeight; + messageWrapperScroll.scrollTop += heightGained; this.oldScrollHeight = undefined; if (!this.fillSpace()) { this.setState({paginating: false}); } } else if (this.atBottom) { - messageWrapper.scrollTop = messageWrapper.scrollHeight; + messageWrapperScroll.scrollTop = messageWrapperScroll.scrollHeight; if (this.state.numUnreadMessages !== 0) { this.setState({numUnreadMessages: 0}); } @@ -247,11 +278,11 @@ module.exports = { fillSpace: function() { if (!this.refs.messageWrapper) return; - var messageWrapper = this.refs.messageWrapper.getDOMNode(); - if (messageWrapper.scrollTop < messageWrapper.clientHeight && this.state.room.oldState.paginationToken) { + var messageWrapperScroll = ReactDOM.findDOMNode(this.refs.messageWrapper).children[2]; + if (messageWrapperScroll.scrollTop < messageWrapperScroll.clientHeight && this.state.room.oldState.paginationToken) { this.setState({paginating: true}); - this.oldScrollHeight = messageWrapper.scrollHeight; + this.oldScrollHeight = messageWrapperScroll.scrollHeight; if (this.state.messageCap < this.state.room.timeline.length) { this.waiting_for_paginate = false; @@ -278,6 +309,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() { @@ -298,9 +336,9 @@ module.exports = { onMessageListScroll: function(ev) { if (this.refs.messageWrapper) { - var messageWrapper = this.refs.messageWrapper.getDOMNode(); + var messageWrapperScroll = ReactDOM.findDOMNode(this.refs.messageWrapper).children[2]; var wasAtBottom = this.atBottom; - this.atBottom = messageWrapper.scrollHeight - messageWrapper.scrollTop <= messageWrapper.clientHeight; + this.atBottom = messageWrapperScroll.scrollHeight - messageWrapperScroll.scrollTop <= messageWrapperScroll.clientHeight; if (this.atBottom && !wasAtBottom) { this.forceUpdate(); // remove unread msg count } @@ -363,8 +401,12 @@ module.exports = { self.setState({ upload: undefined }); - }).done(undefined, function() { - // display error message + }).done(undefined, function(error) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Failed to upload file", + description: error.toString() + }); }); }, @@ -390,6 +432,7 @@ module.exports = { room_events: { search_term: term, filter: filter, + order_by: "recent", event_context: { before_limit: 1, after_limit: 1, @@ -403,7 +446,11 @@ module.exports = { searchResults: data, }); }, function(error) { - // TODO: show dialog or something + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Search failed", + description: error.toString() + }); }); }, @@ -421,7 +468,7 @@ module.exports = { var eventIds = Object.keys(results); // XXX: todo: merge overlapping results somehow? // XXX: why doesn't searching on name work? - var resultList = eventIds.map(function(key) { return results[key]; }).sort(function(a, b) { b.rank - a.rank }); + var resultList = eventIds.map(function(key) { return results[key]; }); // .sort(function(a, b) { b.rank - a.rank }); for (var i = 0; i < resultList.length; i++) { var ts1 = resultList[i].result.origin_server_ts; ret.push(
  • ); // Rank: {resultList[i].rank} diff --git a/src/skins/vector/css/atoms/MemberAvatar.css b/src/skins/vector/css/atoms/MemberAvatar.css index fc5fd60d..97dae35f 100644 --- a/src/skins/vector/css/atoms/MemberAvatar.css +++ b/src/skins/vector/css/atoms/MemberAvatar.css @@ -19,3 +19,12 @@ limitations under the License. border-radius: 20px; } +.mx_MemberAvatar_initial { + position: absolute; + color: #fff; + text-align: center; +} + +.mx_MemberAvatar_wrapper { + position: relative; +} \ No newline at end of file diff --git a/src/skins/vector/css/atoms/RoomAvatar.css b/src/skins/vector/css/atoms/RoomAvatar.css new file mode 100644 index 00000000..f54a93ee --- /dev/null +++ b/src/skins/vector/css/atoms/RoomAvatar.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_RoomAvatar { +} + +.mx_RoomAvatar_initial { + position: absolute; + color: #fff; + text-align: center; + font-weight: normal ! important; +} \ No newline at end of file diff --git a/src/skins/vector/css/atoms/Spinner.css b/src/skins/vector/css/atoms/Spinner.css new file mode 100644 index 00000000..1c8aa97d --- /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/css/gemini-scrollbar.css b/src/skins/vector/css/gemini-scrollbar.css new file mode 120000 index 00000000..4e3c83ba --- /dev/null +++ b/src/skins/vector/css/gemini-scrollbar.css @@ -0,0 +1 @@ +../../../../node_modules/react-gemini-scrollbar/node_modules/gemini-scrollbar/gemini-scrollbar.css \ No newline at end of file diff --git a/src/skins/vector/css/hide.css b/src/skins/vector/css/hide.css index 7d8ee302..f84a35b3 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/EventTile.css b/src/skins/vector/css/molecules/EventTile.css index 25fe9646..d2d87976 100644 --- a/src/skins/vector/css/molecules/EventTile.css +++ b/src/skins/vector/css/molecules/EventTile.css @@ -18,13 +18,13 @@ limitations under the License. max-width: 100%; clear: both; margin-top: 24px; - margin-left: 56px; + margin-left: 65px; } .mx_EventTile_avatar { padding-left: 18px; padding-right: 12px; - margin-left: -64px; + margin-left: -73px; margin-top: -4px; float: left; } @@ -78,7 +78,7 @@ limitations under the License. } .mx_EventTile_notSent { - color: #f11; + color: #ddd; } .mx_EventTile_highlight { diff --git a/src/skins/vector/css/molecules/MatrixToolbar.css b/src/skins/vector/css/molecules/MatrixToolbar.css index 99c28240..b545b1ad 100644 --- a/src/skins/vector/css/molecules/MatrixToolbar.css +++ b/src/skins/vector/css/molecules/MatrixToolbar.css @@ -15,20 +15,40 @@ limitations under the License. */ .mx_MatrixToolbar { - text-align: center; - background-color: #ff0064; + background-color: #76cfa6; color: #fff; - font-weight: bold; - padding: 6px; + + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + -webkit-align-items: center; + align-items: center; } -.mx_MatrixToolbar button { - margin-left: 12px; +.mx_MatrixToolbar_warning { + margin-left: 16px; + margin-right: 8px; + margin-top: -2px; +} + +.mx_MatrixToolbar_link +{ + color: #fff ! important; + text-decoration: underline ! important; + cursor: pointer; } .mx_MatrixToolbar_close { - float: right; - margin-top: 3px; - margin-right: 12px; + -webkit-flex: 1; + flex: 1; cursor: pointer; -} \ No newline at end of file + text-align: right; +} + +.mx_MatrixToolbar_close img { + display: block; + float: right; + margin-right: 10px; +} diff --git a/src/skins/vector/css/molecules/MessageComposer.css b/src/skins/vector/css/molecules/MessageComposer.css index 44e12276..2dbe05b5 100644 --- a/src/skins/vector/css/molecules/MessageComposer.css +++ b/src/skins/vector/css/molecules/MessageComposer.css @@ -32,7 +32,7 @@ limitations under the License. .mx_MessageComposer .mx_MessageComposer_avatar { display: table-cell; padding-left: 10px; - padding-right: 20px; + padding-right: 28px; height: 70px; } diff --git a/src/skins/vector/css/molecules/RoomDropTarget.css b/src/skins/vector/css/molecules/RoomDropTarget.css index c42d4499..4eea49e1 100644 --- a/src/skins/vector/css/molecules/RoomDropTarget.css +++ b/src/skins/vector/css/molecules/RoomDropTarget.css @@ -16,12 +16,46 @@ 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; +} + +.collapsed .mx_RoomDropTarget { + margin-right: 10px; +} + +.mx_RoomDropTarget_placeholder { + padding-top: 1px; + padding-bottom: 1px; +} + +.mx_RoomDropTarget_avatar { + background-color: #fff; + border-radius: 24px; + width: 24px; + height: 24px; + float: left; + margin-left: 7px; + margin-right: 7px; +} + +.mx_RoomDropTarget_label { + position: relative; + margin-top: 3px; + line-height: 21px; + z-index: 1; +} + +.collapsed .mx_RoomDropTarget_avatar { + float: none; +} + +.collapsed .mx_RoomDropTarget_label { + display: none; } diff --git a/src/skins/vector/css/molecules/RoomHeader.css b/src/skins/vector/css/molecules/RoomHeader.css index 2eeda241..e86bab2e 100644 --- a/src/skins/vector/css/molecules/RoomHeader.css +++ b/src/skins/vector/css/molecules/RoomHeader.css @@ -33,6 +33,7 @@ limitations under the License. .mx_RoomHeader_leftRow { height: 48px; margin-top: 18px; + margin-left: -2px; -webkit-box-ordinal-group: 1; -moz-box-ordinal-group: 1; @@ -103,7 +104,7 @@ limitations under the License. color: #454545; font-weight: 800; font-size: 24px; - padding-left: 8px; + padding-left: 19px; padding-right: 16px; text-overflow: ellipsis; } @@ -153,7 +154,7 @@ limitations under the License. max-height: 38px; color: #454545; font-weight: 300; - padding-left: 8px; + padding-left: 19px; padding-right: 16px; overflow: hidden; text-overflow: ellipsis; diff --git a/src/skins/vector/css/molecules/RoomTile.css b/src/skins/vector/css/molecules/RoomTile.css index f2c1daad..4bc71cb8 100644 --- a/src/skins/vector/css/molecules/RoomTile.css +++ b/src/skins/vector/css/molecules/RoomTile.css @@ -16,13 +16,13 @@ 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; } .mx_RoomTile_avatar { display: table-cell; - background: #eaf5f0; padding-right: 8px; padding-top: 4px; padding-bottom: 2px; @@ -39,17 +39,16 @@ limitations under the License. .mx_RoomTile_name { display: table-cell; + width: 100%; vertical-align: middle; 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 +105,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/molecules/RoomTooltip.css b/src/skins/vector/css/molecules/RoomTooltip.css index 604c6a56..4e831d48 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; } diff --git a/src/skins/vector/css/organisms/LeftPanel.css b/src/skins/vector/css/organisms/LeftPanel.css index 67f00c35..37de0f0e 100644 --- a/src/skins/vector/css/organisms/LeftPanel.css +++ b/src/skins/vector/css/organisms/LeftPanel.css @@ -34,16 +34,21 @@ limitations under the License. cursor: pointer; } -.mx_LeftPanel .mx_RoomList { +.mx_LeftPanel_callView { + +} + +.mx_LeftPanel .mx_RoomList_scrollbar { -webkit-box-ordinal-group: 1; -moz-box-ordinal-group: 1; -ms-flex-order: 1; -webkit-order: 1; order: 1; - overflow-y: auto; -webkit-flex: 1 1 0; flex: 1 1 0; + + overflow-y: auto; } .mx_LeftPanel .mx_BottomLeftMenu { @@ -53,8 +58,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 { @@ -62,7 +69,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 34ebd1db..7f5e2272 100644 --- a/src/skins/vector/css/organisms/RoomList.css +++ b/src/skins/vector/css/organisms/RoomList.css @@ -16,13 +16,7 @@ limitations under the License. .mx_RoomList { padding-top: 24px; -} - -.mx_RoomList_invites, -.mx_RoomList_recents { - display: table; - table-layout: fixed; - width: 100%; + padding-bottom: 12px; } .mx_RoomList_expandButton { @@ -31,14 +25,3 @@ limitations under the License. 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 00000000..57d23a38 --- /dev/null +++ b/src/skins/vector/css/organisms/RoomSubList.css @@ -0,0 +1,45 @@ +/* +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_bottommost { + /* XXX: this should really be 100% of the RoomList height, but can't seem to get at it */ + min-height: 400px; +} + +.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; +} + +.mx_RoomSubList_chevron { + padding-left: 5px; +} + +.collapsed .mx_RoomSubList_chevron { + padding-left: 13px; +} diff --git a/src/skins/vector/css/organisms/RoomView.css b/src/skins/vector/css/organisms/RoomView.css index d564b086..191742f5 100644 --- a/src/skins/vector/css/organisms/RoomView.css +++ b/src/skins/vector/css/organisms/RoomView.css @@ -129,7 +129,7 @@ limitations under the License. clear: both; margin-top: 32px; margin-bottom: 8px; - margin-left: 54px; + margin-left: 63px; padding-bottom: 6px; border-bottom: 1px solid #eee; } @@ -158,18 +158,19 @@ limitations under the License. order: 4; width: 100%; - -webkit-flex: 0 0 36px; - flex: 0 0 36px; + -webkit-flex: 0 0 auto; + flex: 0 0 auto; } .mx_RoomView_statusAreaBox { max-width: 960px; margin: auto; + min-height: 36px; } .mx_RoomView_statusAreaBox_line { border-top: 1px solid #eee; - margin-left: 54px; + margin-left: 63px; height: 1px; } @@ -185,16 +186,44 @@ limitations under the License. vertical-align: middle; } +.mx_RoomView_connectionLostBar { + margin-top: 19px; + height: 58px; +} + +.mx_RoomView_connectionLostBar img { + padding-left: 10px; + padding-right: 22px; + vertical-align: middle; + float: left; +} + +.mx_RoomView_connectionLostBar_title { + color: #ff0064; +} + +.mx_RoomView_connectionLostBar_desc { + color: #454545; + font-size: 14px; + opacity: 0.5; +} + +.mx_RoomView_resend_link { + color: #454545 ! important; + text-decoration: underline ! important; + cursor: pointer; +} + .mx_RoomView_typingBar { margin-top: 10px; - margin-left: 54px; + margin-left: 63px; color: #4a4a4a; opacity: 0.5; } .mx_RoomView_typingImage { display: inline; - margin-left: -38px; + margin-left: -47px; margin-top: -4px; float: left; } @@ -214,7 +243,7 @@ limitations under the License. .mx_RoomView_uploadProgressOuter { height: 4px; - margin-left: 54px; + margin-left: 63px; margin-top: -1px; } @@ -225,7 +254,7 @@ limitations under the License. .mx_RoomView_uploadFilename { margin-top: 5px; - margin-left: 56px; + margin-left: 65px; opacity: 0.5; color: #4a4a4a; } diff --git a/src/skins/vector/css/pages/MatrixChat.css b/src/skins/vector/css/pages/MatrixChat.css index f649aa24..e6d7d30b 100644 --- a/src/skins/vector/css/pages/MatrixChat.css +++ b/src/skins/vector/css/pages/MatrixChat.css @@ -35,7 +35,7 @@ limitations under the License. -webkit-order: 1; order: 1; - height: 21px; + height: 40px; } .mx_MatrixChat_toolbarShowing { @@ -71,8 +71,8 @@ limitations under the License. background-color: #eaf5f0; - -webkit-flex: 0 0 230px; - flex: 0 0 230px; + -webkit-flex: 0 0 210px; + flex: 0 0 210px; } .mx_MatrixChat .mx_LeftPanel.collapsed { @@ -87,8 +87,8 @@ limitations under the License. -webkit-order: 2; order: 2; - padding-left: 12px; - padding-right: 12px; + padding-left: 25px; + padding-right: 22px; background-color: #fff; -webkit-flex: 1; @@ -116,8 +116,8 @@ limitations under the License. -webkit-order: 3; order: 3; - -webkit-flex: 0 0 230px; - flex: 0 0 230px; + -webkit-flex: 0 0 235px; + flex: 0 0 235px; } .mx_MatrixChat .mx_RightPanel.collapsed { diff --git a/src/skins/vector/img/cancel-black2.png b/src/skins/vector/img/cancel-black2.png new file mode 100644 index 00000000..a928c61b Binary files /dev/null and b/src/skins/vector/img/cancel-black2.png differ diff --git a/src/skins/vector/img/list-close.png b/src/skins/vector/img/list-close.png new file mode 100644 index 00000000..82b322f9 Binary files /dev/null and b/src/skins/vector/img/list-close.png differ diff --git a/src/skins/vector/img/list-open.png b/src/skins/vector/img/list-open.png new file mode 100644 index 00000000..f8c80631 Binary files /dev/null and b/src/skins/vector/img/list-open.png differ diff --git a/src/skins/vector/img/warning.png b/src/skins/vector/img/warning.png new file mode 100644 index 00000000..c5553530 Binary files /dev/null and b/src/skins/vector/img/warning.png differ diff --git a/src/skins/vector/img/warning2.png b/src/skins/vector/img/warning2.png new file mode 100644 index 00000000..db0fd4a8 Binary files /dev/null and b/src/skins/vector/img/warning2.png differ diff --git a/src/skins/vector/skindex.js b/src/skins/vector/skindex.js index 54dbad88..58fbb15d 100644 --- a/src/skins/vector/skindex.js +++ b/src/skins/vector/skindex.js @@ -81,6 +81,7 @@ 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'); diff --git a/src/skins/vector/views/atoms/MemberAvatar.js b/src/skins/vector/views/atoms/MemberAvatar.js index 69652e1a..c4153b85 100644 --- a/src/skins/vector/views/atoms/MemberAvatar.js +++ b/src/skins/vector/views/atoms/MemberAvatar.js @@ -40,6 +40,25 @@ module.exports = React.createClass({ }, render: function() { + // XXX: recalculates default avatar url constantly + if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) { + var initial; + if (this.props.member.name[0]) + initial = this.props.member.name[0].toUpperCase(); + if (initial === '@' && this.props.member.name[1]) + initial = this.props.member.name[1].toUpperCase(); + + return ( + + { initial } + + + ); + } return ( - ); + + // XXX: recalculates fallback avatar constantly + if (this.state.imageUrl === this.getFallbackAvatar()) { + var initial; + if (this.props.room.name[0]) + initial = this.props.room.name[0].toUpperCase(); + if ((initial === '@' || initial === '#') && this.props.room.name[1]) + initial = this.props.room.name[1].toUpperCase(); + + return ( + + { initial } + + + ); + } + else { + return + } + } }); diff --git a/src/skins/vector/views/atoms/Spinner.js b/src/skins/vector/views/atoms/Spinner.js index 908f2678..6dfd0c41 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/molecules/ChangePassword.js b/src/skins/vector/views/molecules/ChangePassword.js index 004fed39..32315158 100644 --- a/src/skins/vector/views/molecules/ChangePassword.js +++ b/src/skins/vector/views/molecules/ChangePassword.js @@ -27,9 +27,9 @@ module.exports = React.createClass({ mixins: [ChangePasswordController], onClickChange: function() { - var old_password = this.refs.old_input.getDOMNode().value; - var new_password = this.refs.new_input.getDOMNode().value; - var confirm_password = this.refs.confirm_input.getDOMNode().value; + var old_password = this.refs.old_input.value; + var new_password = this.refs.new_input.value; + var confirm_password = this.refs.confirm_input.value; if (new_password != confirm_password) { this.setState({ state: this.Phases.Error, diff --git a/src/skins/vector/views/molecules/MatrixToolbar.js b/src/skins/vector/views/molecules/MatrixToolbar.js index 4a299f14..361e39d6 100644 --- a/src/skins/vector/views/molecules/MatrixToolbar.js +++ b/src/skins/vector/views/molecules/MatrixToolbar.js @@ -28,12 +28,20 @@ module.exports = React.createClass({ Notifier.setToolbarHidden(true); }, + onClick: function() { + var Notifier = sdk.getComponent('organisms.Notifier'); + Notifier.setEnabled(true); + }, + render: function() { var EnableNotificationsButton = sdk.getComponent("atoms.EnableNotificationsButton"); return (
    - You are not receiving desktop notifications. -
    + /!\ +
    + You are not receiving desktop notifications. Enable them now +
    +
    ); } diff --git a/src/skins/vector/views/molecules/MessageComposer.js b/src/skins/vector/views/molecules/MessageComposer.js index 25f69bda..c75aaa14 100644 --- a/src/skins/vector/views/molecules/MessageComposer.js +++ b/src/skins/vector/views/molecules/MessageComposer.js @@ -29,7 +29,7 @@ module.exports = React.createClass({ mixins: [MessageComposerController], onUploadClick: function(ev) { - this.refs.uploadInput.getDOMNode().click(); + this.refs.uploadInput.click(); }, onUploadFileSelected: function(ev) { @@ -38,7 +38,7 @@ module.exports = React.createClass({ if (files && files.length > 0) { this.props.uploadFile(files[0]); } - this.refs.uploadInput.getDOMNode().value = null; + this.refs.uploadInput.value = null; }, onCallClick: function(ev) { diff --git a/src/skins/vector/views/molecules/MessageContextMenu.js b/src/skins/vector/views/molecules/MessageContextMenu.js index 995c2c4b..b36d4ccb 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/molecules/RoomDropTarget.js b/src/skins/vector/views/molecules/RoomDropTarget.js index b1e15077..00d0546c 100644 --- a/src/skins/vector/views/molecules/RoomDropTarget.js +++ b/src/skins/vector/views/molecules/RoomDropTarget.js @@ -18,16 +18,25 @@ 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} -
    - ); + if (this.props.placeholder) { + return ( +
    +
    + ); + } + else { + return ( +
    +
    +
    + { this.props.label } +
    +
    + ); + } } }); diff --git a/src/skins/vector/views/molecules/RoomHeader.js b/src/skins/vector/views/molecules/RoomHeader.js index 7f45fd42..cc43b1cd 100644 --- a/src/skins/vector/views/molecules/RoomHeader.js +++ b/src/skins/vector/views/molecules/RoomHeader.js @@ -35,7 +35,7 @@ module.exports = React.createClass({ }, getRoomName: function() { - return this.refs.name_edit.getDOMNode().value; + return this.refs.name_edit.value; }, onFullscreenClick: function() { diff --git a/src/skins/vector/views/molecules/RoomSettings.js b/src/skins/vector/views/molecules/RoomSettings.js index c5e08ff9..4fdd40d9 100644 --- a/src/skins/vector/views/molecules/RoomSettings.js +++ b/src/skins/vector/views/molecules/RoomSettings.js @@ -27,15 +27,15 @@ module.exports = React.createClass({ mixins: [RoomSettingsController], getTopic: function() { - return this.refs.topic.getDOMNode().value; + return this.refs.topic.value; }, getJoinRules: function() { - return this.refs.is_private.getDOMNode().checked ? "invite" : "public"; + return this.refs.is_private.checked ? "invite" : "public"; }, getHistoryVisibility: function() { - return this.refs.share_history.getDOMNode().checked ? "shared" : "invited"; + return this.refs.share_history.checked ? "shared" : "invited"; }, getPowerLevels: function() { @@ -45,13 +45,13 @@ module.exports = React.createClass({ power_levels = power_levels.getContent(); var new_power_levels = { - ban: parseInt(this.refs.ban.getDOMNode().value), - kick: parseInt(this.refs.kick.getDOMNode().value), - redact: parseInt(this.refs.redact.getDOMNode().value), - invite: parseInt(this.refs.invite.getDOMNode().value), - events_default: parseInt(this.refs.events_default.getDOMNode().value), - state_default: parseInt(this.refs.state_default.getDOMNode().value), - users_default: parseInt(this.refs.users_default.getDOMNode().value), + ban: parseInt(this.refs.ban.value), + kick: parseInt(this.refs.kick.value), + redact: parseInt(this.refs.redact.value), + invite: parseInt(this.refs.invite.value), + events_default: parseInt(this.refs.events_default.value), + state_default: parseInt(this.refs.state_default.value), + users_default: parseInt(this.refs.users_default.value), users: power_levels.users, events: power_levels.events, }; diff --git a/src/skins/vector/views/molecules/RoomTile.js b/src/skins/vector/views/molecules/RoomTile.js index 82616b5a..471bd8a1 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,178 @@ 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 = { + canDrag: function(props, monitor) { + return props.roomSubList.props.editable; + }, + + 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 + lastTargetRoom: null, + lastYOffset: null, + lastYDelta: null, + }; + + if (props.roomSubList.debug) 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; + }, + + endDrag: function (props, monitor, component) { + var item = monitor.getItem(); + + if (props.roomSubList.debug) console.log("roomTile endDrag for " + item.room.roomId + " with didDrop=" + monitor.didDrop()); + + props.room._dragging = false; + if (monitor.didDrop()) { + if (props.roomSubList.debug) console.log("force updating component " + item.targetList.props.label); + item.targetList.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) { + // 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 + if (props.roomSubList.debug) console.log("cancelling drop & drag"); + props.roomSubList.moveRoomTile(item.room, item.originalIndex); + if (item.targetList && item.targetList !== item.originalList) { + item.targetList.removeRoomTile(item.room); + } + } + } +}; + +var roomTileTarget = { + canDrop: function() { + return false; + }, + + hover: function(props, monitor) { + var item = monitor.getItem(); + var off = monitor.getClientOffset(); + // 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. + if (props.roomSubList.debug) console.log("switched target sublist"); + switchedTarget = true; + item.targetList.removeRoomTile(item.room); + item.targetList = props.roomSubList; + } + + if (!item.targetList.props.editable) return; + + if (item.targetList.props.order === 'manual') { + 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) { + // add to the list in the right place + props.roomSubList.moveRoomTile(item.room, 0); + } + // we have to sort the list whatever to recalculate it + props.roomSubList.sortList(); + } + }, +}; + +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 }); }, @@ -42,21 +212,32 @@ module.exports = 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({ '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; 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; + // 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 @@ -91,7 +272,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(
    @@ -99,6 +287,27 @@ 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, 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) { + 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/molecules/RoomTooltip.js b/src/skins/vector/views/molecules/RoomTooltip.js index 82e3e744..51b13526 100644 --- a/src/skins/vector/views/molecules/RoomTooltip.js +++ b/src/skins/vector/views/molecules/RoomTooltip.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; var React = require('react'); +var ReactDOM = require('react-dom'); var dis = require('matrix-react-sdk/lib/dispatcher'); @@ -24,21 +25,21 @@ module.exports = React.createClass({ displayName: 'RoomTooltip', componentDidMount: function() { + var tooltip = ReactDOM.findDOMNode(this); if (!this.props.bottom) { // tell the roomlist about us so it can position us dis.dispatch({ action: 'view_tooltip', - tooltip: this.getDOMNode(), + tooltip: tooltip, }); } else { - var tooltip = this.getDOMNode(); tooltip.style.top = tooltip.parentElement.getBoundingClientRect().top + "px"; tooltip.style.display = "block"; } }, - componentDidUnmount: function() { + componentWillUnmount: function() { if (!this.props.bottom) { dis.dispatch({ action: 'view_tooltip', diff --git a/src/skins/vector/views/molecules/SearchBar.js b/src/skins/vector/views/molecules/SearchBar.js index d31e24b4..585b9a6d 100644 --- a/src/skins/vector/views/molecules/SearchBar.js +++ b/src/skins/vector/views/molecules/SearchBar.js @@ -39,7 +39,7 @@ module.exports = React.createClass({ onSearchChange: function(e) { if (e.keyCode === 13) { // on enter... - this.props.onSearch(this.refs.search_term.getDOMNode().value, this.state.scope); + this.props.onSearch(this.refs.search_term.value, this.state.scope); } }, diff --git a/src/skins/vector/views/molecules/UserSelector.js b/src/skins/vector/views/molecules/UserSelector.js index 6b233690..58cb7d21 100644 --- a/src/skins/vector/views/molecules/UserSelector.js +++ b/src/skins/vector/views/molecules/UserSelector.js @@ -25,8 +25,8 @@ module.exports = React.createClass({ mixins: [UserSelectorController], onAddUserId: function() { - this.addUser(this.refs.user_id_input.getDOMNode().value); - this.refs.user_id_input.getDOMNode().value = ""; + this.addUser(this.refs.user_id_input.value); + this.refs.user_id_input.value = ""; }, render: function() { diff --git a/src/skins/vector/views/molecules/voip/CallView.js b/src/skins/vector/views/molecules/voip/CallView.js index 07987bd3..52297bbc 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/IncomingCallBox.js b/src/skins/vector/views/molecules/voip/IncomingCallBox.js index c3bcd825..bf129904 100644 --- a/src/skins/vector/views/molecules/voip/IncomingCallBox.js +++ b/src/skins/vector/views/molecules/voip/IncomingCallBox.js @@ -27,7 +27,7 @@ module.exports = React.createClass({ mixins: [IncomingCallBoxController], getRingAudio: function() { - return this.refs.ringAudio.getDOMNode(); + return this.refs.ringAudio; }, render: function() { diff --git a/src/skins/vector/views/molecules/voip/VideoView.js b/src/skins/vector/views/molecules/voip/VideoView.js index 4e0fb913..75a2500d 100644 --- a/src/skins/vector/views/molecules/voip/VideoView.js +++ b/src/skins/vector/views/molecules/voip/VideoView.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; var React = require('react'); +var ReactDOM = require('react-dom'); var sdk = require('matrix-react-sdk') var dis = require('matrix-react-sdk/lib/dispatcher') @@ -29,15 +30,15 @@ module.exports = React.createClass({ }, getRemoteVideoElement: function() { - return this.refs.remote.getDOMNode(); + return ReactDOM.findDOMNode(this.refs.remote); }, getRemoteAudioElement: function() { - return this.refs.remoteAudio.getDOMNode(); + return this.refs.remoteAudio; }, getLocalVideoElement: function() { - return this.refs.local.getDOMNode(); + return ReactDOM.findDOMNode(this.refs.local); }, setContainer: function(c) { @@ -50,7 +51,7 @@ module.exports = React.createClass({ if (!this.container) { return; } - var element = this.container.getDOMNode(); + var element = this.container; if (payload.fullscreen) { var requestMethod = ( element.requestFullScreen || @@ -78,7 +79,7 @@ module.exports = React.createClass({ render: function() { var VideoFeed = sdk.getComponent('atoms.voip.VideoFeed'); return ( -
    +