Merge remote-tracking branch 'origin/develop' into dbkr/dont_crash_if_no_dm_rooms

This commit is contained in:
David Baker 2016-09-09 17:38:13 +01:00
commit 8e518af96c
5 changed files with 185 additions and 124 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;
}
} }
}, },
@ -233,10 +229,6 @@ module.exports = React.createClass({
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists // 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" || else if (me.membership == "join" || me.membership === "ban" ||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) (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); 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 { else {
s.lists["im.vector.fake.recent"].push(room); s.lists["im.vector.fake.recent"].push(room);
} }

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;
}
}
} }
} }
@ -43,8 +38,26 @@ 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();
}
// Here, we return undefined if the room is not in the map: // 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. // the room ID you gave is not a DM room for any user.
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;
}