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
*/
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(
<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"
label={ label } onClick={ this.onChatClick }/>
const labelClasses = classNames({
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) {

View file

@ -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();
}
},

View file

@ -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 = (
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_menu" onClick={this.onAvatarClicked}>
@ -298,6 +301,11 @@ module.exports = React.createClass({
{ incomingCallBox }
{ tooltip }
</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 {
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;
}
}
}
}
@ -41,6 +36,24 @@ 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();
}
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;
}