diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index be6663be67..b6944a3d4e 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -26,12 +26,16 @@ limitations under the License. * 'isTargetMod': boolean */ var React = require('react'); +var classNames = require('classnames'); var MatrixClientPeg = require("../../../MatrixClientPeg"); var dis = require("../../../dispatcher"); var Modal = require("../../../Modal"); var sdk = require('../../../index'); var UserSettingsStore = require('../../../UserSettingsStore'); var createRoom = require('../../../createRoom'); +var DMRoomMap = require('../../../utils/DMRoomMap'); +var Unread = require('../../../Unread'); +var Receipt = require('../../../utils/Receipt'); module.exports = React.createClass({ displayName: 'MemberInfo', @@ -60,7 +64,6 @@ module.exports = React.createClass({ updating: 0, devicesLoading: true, devices: null, - existingOneToOneRoomId: null, } }, @@ -72,14 +75,20 @@ module.exports = React.createClass({ this._enableDevices = MatrixClientPeg.get().isCryptoEnabled() && UserSettingsStore.isFeatureEnabled("e2e_encryption"); - this.setState({ - existingOneToOneRoomId: this.getExistingOneToOneRoomId() - }); + const cli = MatrixClientPeg.get(); + cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged); + cli.on("Room", this.onRoom); + cli.on("deleteRoom", this.onDeleteRoom); + cli.on("Room.timeline", this.onRoomTimeline); + cli.on("Room.name", this.onRoomName); + cli.on("Room.receipt", this.onRoomReceipt); + cli.on("RoomState.events", this.onRoomStateEvents); + cli.on("RoomMember.name", this.onRoomMemberName); + cli.on("accountData", this.onAccountData); }, componentDidMount: function() { this._updateStateForNewMember(this.props.member); - MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged); }, componentWillReceiveProps: function(newProps) { @@ -92,65 +101,20 @@ module.exports = React.createClass({ var client = MatrixClientPeg.get(); if (client) { client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); + client.removeListener("Room", this.onRoom); + client.removeListener("deleteRoom", this.onDeleteRoom); + client.removeListener("Room.timeline", this.onRoomTimeline); + client.removeListener("Room.name", this.onRoomName); + client.removeListener("Room.receipt", this.onRoomReceipt); + client.removeListener("RoomState.events", this.onRoomStateEvents); + client.removeListener("RoomMember.name", this.onRoomMemberName); + client.removeListener("accountData", this.onAccountData); } if (this._cancelDeviceList) { this._cancelDeviceList(); } }, - getExistingOneToOneRoomId: function() { - const rooms = MatrixClientPeg.get().getRooms(); - const userIds = [ - this.props.member.userId, - MatrixClientPeg.get().credentials.userId - ]; - let existingRoomId = null; - let invitedRoomId = null; - - // roomId can be null here because of a hack in MatrixChat.onUserClick where we - // abuse this to view users rather than room members. - let currentMembers; - if (this.props.member.roomId) { - const currentRoom = MatrixClientPeg.get().getRoom(this.props.member.roomId); - currentMembers = currentRoom.getJoinedMembers(); - } - - // reuse the first private 1:1 we find - existingRoomId = null; - - for (let i = 0; i < rooms.length; i++) { - // don't try to reuse public 1:1 rooms - const join_rules = rooms[i].currentState.getStateEvents("m.room.join_rules", ''); - if (join_rules && join_rules.getContent().join_rule === 'public') continue; - - const members = rooms[i].getJoinedMembers(); - if (members.length === 2 && - userIds.indexOf(members[0].userId) !== -1 && - userIds.indexOf(members[1].userId) !== -1) - { - existingRoomId = rooms[i].roomId; - break; - } - - const invited = rooms[i].getMembersWithMembership('invite'); - if (members.length === 1 && - invited.length === 1 && - userIds.indexOf(members[0].userId) !== -1 && - userIds.indexOf(invited[0].userId) !== -1 && - invitedRoomId === null) - { - invitedRoomId = rooms[i].roomId; - // keep looking: we'll use this one if there's nothing better - } - } - - if (existingRoomId === null) { - existingRoomId = invitedRoomId; - } - - return existingRoomId; - }, - onDeviceVerificationChanged: function(userId, device) { if (!this._enableDevices) { return; @@ -164,6 +128,45 @@ module.exports = React.createClass({ } }, + onRoom: function(room) { + this.forceUpdate(); + }, + + onDeleteRoom: function(roomId) { + this.forceUpdate(); + }, + + onRoomTimeline: function(ev, room, toStartOfTimeline) { + if (toStartOfTimeline) return; + this.forceUpdate(); + }, + + onRoomName: function(room) { + this.forceUpdate(); + }, + + onRoomReceipt: function(receiptEvent, room) { + // because if we read a notification, it will affect notification count + // only bother updating if there's a receipt from us + if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { + this.forceUpdate(); + } + }, + + onRoomStateEvents: function(ev, state) { + this.forceUpdate(); + }, + + onRoomMemberName: function(ev, member) { + this.forceUpdate(); + }, + + onAccountData: function(ev) { + if (ev.getType() == 'm.direct') { + this.forceUpdate(); + } + }, + _updateStateForNewMember: function(member) { var newState = this._calculateOpsPermissions(member); newState.devicesLoading = true; @@ -416,33 +419,16 @@ module.exports = React.createClass({ } }, - onChatClick: function() { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - - // TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere - - const useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId); - - // check if there are any existing rooms with just us and them (1:1) - // If so, just view that room. If not, create a private room with them. - if (useExistingOneToOneRoom) { - dis.dispatch({ - action: 'view_room', - room_id: this.state.existingOneToOneRoomId, - }); + onNewDMClick: function() { + this.setState({ updating: this.state.updating + 1 }); + createRoom({ + createOpts: { + invite: [this.props.member.userId], + }, + }).finally(() => { this.props.onFinished(); - } - else { - this.setState({ updating: this.state.updating + 1 }); - createRoom({ - createOpts: { - invite: [this.props.member.userId], - }, - }).finally(() => { - this.props.onFinished(); - this.setState({ updating: this.state.updating - 1 }); - }).done(); - } + this.setState({ updating: this.state.updating - 1 }); + }).done(); }, onLeaveClick: function() { @@ -583,24 +569,50 @@ module.exports = React.createClass({ render: function() { var startChat, kickButton, banButton, muteButton, giveModButton, spinner; if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) { - // FIXME: we're referring to a vector component from react-sdk - var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile'); + const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); + const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId); - var label; - if (this.state.existingOneToOneRoomId) { - if (this.state.existingOneToOneRoomId == this.props.member.roomId) { - label = "Start new direct chat"; - } - else { - label = "Go to direct chat"; + const RoomTile = sdk.getComponent("rooms.RoomTile"); + + const tiles = []; + for (const roomId of dmRooms) { + const room = MatrixClientPeg.get().getRoom(roomId); + if (room) { + const me = room.getMember(MatrixClientPeg.get().credentials.userId); + const highlight = ( + room.getUnreadNotificationCount('highlight') > 0 || + me.membership == "invite" + ); + tiles.push( + + ); } } - else { - label = "Start direct chat"; - } - startChat = + const labelClasses = classNames({ + mx_MemberInfo_createRoom_label: true, + mx_RoomTile_name: true, + }); + const startNewChat =
+
+ +
+
Start new direct chat
+
+ + startChat =
+ {tiles} + {startNewChat} +
; } if (this.state.updating) { diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 1265fc1af0..bb4c6d336a 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -27,6 +27,7 @@ var sdk = require('../../../index'); var rate_limited_func = require('../../../ratelimitedfunc'); var Rooms = require('../../../Rooms'); var DMRoomMap = require('../../../utils/DMRoomMap'); +var Receipt = require('../../../utils/Receipt'); var HIDE_CONFERENCE_CHANS = true; @@ -154,13 +155,8 @@ module.exports = React.createClass({ onRoomReceipt: function(receiptEvent, room) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us - var receiptKeys = Object.keys(receiptEvent.getContent()); - for (var i = 0; i < receiptKeys.length; ++i) { - var rcpt = receiptEvent.getContent()[receiptKeys[i]]; - if (rcpt['m.read'] && rcpt['m.read'][MatrixClientPeg.get().credentials.userId]) { - this._delayedRefreshRoomList(); - break; - } + if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) { + this._delayedRefreshRoomList(); } }, @@ -233,10 +229,6 @@ module.exports = React.createClass({ else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { // skip past this room & don't put it in any lists } - else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { - // "Direct Message" rooms - s.lists["im.vector.fake.direct"].push(room); - } else if (me.membership == "join" || me.membership === "ban" || (me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) { @@ -250,6 +242,10 @@ module.exports = React.createClass({ s.lists[tagNames[i]].push(room); } } + else if (dmRoomMap.getUserIdForRoomId(room.roomId)) { + // "Direct Message" rooms (that we're still in and that aren't otherwise tagged) + s.lists["im.vector.fake.direct"].push(room); + } else { s.lists["im.vector.fake.recent"].push(room); } diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index ac79da9851..bac8f5aa07 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -30,10 +30,9 @@ module.exports = React.createClass({ displayName: 'RoomTile', propTypes: { - // TODO: We should *optionally* support DND stuff and ideally be impl agnostic about it - connectDragSource: React.PropTypes.func.isRequired, - connectDropTarget: React.PropTypes.func.isRequired, - isDragging: React.PropTypes.bool.isRequired, + connectDragSource: React.PropTypes.func, + connectDropTarget: React.PropTypes.func, + isDragging: React.PropTypes.bool, room: React.PropTypes.object.isRequired, collapsed: React.PropTypes.bool.isRequired, @@ -41,11 +40,15 @@ module.exports = React.createClass({ unread: React.PropTypes.bool.isRequired, highlight: React.PropTypes.bool.isRequired, isInvite: React.PropTypes.bool.isRequired, - roomSubList: React.PropTypes.object.isRequired, - refreshSubList: React.PropTypes.func.isRequired, incomingCall: React.PropTypes.object, }, + getDefaultProps: function() { + return { + isDragging: false, + }; + }, + getInitialState: function() { return({ hover : false, @@ -281,7 +284,7 @@ module.exports = React.createClass({ var connectDragSource = this.props.connectDragSource; var connectDropTarget = this.props.connectDropTarget; - return connectDragSource(connectDropTarget( + let ret = (
@@ -298,6 +301,11 @@ module.exports = React.createClass({ { incomingCallBox } { tooltip }
- )); + ); + + if (connectDropTarget) ret = connectDropTarget(ret); + if (connectDragSource) ret = connectDragSource(ret); + + return ret; } }); diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.js index 95b4edb546..3bcc492a9f 100644 --- a/src/utils/DMRoomMap.js +++ b/src/utils/DMRoomMap.js @@ -21,18 +21,13 @@ limitations under the License. */ export default class DMRoomMap { constructor(matrixClient) { + this.roomToUser = null; + const mDirectEvent = matrixClient.getAccountData('m.direct'); if (!mDirectEvent) { this.userToRooms = {}; - this.roomToUser = {}; } else { this.userToRooms = mDirectEvent.getContent(); - this.roomToUser = {}; - for (const user of Object.keys(this.userToRooms)) { - for (const roomId of this.userToRooms[user]) { - this.roomToUser[roomId] = user; - } - } } } @@ -43,8 +38,26 @@ export default class DMRoomMap { } getUserIdForRoomId(roomId) { + if (this.roomToUser == null) { + // we lazily populate roomToUser so you can use + // this class just to call getDMRoomsForUserId + // which doesn't do very much, but is a fairly + // convenient wrapper and there's no point + // iterating through the map if getUserIdForRoomId() + // is never called. + this._populateRoomToUser(); + } // Here, we return undefined if the room is not in the map: // the room ID you gave is not a DM room for any user. return this.roomToUser[roomId]; } + + _populateRoomToUser() { + this.roomToUser = {}; + for (const user of Object.keys(this.userToRooms)) { + for (const roomId of this.userToRooms[user]) { + this.roomToUser[roomId] = user; + } + } + } } diff --git a/src/utils/Receipt.js b/src/utils/Receipt.js new file mode 100644 index 0000000000..549a0fd8b3 --- /dev/null +++ b/src/utils/Receipt.js @@ -0,0 +1,32 @@ +/* +Copyright 2016 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. +*/ + +/** + * Given MatrixEvent containing receipts, return the first + * read receipt from the given user ID, or null if no such + * receipt exists. + */ +export function findReadReceiptFromUserId(receiptEvent, userId) { + var receiptKeys = Object.keys(receiptEvent.getContent()); + for (var i = 0; i < receiptKeys.length; ++i) { + var rcpt = receiptEvent.getContent()[receiptKeys[i]]; + if (rcpt['m.read'] && rcpt['m.read'][userId]) { + return rcpt; + } + } + + return null; +}