Merge pull request #455 from matrix-org/dbkr/memberinfo_list_rooms

Add DM rooms with that person to the MemberInfo panel
This commit is contained in:
David Baker 2016-09-09 17:30:32 +01:00 committed by GitHub
commit 78f53bd403
5 changed files with 181 additions and 120 deletions

View file

@ -26,12 +26,16 @@ limitations under the License.
* 'isTargetMod': boolean * 'isTargetMod': boolean
*/ */
var React = require('react'); var React = require('react');
var classNames = require('classnames');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
var Modal = require("../../../Modal"); var Modal = require("../../../Modal");
var sdk = require('../../../index'); var sdk = require('../../../index');
var UserSettingsStore = require('../../../UserSettingsStore'); var UserSettingsStore = require('../../../UserSettingsStore');
var createRoom = require('../../../createRoom'); var createRoom = require('../../../createRoom');
var DMRoomMap = require('../../../utils/DMRoomMap');
var Unread = require('../../../Unread');
var Receipt = require('../../../utils/Receipt');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MemberInfo', displayName: 'MemberInfo',
@ -60,7 +64,6 @@ module.exports = React.createClass({
updating: 0, updating: 0,
devicesLoading: true, devicesLoading: true,
devices: null, devices: null,
existingOneToOneRoomId: null,
} }
}, },
@ -72,14 +75,20 @@ module.exports = React.createClass({
this._enableDevices = MatrixClientPeg.get().isCryptoEnabled() && this._enableDevices = MatrixClientPeg.get().isCryptoEnabled() &&
UserSettingsStore.isFeatureEnabled("e2e_encryption"); UserSettingsStore.isFeatureEnabled("e2e_encryption");
this.setState({ const cli = MatrixClientPeg.get();
existingOneToOneRoomId: this.getExistingOneToOneRoomId() 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() { componentDidMount: function() {
this._updateStateForNewMember(this.props.member); this._updateStateForNewMember(this.props.member);
MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged);
}, },
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
@ -92,65 +101,20 @@ module.exports = React.createClass({
var client = MatrixClientPeg.get(); var client = MatrixClientPeg.get();
if (client) { if (client) {
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); 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) { if (this._cancelDeviceList) {
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) { onDeviceVerificationChanged: function(userId, device) {
if (!this._enableDevices) { if (!this._enableDevices) {
return; 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) { _updateStateForNewMember: function(member) {
var newState = this._calculateOpsPermissions(member); var newState = this._calculateOpsPermissions(member);
newState.devicesLoading = true; newState.devicesLoading = true;
@ -416,33 +419,16 @@ module.exports = React.createClass({
} }
}, },
onChatClick: function() { onNewDMClick: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); this.setState({ updating: this.state.updating + 1 });
createRoom({
// TODO: keep existingOneToOneRoomId updated if we see any room member changes anywhere createOpts: {
invite: [this.props.member.userId],
const useExistingOneToOneRoom = this.state.existingOneToOneRoomId && (this.state.existingOneToOneRoomId !== this.props.member.roomId); },
}).finally(() => {
// 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,
});
this.props.onFinished(); this.props.onFinished();
} this.setState({ updating: this.state.updating - 1 });
else { }).done();
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();
}
}, },
onLeaveClick: function() { onLeaveClick: function() {
@ -583,24 +569,50 @@ module.exports = React.createClass({
render: function() { render: function() {
var startChat, kickButton, banButton, muteButton, giveModButton, spinner; var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) { if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
// FIXME: we're referring to a vector component from react-sdk const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile'); const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId);
var label; const RoomTile = sdk.getComponent("rooms.RoomTile");
if (this.state.existingOneToOneRoomId) {
if (this.state.existingOneToOneRoomId == this.props.member.roomId) { const tiles = [];
label = "Start new direct chat"; for (const roomId of dmRooms) {
} const room = MatrixClientPeg.get().getRoom(roomId);
else { if (room) {
label = "Go to direct chat"; const me = room.getMember(MatrixClientPeg.get().credentials.userId);
const highlight = (
room.getUnreadNotificationCount('highlight') > 0 ||
me.membership == "invite"
);
tiles.push(
<RoomTile key={room.roomId} room={room}
collapsed={false}
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={highlight}
isInvite={me.membership == "invite"}
/>
);
} }
} }
else {
label = "Start direct chat";
}
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" const labelClasses = classNames({
label={ label } onClick={ this.onChatClick }/> mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
});
const startNewChat = <div
className="mx_MemberInfo_createRoom"
onClick={this.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>Start new direct chat</i></div>
</div>
startChat = <div>
{tiles}
{startNewChat}
</div>;
} }
if (this.state.updating) { if (this.state.updating) {

View file

@ -27,6 +27,7 @@ var sdk = require('../../../index');
var rate_limited_func = require('../../../ratelimitedfunc'); var rate_limited_func = require('../../../ratelimitedfunc');
var Rooms = require('../../../Rooms'); var Rooms = require('../../../Rooms');
var DMRoomMap = require('../../../utils/DMRoomMap'); var DMRoomMap = require('../../../utils/DMRoomMap');
var Receipt = require('../../../utils/Receipt');
var HIDE_CONFERENCE_CHANS = true; var HIDE_CONFERENCE_CHANS = true;
@ -154,13 +155,8 @@ module.exports = React.createClass({
onRoomReceipt: function(receiptEvent, room) { onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count // because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us // only bother updating if there's a receipt from us
var receiptKeys = Object.keys(receiptEvent.getContent()); if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
for (var i = 0; i < receiptKeys.length; ++i) { this._delayedRefreshRoomList();
var rcpt = receiptEvent.getContent()[receiptKeys[i]];
if (rcpt['m.read'] && rcpt['m.read'][MatrixClientPeg.get().credentials.userId]) {
this._delayedRefreshRoomList();
break;
}
} }
}, },

View file

@ -30,10 +30,9 @@ module.exports = React.createClass({
displayName: 'RoomTile', displayName: 'RoomTile',
propTypes: { propTypes: {
// TODO: We should *optionally* support DND stuff and ideally be impl agnostic about it connectDragSource: React.PropTypes.func,
connectDragSource: React.PropTypes.func.isRequired, connectDropTarget: React.PropTypes.func,
connectDropTarget: React.PropTypes.func.isRequired, isDragging: React.PropTypes.bool,
isDragging: React.PropTypes.bool.isRequired,
room: React.PropTypes.object.isRequired, room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired, collapsed: React.PropTypes.bool.isRequired,
@ -41,11 +40,15 @@ module.exports = React.createClass({
unread: React.PropTypes.bool.isRequired, unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired, highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired, isInvite: React.PropTypes.bool.isRequired,
roomSubList: React.PropTypes.object.isRequired,
refreshSubList: React.PropTypes.func.isRequired,
incomingCall: React.PropTypes.object, incomingCall: React.PropTypes.object,
}, },
getDefaultProps: function() {
return {
isDragging: false,
};
},
getInitialState: function() { getInitialState: function() {
return({ return({
hover : false, hover : false,
@ -281,7 +284,7 @@ module.exports = React.createClass({
var connectDragSource = this.props.connectDragSource; var connectDragSource = this.props.connectDragSource;
var connectDropTarget = this.props.connectDropTarget; var connectDropTarget = this.props.connectDropTarget;
return connectDragSource(connectDropTarget( let ret = (
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}> <div className={avatarClasses}>
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}> <div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
@ -298,6 +301,11 @@ module.exports = React.createClass({
{ incomingCallBox } { incomingCallBox }
{ tooltip } { tooltip }
</div> </div>
)); );
if (connectDropTarget) ret = connectDropTarget(ret);
if (connectDragSource) ret = connectDragSource(ret);
return ret;
} }
}); });

View file

@ -21,18 +21,13 @@ limitations under the License.
*/ */
export default class DMRoomMap { export default class DMRoomMap {
constructor(matrixClient) { constructor(matrixClient) {
this.roomToUser = null;
const mDirectEvent = matrixClient.getAccountData('m.direct'); const mDirectEvent = matrixClient.getAccountData('m.direct');
if (!mDirectEvent) { if (!mDirectEvent) {
this.userToRooms = {}; this.userToRooms = {};
this.roomToUser = {};
} else { } else {
this.userToRooms = mDirectEvent.getContent(); 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;
}
}
} }
} }
@ -41,6 +36,24 @@ export default class DMRoomMap {
} }
getUserIdForRoomId(roomId) { 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();
}
return this.roomToUser[roomId]; 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;
}
}
}
} }

32
src/utils/Receipt.js Normal file
View file

@ -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;
}