Merge remote-tracking branch 'matrix-org/develop' into travis/granular-settings

This commit is contained in:
Travis Ralston 2017-11-08 17:43:38 -07:00
commit 030633fa90
12 changed files with 134 additions and 75 deletions

View file

@ -49,20 +49,26 @@ export function showGroupInviteDialog(groupId) {
export function showGroupAddRoomDialog(groupId) { export function showGroupAddRoomDialog(groupId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let addRoomsPublicly = false;
const onCheckboxClicked = (e) => {
addRoomsPublicly = e.target.checked;
};
const description = <div> const description = <div>
<div>{ _t("Which rooms would you like to add to this community?") }</div> <div>{ _t("Which rooms would you like to add to this community?") }</div>
<div className="warning">
{ _t(
"Warning: any room you add to a community will be publicly "+
"visible to anyone who knows the community ID",
) }
</div>
</div>; </div>;
const checkboxContainer = <label className="mx_GroupAddressPicker_checkboxContainer">
<input type="checkbox" onClick={onCheckboxClicked} />
<div>
{ _t("Show these rooms to non-members on the community page and room list?") }
</div>
</label>;
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, { Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
title: _t("Add rooms to the community"), title: _t("Add rooms to the community"),
description: description, description: description,
extraNode: checkboxContainer,
placeholder: _t("Room name or alias"), placeholder: _t("Room name or alias"),
button: _t("Add to community"), button: _t("Add to community"),
pickerType: 'room', pickerType: 'room',
@ -70,7 +76,7 @@ export function showGroupAddRoomDialog(groupId) {
onFinished: (success, addrs) => { onFinished: (success, addrs) => {
if (!success) return; if (!success) return;
_onGroupAddRoomFinished(groupId, addrs).then(resolve, reject); _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly).then(resolve, reject);
}, },
}); });
}); });
@ -106,13 +112,13 @@ function _onGroupInviteFinished(groupId, addrs) {
}); });
} }
function _onGroupAddRoomFinished(groupId, addrs) { function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
const matrixClient = MatrixClientPeg.get(); const matrixClient = MatrixClientPeg.get();
const groupStore = GroupStoreCache.getGroupStore(matrixClient, groupId); const groupStore = GroupStoreCache.getGroupStore(matrixClient, groupId);
const errorList = []; const errorList = [];
return Promise.all(addrs.map((addr) => { return Promise.all(addrs.map((addr) => {
return groupStore return groupStore
.addRoomToGroup(addr.address) .addRoomToGroup(addr.address, addRoomsPublicly)
.catch(() => { errorList.push(addr.address); }) .catch(() => { errorList.push(addr.address); })
.then(() => { .then(() => {
const roomId = addr.address; const roomId = addr.address;

View file

@ -25,6 +25,7 @@ const onAction = function(payload) {
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
isDialogOpen = true; isDialogOpen = true;
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, { Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
devices: payload.err.devices,
room: payload.room, room: payload.room,
onFinished: (r) => { onFinished: (r) => {
isDialogOpen = false; isDialogOpen = false;

View file

@ -430,6 +430,7 @@ export default React.createClass({
uploadingAvatar: false, uploadingAvatar: false,
membershipBusy: false, membershipBusy: false,
publicityBusy: false, publicityBusy: false,
inviterProfile: null,
}; };
}, },
@ -463,6 +464,10 @@ export default React.createClass({
}, },
_initGroupStore: function(groupId, firstInit) { _initGroupStore: function(groupId, firstInit) {
const group = MatrixClientPeg.get().getGroup(groupId);
if (group && group.inviter && group.inviter.userId) {
this._fetchInviterProfile(group.inviter.userId);
}
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId); this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
this._groupStore.registerListener(() => { this._groupStore.registerListener(() => {
const summary = this._groupStore.getSummary(); const summary = this._groupStore.getSummary();
@ -497,6 +502,26 @@ export default React.createClass({
}); });
}, },
_fetchInviterProfile(userId) {
this.setState({
inviterProfileBusy: true,
});
MatrixClientPeg.get().getProfileInfo(userId).then((resp) => {
this.setState({
inviterProfile: {
avatarUrl: resp.avatar_url,
displayName: resp.displayname,
},
});
}).catch((e) => {
console.error('Error getting group inviter profile', e);
}).finally(() => {
this.setState({
inviterProfileBusy: false,
});
});
},
_onShowRhsClick: function(ev) { _onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' }); dis.dispatch({ action: 'show_right_panel' });
}, },
@ -591,7 +616,7 @@ export default React.createClass({
_onAcceptInviteClick: function() { _onAcceptInviteClick: function() {
this.setState({membershipBusy: true}); this.setState({membershipBusy: true});
MatrixClientPeg.get().acceptGroupInvite(this.props.groupId).then(() => { this._groupStore.acceptGroupInvite().then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync // don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => { }).catch((e) => {
this.setState({membershipBusy: false}); this.setState({membershipBusy: false});
@ -802,20 +827,37 @@ export default React.createClass({
_getMembershipSection: function() { _getMembershipSection: function() {
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const group = MatrixClientPeg.get().getGroup(this.props.groupId); const group = MatrixClientPeg.get().getGroup(this.props.groupId);
if (!group) return null; if (!group) return null;
if (group.myMembership === 'invite') { if (group.myMembership === 'invite') {
if (this.state.membershipBusy) { if (this.state.membershipBusy || this.state.inviterProfileBusy) {
return <div className="mx_GroupView_membershipSection"> return <div className="mx_GroupView_membershipSection">
<Spinner /> <Spinner />
</div>; </div>;
} }
const httpInviterAvatar = this.state.inviterProfile ?
MatrixClientPeg.get().mxcUrlToHttp(
this.state.inviterProfile.avatarUrl, 36, 36,
) : null;
let inviterName = group.inviter.userId;
if (this.state.inviterProfile) {
inviterName = this.state.inviterProfile.displayName || group.inviter.userId;
}
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited"> return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
<div className="mx_GroupView_membershipSubSection"> <div className="mx_GroupView_membershipSubSection">
<div className="mx_GroupView_membershipSection_description"> <div className="mx_GroupView_membershipSection_description">
{ _t("%(inviter)s has invited you to join this community", {inviter: group.inviter.userId}) } <BaseAvatar url={httpInviterAvatar}
name={inviterName}
width={36}
height={36}
/>
{ _t("%(inviter)s has invited you to join this community", {
inviter: inviterName,
}) }
</div> </div>
<div className="mx_GroupView_membership_buttonContainer"> <div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton" <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"

View file

@ -34,6 +34,8 @@ module.exports = React.createClass({
propTypes: { propTypes: {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
description: PropTypes.node, description: PropTypes.node,
// Extra node inserted after picker input, dropdown and errors
extraNode: PropTypes.node,
value: PropTypes.string, value: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
roomId: PropTypes.string, roomId: PropTypes.string,
@ -268,34 +270,53 @@ module.exports = React.createClass({
const rooms = MatrixClientPeg.get().getRooms(); const rooms = MatrixClientPeg.get().getRooms();
const results = []; const results = [];
rooms.forEach((room) => { rooms.forEach((room) => {
let rank = Infinity;
const nameEvent = room.currentState.getStateEvents('m.room.name', ''); const nameEvent = room.currentState.getStateEvents('m.room.name', '');
const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
const name = nameEvent ? nameEvent.getContent().name : ''; const name = nameEvent ? nameEvent.getContent().name : '';
const canonicalAlias = room.getCanonicalAlias(); const canonicalAlias = room.getCanonicalAlias();
const aliasEvents = room.currentState.getStateEvents('m.room.aliases'); const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => { const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
return a.concat(b); return a.concat(b);
}, []); }, []);
const topic = topicEvent ? topicEvent.getContent().topic : '';
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery); const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = aliases.some((alias) => let aliasMatch = false;
(alias || '').toLowerCase().includes(lowerCaseQuery), let shortestMatchingAliasLength = Infinity;
); aliases.forEach((alias) => {
const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery); if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
if (!(nameMatch || topicMatch || aliasMatch)) { aliasMatch = true;
if (shortestMatchingAliasLength > alias.length) {
shortestMatchingAliasLength = alias.length;
}
}
});
if (!(nameMatch || aliasMatch)) {
return; return;
} }
if (aliasMatch) {
// A shorter matching alias will give a better rank
rank = shortestMatchingAliasLength;
}
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', ''); const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined; const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
results.push({ results.push({
rank,
room_id: room.roomId, room_id: room.roomId,
avatar_url: avatarUrl, avatar_url: avatarUrl,
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'), name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
}); });
}); });
this._processResults(results, query);
// Sort by rank ascending (a high rank being less relevant)
const sortedResults = results.sort((a, b) => {
return a.rank - b.rank;
});
this._processResults(sortedResults, query);
this.setState({ this.setState({
busy: false, busy: false,
}); });
@ -574,6 +595,7 @@ module.exports = React.createClass({
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div> <div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
{ error } { error }
{ addressSelector } { addressSelector }
{ this.props.extraNode }
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onButtonClick}> <button className="mx_Dialog_primary" onClick={this.onButtonClick}>

View file

@ -49,9 +49,8 @@ function UserUnknownDeviceList(props) {
const {userId, userDevices} = props; const {userId, userDevices} = props;
const deviceListEntries = Object.keys(userDevices).map((deviceId) => const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
<DeviceListEntry key={deviceId} userId={userId} <DeviceListEntry key={deviceId} userId={userId}
device={userDevices[deviceId]} device={userDevices[deviceId]} />,
/>,
); );
return ( return (
@ -94,52 +93,23 @@ export default React.createClass({
propTypes: { propTypes: {
room: React.PropTypes.object.isRequired, room: React.PropTypes.object.isRequired,
// map from userid -> deviceid -> deviceinfo
devices: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired, onFinished: React.PropTypes.func.isRequired,
}, },
componentWillMount: function() { componentDidMount: function() {
this._unmounted = false; // Given we've now shown the user the unknown device, it is no longer
// unknown to them. Therefore mark it as 'known'.
const roomMembers = this.props.room.getJoinedMembers().map((m) => { Object.keys(this.props.devices).forEach((userId) => {
return m.userId; Object.keys(this.props.devices[userId]).map((deviceId) => {
}); MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
this.setState({
// map from userid -> deviceid -> deviceinfo
devices: null,
});
MatrixClientPeg.get().downloadKeys(roomMembers, false).then((devices) => {
if (this._unmounted) return;
const unknownDevices = {};
// This is all devices in this room, so find the unknown ones.
Object.keys(devices).forEach((userId) => {
Object.keys(devices[userId]).map((deviceId) => {
const device = devices[userId][deviceId];
if (device.isUnverified() && !device.isKnown()) {
if (unknownDevices[userId] === undefined) {
unknownDevices[userId] = {};
}
unknownDevices[userId][deviceId] = device;
}
// Given we've now shown the user the unknown device, it is no longer
// unknown to them. Therefore mark it as 'known'.
if (!device.isKnown()) {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
}
});
});
this.setState({
devices: unknownDevices,
}); });
}); });
},
componentWillUnmount: function() { // XXX: temporary logging to try to diagnose
this._unmounted = true; // https://github.com/vector-im/riot-web/issues/3148
console.log('Opening UnknownDeviceDialog');
}, },
render: function() { render: function() {
@ -186,7 +156,7 @@ export default React.createClass({
{ warning } { warning }
{ _t("Unknown devices") }: { _t("Unknown devices") }:
<UnknownDeviceList devices={this.state.devices} /> <UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar> </GeminiScrollbar>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" autoFocus={true} <button className="mx_Dialog_primary" autoFocus={true}

View file

@ -108,14 +108,20 @@ export default withMatrixClient(React.createClass({
if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m; if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m;
}); });
memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]); memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]);
// Descending sort on isPrivileged = true = 1 to isPrivileged = false = 0
memberList.sort((a, b) => { memberList.sort((a, b) => {
// TODO: should put admins at the top: we don't yet have that info if (a.isPrivileged === b.isPrivileged) {
if (a < b) { const aName = a.displayname || a.userId;
return -1; const bName = b.displayname || b.userId;
} else if (a > b) { if (aName < bName) {
return 1; return -1;
} else if (aName > bName) {
return 1;
} else {
return 0;
}
} else { } else {
return 0; return a.isPrivileged ? -1 : 1;
} }
}); });

View file

@ -63,7 +63,7 @@ export default withMatrixClient(React.createClass({
return ( return (
<EntityTile name={name} avatarJsx={av} onClick={this.onClick} <EntityTile name={name} avatarJsx={av} onClick={this.onClick}
suppressOnHover={true} presenceState="online" suppressOnHover={true} presenceState="online"
powerStatus={this.props.member.isAdmin ? EntityTile.POWER_STATUS_ADMIN : null} powerStatus={this.props.member.isPrivileged ? EntityTile.POWER_STATUS_ADMIN : null}
/> />
); );
}, },

View file

@ -94,7 +94,7 @@ export default React.createClass({
let roomList = this.state.rooms; let roomList = this.state.rooms;
if (query) { if (query) {
roomList = roomList.filter((room) => { roomList = roomList.filter((room) => {
const matchesName = (room.name || "").toLowerCase().include(query); const matchesName = (room.name || "").toLowerCase().includes(query);
const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query); const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
return matchesName || matchesAlias; return matchesName || matchesAlias;
}); });

View file

@ -36,7 +36,7 @@ export function groupMemberFromApiObject(apiObject) {
userId: apiObject.user_id, userId: apiObject.user_id,
displayname: apiObject.displayname, displayname: apiObject.displayname,
avatarUrl: apiObject.avatar_url, avatarUrl: apiObject.avatar_url,
isAdmin: apiObject.is_admin, isPrivileged: apiObject.is_privileged,
}; };
} }

View file

@ -49,7 +49,7 @@
"Name or matrix ID": "Name or matrix ID", "Name or matrix ID": "Name or matrix ID",
"Invite to Community": "Invite to Community", "Invite to Community": "Invite to Community",
"Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?", "Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?",
"Warning: any room you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any room you add to a community will be publicly visible to anyone who knows the community ID", "Show these rooms to non-members on the community page and room list?": "Show these rooms to non-members on the community page and room list?",
"Add rooms to the community": "Add rooms to the community", "Add rooms to the community": "Add rooms to the community",
"Room name or alias": "Room name or alias", "Room name or alias": "Room name or alias",
"Add to community": "Add to community", "Add to community": "Add to community",

View file

@ -69,9 +69,13 @@ class FlairStore extends EventEmitter {
} }
// Bulk lookup ongoing, return promise to resolve/reject // Bulk lookup ongoing, return promise to resolve/reject
if (this._usersPending[userId] || this._usersInFlight[userId]) { if (this._usersPending[userId]) {
return this._usersPending[userId].prom; return this._usersPending[userId].prom;
} }
// User has been moved from pending to in-flight
if (this._usersInFlight[userId]) {
return this._usersInFlight[userId].prom;
}
this._usersPending[userId] = {}; this._usersPending[userId] = {};
this._usersPending[userId].prom = new Promise((resolve, reject) => { this._usersPending[userId].prom = new Promise((resolve, reject) => {

View file

@ -169,6 +169,14 @@ export default class GroupStore extends EventEmitter {
.then(this._fetchMembers.bind(this)); .then(this._fetchMembers.bind(this));
} }
acceptGroupInvite() {
return this._matrixClient.acceptGroupInvite(this.groupId)
// The user might be able to see more rooms now
.then(this._fetchRooms.bind(this))
// The user should now appear as a member
.then(this._fetchMembers.bind(this));
}
addRoomToGroupSummary(roomId, categoryId) { addRoomToGroupSummary(roomId, categoryId) {
return this._matrixClient return this._matrixClient
.addRoomToGroupSummary(this.groupId, roomId, categoryId) .addRoomToGroupSummary(this.groupId, roomId, categoryId)