Merge remote-tracking branch 'matrix-org/develop' into travis/presence
Signed-off-by: Travis Ralston <travpc@gmail.com>
This commit is contained in:
commit
e773585a02
80 changed files with 9256 additions and 2909 deletions
|
@ -110,7 +110,7 @@ module.exports = React.createClass({
|
|||
|
||||
let idx = 0;
|
||||
const initial = name[0];
|
||||
if ((initial === '@' || initial === '#') && name[1]) {
|
||||
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export default React.createClass({
|
|||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
|
|
@ -272,20 +272,27 @@ module.exports = React.createClass({
|
|||
const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
|
||||
const name = nameEvent ? nameEvent.getContent().name : '';
|
||||
const canonicalAlias = room.getCanonicalAlias();
|
||||
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
|
||||
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
|
||||
return a.concat(b);
|
||||
}, []);
|
||||
const topic = topicEvent ? topicEvent.getContent().topic : '';
|
||||
|
||||
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const aliasMatch = (canonicalAlias || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const aliasMatch = aliases.some((alias) =>
|
||||
(alias || '').toLowerCase().includes(lowerCaseQuery),
|
||||
);
|
||||
const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
||||
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
||||
|
||||
results.push({
|
||||
room_id: room.roomId,
|
||||
avatar_url: avatarUrl,
|
||||
name: name || canonicalAlias,
|
||||
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
|
||||
});
|
||||
});
|
||||
this._processResults(results, query);
|
||||
|
@ -489,7 +496,12 @@ module.exports = React.createClass({
|
|||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
for (let i = 0; i < this.state.userList.length; i++) {
|
||||
query.push(
|
||||
<AddressTile key={i} address={this.state.userList[i]} canDismiss={true} onDismissed={this.onDismissed(i)} />,
|
||||
<AddressTile
|
||||
key={i}
|
||||
address={this.state.userList[i]}
|
||||
canDismiss={true}
|
||||
onDismissed={this.onDismissed(i)}
|
||||
showAddress={this.props.pickerType === 'user'} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -539,6 +551,7 @@ module.exports = React.createClass({
|
|||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={this.state.queryList}
|
||||
showAddress={this.props.pickerType === 'user'}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
/>
|
||||
|
|
|
@ -21,10 +21,6 @@ import dis from '../../../dispatcher';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
// We match fairly liberally and leave it up to the server to reject if
|
||||
// there are invalid characters etc.
|
||||
const GROUP_REGEX = /^\+(.*?):(.*)$/;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'CreateGroupDialog',
|
||||
propTypes: {
|
||||
|
@ -58,22 +54,9 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
_checkGroupId: function(e) {
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
let error = null;
|
||||
if (parsedGroupId === null) {
|
||||
error = _t(
|
||||
"Group IDs must be of the form +localpart:%(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
} else {
|
||||
const domain = parsedGroupId[1];
|
||||
if (domain !== MatrixClientPeg.get().getDomain()) {
|
||||
error = _t(
|
||||
"It is currently only possible to create groups on your own home server: "+
|
||||
"use a group ID ending with %(domain)s",
|
||||
{domain: MatrixClientPeg.get().getDomain()},
|
||||
);
|
||||
}
|
||||
if (!/^[a-zA-Z0-9]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain alphanumeric characters");
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
|
@ -86,14 +69,13 @@ export default React.createClass({
|
|||
|
||||
if (this._checkGroupId()) return;
|
||||
|
||||
const parsedGroupId = this._parseGroupId(this.state.groupId);
|
||||
const profile = {};
|
||||
if (this.state.groupName !== '') {
|
||||
profile.name = this.state.groupName;
|
||||
}
|
||||
this.setState({creating: true});
|
||||
MatrixClientPeg.get().createGroup({
|
||||
localpart: parsedGroupId[0],
|
||||
localpart: this.state.groupId,
|
||||
profile: profile,
|
||||
}).then((result) => {
|
||||
dis.dispatch({
|
||||
|
@ -112,22 +94,6 @@ export default React.createClass({
|
|||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse a string that may be a group ID
|
||||
* If the string is a valid group ID, return a list of [localpart, domain],
|
||||
* otherwise return null.
|
||||
*
|
||||
* @param {string} groupId The ID of the group
|
||||
* @return {string[]} array of localpart, domain
|
||||
*/
|
||||
_parseGroupId: function(groupId) {
|
||||
const matches = GROUP_REGEX.exec(this.state.groupId);
|
||||
if (!matches || matches.length < 3) {
|
||||
return null;
|
||||
}
|
||||
return [matches[1], matches[2]];
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
|
@ -142,7 +108,7 @@ export default React.createClass({
|
|||
// rather than displaying what the server gives us, but synapse doesn't give
|
||||
// any yet.
|
||||
createErrorNode = <div className="error">
|
||||
<div>{ _t('Room creation failed') }</div>
|
||||
<div>{ _t('Something went wrong whilst creating your community') }</div>
|
||||
<div>{ this.state.createError.message }</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -150,13 +116,13 @@ export default React.createClass({
|
|||
return (
|
||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this._onFormSubmit}
|
||||
title={_t('Create Group')}
|
||||
title={_t('Create Community')}
|
||||
>
|
||||
<form onSubmit={this._onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupname">{ _t('Group Name') }</label>
|
||||
<label htmlFor="groupname">{ _t('Community Name') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||
|
@ -169,16 +135,18 @@ export default React.createClass({
|
|||
</div>
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupid">{ _t('Group ID') }</label>
|
||||
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<span>+</span>
|
||||
<input id="groupid" className="mx_CreateGroupDialog_input"
|
||||
size="64"
|
||||
placeholder={_t('+example:%(domain)s', {domain: MatrixClientPeg.get().getDomain()})}
|
||||
size="32"
|
||||
placeholder={_t('example')}
|
||||
onChange={this._onGroupIdChange}
|
||||
onBlur={this._onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
<span>:{ MatrixClientPeg.get().getDomain() }</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error">
|
||||
|
|
|
@ -30,6 +30,8 @@ export default React.createClass({
|
|||
|
||||
// List of the addresses to display
|
||||
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
|
||||
// Whether to show the address on the address tiles
|
||||
showAddress: React.PropTypes.bool,
|
||||
truncateAt: React.PropTypes.number.isRequired,
|
||||
selected: React.PropTypes.number,
|
||||
|
||||
|
@ -142,7 +144,13 @@ export default React.createClass({
|
|||
key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address}
|
||||
ref={(ref) => { this.addressListElement = ref; }}
|
||||
>
|
||||
<AddressTile address={this.props.addressList[i]} justified={true} networkName="vector" networkUrl="img/search-icon-vector.svg" />
|
||||
<AddressTile
|
||||
address={this.props.addressList[i]}
|
||||
showAddress={this.props.showAddress}
|
||||
justified={true}
|
||||
networkName="vector"
|
||||
networkUrl="img/search-icon-vector.svg"
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -87,7 +87,10 @@ export default React.createClass({
|
|||
info = (
|
||||
<div className="mx_AddressTile_mx">
|
||||
<div className={nameClasses}>{ name }</div>
|
||||
<div className={idClasses}>{ address.address }</div>
|
||||
{ this.props.showAddress ?
|
||||
<div className={idClasses}>{ address.address }</div> :
|
||||
<div />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
} else if (isMatrixAddress) {
|
||||
|
|
|
@ -172,18 +172,29 @@ export default React.createClass({
|
|||
*/
|
||||
_onDeleteClick: function() {
|
||||
if (this._canUserModify()) {
|
||||
console.log("Delete widget %s", this.props.id);
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).then(() => {
|
||||
console.log('Deleted widget');
|
||||
}, (e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
this.setState({deleting: false});
|
||||
// Show delete confirmation dialog
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||
title: _t("Delete Widget"),
|
||||
description: _t(
|
||||
"Deleting a widget removes it for all users in this room." +
|
||||
" Are you sure you want to delete this widget?"),
|
||||
button: _t("Delete widget"),
|
||||
onFinished: (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.log("Revoke widget permissions - %s", this.props.id);
|
||||
|
@ -305,7 +316,7 @@ export default React.createClass({
|
|||
let deleteIcon = 'img/cancel.svg';
|
||||
let deleteClasses = 'mx_filterFlipColor mx_AppTileMenuBarWidget';
|
||||
if(this._canUserModify()) {
|
||||
deleteIcon = 'img/cancel-red.svg';
|
||||
deleteIcon = 'img/icon-delete-pink.svg';
|
||||
deleteClasses += ' mx_AppTileMenuBarWidgetDelete';
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,9 @@ module.exports = React.createClass({
|
|||
onNewItemChanged: PropTypes.func,
|
||||
onItemAdded: PropTypes.func,
|
||||
onItemEdited: PropTypes.func,
|
||||
onItemRemoved: PropTypes. func,
|
||||
onItemRemoved: PropTypes.func,
|
||||
|
||||
canEdit: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -136,14 +138,16 @@ module.exports = React.createClass({
|
|||
{ label }
|
||||
</div>
|
||||
{ editableItems }
|
||||
<EditableItem
|
||||
key={-1}
|
||||
initialValue={this.props.newItem}
|
||||
onAdd={this.onItemAdded}
|
||||
onChange={this.onNewItemChanged}
|
||||
addOnChange={true}
|
||||
placeholder={this.props.placeholder}
|
||||
/>
|
||||
{ this.props.canEdit ?
|
||||
<EditableItem
|
||||
key={-1}
|
||||
initialValue={this.props.newItem}
|
||||
onAdd={this.onItemAdded}
|
||||
onChange={this.onNewItemChanged}
|
||||
addOnChange={true}
|
||||
placeholder={this.props.placeholder}
|
||||
/> : <div />
|
||||
}
|
||||
</div>);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -20,123 +20,9 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {MatrixClient} from 'matrix-js-sdk';
|
||||
import UserSettingsStore from '../../../UserSettingsStore';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import dis from '../../../dispatcher';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const BULK_REQUEST_DEBOUNCE_MS = 200;
|
||||
|
||||
// Does the server support groups? Assume yes until we receive M_UNRECOGNIZED.
|
||||
// If true, flair can function and we should keep sending requests for groups and avatars.
|
||||
let groupSupport = true;
|
||||
|
||||
const USER_GROUPS_CACHE_BUST_MS = 1800000; // 30 mins
|
||||
const GROUP_PROFILES_CACHE_BUST_MS = 1800000; // 30 mins
|
||||
|
||||
// TODO: Cache-busting based on time. (The server won't inform us of membership changes.)
|
||||
// This applies to userGroups and groupProfiles. We can provide a slightly better UX by
|
||||
// cache-busting when the current user joins/leaves a group.
|
||||
const userGroups = {
|
||||
// $userId: ['+group1:domain', '+group2:domain', ...]
|
||||
};
|
||||
|
||||
const groupProfiles = {
|
||||
// $groupId: {
|
||||
// avatar_url: 'mxc://...'
|
||||
// }
|
||||
};
|
||||
|
||||
// Represents all unsettled promises to retrieve the groups for each userId. When a promise
|
||||
// is settled, it is deleted from this object.
|
||||
const usersPending = {
|
||||
// $userId: {
|
||||
// prom: Promise
|
||||
// resolve: () => {}
|
||||
// reject: () => {}
|
||||
// }
|
||||
};
|
||||
|
||||
let debounceTimeoutID;
|
||||
function getPublicisedGroupsCached(matrixClient, userId) {
|
||||
if (userGroups[userId]) {
|
||||
return Promise.resolve(userGroups[userId]);
|
||||
}
|
||||
|
||||
// Bulk lookup ongoing, return promise to resolve/reject
|
||||
if (usersPending[userId]) {
|
||||
return usersPending[userId].prom;
|
||||
}
|
||||
|
||||
usersPending[userId] = {};
|
||||
usersPending[userId].prom = new Promise((resolve, reject) => {
|
||||
usersPending[userId].resolve = resolve;
|
||||
usersPending[userId].reject = reject;
|
||||
}).then((groups) => {
|
||||
userGroups[userId] = groups;
|
||||
setTimeout(() => {
|
||||
delete userGroups[userId];
|
||||
}, USER_GROUPS_CACHE_BUST_MS);
|
||||
return userGroups[userId];
|
||||
}).catch((err) => {
|
||||
throw err;
|
||||
}).finally(() => {
|
||||
delete usersPending[userId];
|
||||
});
|
||||
|
||||
// This debounce will allow consecutive requests for the public groups of users that
|
||||
// are sent in intervals of < BULK_REQUEST_DEBOUNCE_MS to be batched and only requested
|
||||
// when no more requests are received within the next BULK_REQUEST_DEBOUNCE_MS. The naive
|
||||
// implementation would do a request that only requested the groups for `userId`, leading
|
||||
// to a worst and best case of 1 user per request. This implementation's worst is still
|
||||
// 1 user per request but only if the requests are > BULK_REQUEST_DEBOUNCE_MS apart and the
|
||||
// best case is N users per request.
|
||||
//
|
||||
// This is to reduce the number of requests made whilst trading off latency when viewing
|
||||
// a Flair component.
|
||||
if (debounceTimeoutID) clearTimeout(debounceTimeoutID);
|
||||
debounceTimeoutID = setTimeout(() => {
|
||||
batchedGetPublicGroups(matrixClient);
|
||||
}, BULK_REQUEST_DEBOUNCE_MS);
|
||||
|
||||
return usersPending[userId].prom;
|
||||
}
|
||||
|
||||
async function batchedGetPublicGroups(matrixClient) {
|
||||
// Take the userIds from the keys of usersPending
|
||||
const usersInFlight = Object.keys(usersPending);
|
||||
let resp = {
|
||||
users: [],
|
||||
};
|
||||
try {
|
||||
resp = await matrixClient.getPublicisedGroups(usersInFlight);
|
||||
} catch (err) {
|
||||
// Propagate the same error to all usersInFlight
|
||||
usersInFlight.forEach((userId) => {
|
||||
usersPending[userId].reject(err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const updatedUserGroups = resp.users;
|
||||
usersInFlight.forEach((userId) => {
|
||||
usersPending[userId].resolve(updatedUserGroups[userId] || []);
|
||||
});
|
||||
}
|
||||
|
||||
async function getGroupProfileCached(matrixClient, groupId) {
|
||||
if (groupProfiles[groupId]) {
|
||||
return groupProfiles[groupId];
|
||||
}
|
||||
|
||||
const profile = await matrixClient.getGroupProfile(groupId);
|
||||
groupProfiles[groupId] = {
|
||||
groupId,
|
||||
avatarUrl: profile.avatar_url,
|
||||
};
|
||||
setTimeout(() => {
|
||||
delete groupProfiles[groupId];
|
||||
}, GROUP_PROFILES_CACHE_BUST_MS);
|
||||
|
||||
return groupProfiles[groupId];
|
||||
}
|
||||
|
||||
class FlairAvatar extends React.Component {
|
||||
constructor() {
|
||||
|
@ -156,11 +42,11 @@ class FlairAvatar extends React.Component {
|
|||
|
||||
render() {
|
||||
const httpUrl = this.context.matrixClient.mxcUrlToHttp(
|
||||
this.props.groupProfile.avatarUrl, 14, 14, 'scale', false);
|
||||
this.props.groupProfile.avatarUrl, 16, 16, 'scale', false);
|
||||
return <img
|
||||
src={httpUrl}
|
||||
width="14px"
|
||||
height="14px"
|
||||
width="16"
|
||||
height="16"
|
||||
onClick={this.onClick}
|
||||
title={this.props.groupProfile.groupId} />;
|
||||
}
|
||||
|
@ -193,14 +79,14 @@ export default class Flair extends React.Component {
|
|||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
if (UserSettingsStore.isFeatureEnabled('feature_groups') && groupSupport) {
|
||||
if (UserSettingsStore.isFeatureEnabled('feature_groups') && FlairStore.groupSupport()) {
|
||||
this._generateAvatars();
|
||||
}
|
||||
this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
onRoomStateEvents(event) {
|
||||
if (event.getType() === 'm.room.related_groups' && groupSupport) {
|
||||
if (event.getType() === 'm.room.related_groups' && FlairStore.groupSupport()) {
|
||||
this._generateAvatars();
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +96,7 @@ export default class Flair extends React.Component {
|
|||
for (const groupId of groups) {
|
||||
let groupProfile = null;
|
||||
try {
|
||||
groupProfile = await getGroupProfileCached(this.context.matrixClient, groupId);
|
||||
groupProfile = await FlairStore.getGroupProfileCached(this.context.matrixClient, groupId);
|
||||
} catch (err) {
|
||||
console.error('Could not get profile for group', groupId, err);
|
||||
}
|
||||
|
@ -220,19 +106,7 @@ export default class Flair extends React.Component {
|
|||
}
|
||||
|
||||
async _generateAvatars() {
|
||||
let groups;
|
||||
try {
|
||||
groups = await getPublicisedGroupsCached(this.context.matrixClient, this.props.userId);
|
||||
} catch (err) {
|
||||
// Indicate whether the homeserver supports groups
|
||||
if (err.errcode === 'M_UNRECOGNIZED') {
|
||||
console.warn('Cannot display flair, server does not support groups');
|
||||
groupSupport = false;
|
||||
// Return silently to avoid spamming for non-supporting servers
|
||||
return;
|
||||
}
|
||||
console.error('Could not get groups for user', this.props.userId, err);
|
||||
}
|
||||
let groups = await FlairStore.getPublicisedGroupsCached(this.context.matrixClient, this.props.userId);
|
||||
if (this.props.roomId && this.props.showRelated) {
|
||||
const relatedGroupsEvent = this.context.matrixClient
|
||||
.getRoom(this.props.roomId)
|
||||
|
@ -253,7 +127,7 @@ export default class Flair extends React.Component {
|
|||
}
|
||||
const profiles = await this._getGroupProfiles(groups);
|
||||
if (!this.unmounted) {
|
||||
this.setState({profiles});
|
||||
this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ const GroupsButton = function(props) {
|
|||
const ActionButton = sdk.getComponent('elements.ActionButton');
|
||||
return (
|
||||
<ActionButton action="view_my_groups"
|
||||
label={_t("Groups")}
|
||||
label={_t("Communities")}
|
||||
iconPath="img/icons-groups.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
|
|
|
@ -38,19 +38,17 @@ export default React.createClass({
|
|||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
const av = (
|
||||
<BaseAvatar name={this.props.group.name} width={24} height={24}
|
||||
url={this.props.group.avatarUrl}
|
||||
/>
|
||||
);
|
||||
const groupName = this.props.group.name || this.props.group.groupId;
|
||||
|
||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={this.props.group.avatarUrl} />;
|
||||
|
||||
const label = <EmojiText
|
||||
element="div"
|
||||
title={this.props.group.name}
|
||||
title={groupName}
|
||||
className="mx_GroupInviteTile_name"
|
||||
dir="auto"
|
||||
>
|
||||
{ this.props.group.name }
|
||||
{ groupName }
|
||||
</EmojiText>;
|
||||
|
||||
const badge = <div className="mx_GroupInviteTile_badge">!</div>;
|
||||
|
|
|
@ -68,7 +68,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
Modal.createDialog(ConfirmUserActionDialog, {
|
||||
groupMember: this.props.groupMember,
|
||||
action: _t('Remove from group'),
|
||||
action: _t('Remove from community'),
|
||||
danger: true,
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
|
@ -86,7 +86,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: _t('Failed to remove user from group'),
|
||||
description: _t('Failed to remove user from community'),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({removingUser: false});
|
||||
|
@ -128,7 +128,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
kickButton = (
|
||||
<AccessibleButton className="mx_MemberInfo_field"
|
||||
onClick={this._onKick}>
|
||||
{ _t('Remove from group') }
|
||||
{ _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import { groupMemberFromApiObject } from '../../../groups';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import PropTypes from 'prop-types';
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
|
@ -27,36 +27,42 @@ const INITIAL_LOAD_NUM_MEMBERS = 30;
|
|||
export default withMatrixClient(React.createClass({
|
||||
displayName: 'GroupMemberList',
|
||||
|
||||
propTypes: {
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
fetching: false,
|
||||
members: null,
|
||||
invitedMembers: null,
|
||||
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._fetchMembers();
|
||||
this._initGroupStore(this.props.groupId);
|
||||
},
|
||||
|
||||
_initGroupStore: function(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
|
||||
this._groupStore.on('update', () => {
|
||||
this._fetchMembers();
|
||||
});
|
||||
this._groupStore.on('error', (err) => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
|
||||
_fetchMembers: function() {
|
||||
this.setState({fetching: true});
|
||||
this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => {
|
||||
this.setState({
|
||||
members: result.chunk.map((apiMember) => {
|
||||
return groupMemberFromApiObject(apiMember);
|
||||
}),
|
||||
fetching: false,
|
||||
});
|
||||
}).catch((e) => {
|
||||
this.setState({fetching: false});
|
||||
console.error("Failed to get group member list: " + e);
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
members: this._groupStore.getGroupMembers(),
|
||||
invitedMembers: this._groupStore.getGroupInvitedMembers(),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -83,11 +89,10 @@ export default withMatrixClient(React.createClass({
|
|||
this.setState({ searchQuery: ev.target.value });
|
||||
},
|
||||
|
||||
makeGroupMemberTiles: function(query) {
|
||||
makeGroupMemberTiles: function(query, memberList) {
|
||||
const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile");
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
query = (query || "").toLowerCase();
|
||||
|
||||
let memberList = this.state.members;
|
||||
if (query) {
|
||||
memberList = memberList.filter((m) => {
|
||||
const matchesName = m.displayname.toLowerCase().indexOf(query) !== -1;
|
||||
|
@ -101,12 +106,11 @@ export default withMatrixClient(React.createClass({
|
|||
});
|
||||
}
|
||||
|
||||
memberList = memberList.map((m) => {
|
||||
return (
|
||||
<GroupMemberTile key={m.userId} groupId={this.props.groupId} member={m} />
|
||||
);
|
||||
const uniqueMembers = {};
|
||||
memberList.forEach((m) => {
|
||||
if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m;
|
||||
});
|
||||
|
||||
memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]);
|
||||
memberList.sort((a, b) => {
|
||||
// TODO: should put admins at the top: we don't yet have that info
|
||||
if (a < b) {
|
||||
|
@ -118,36 +122,51 @@ export default withMatrixClient(React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
return memberList;
|
||||
const memberTiles = memberList.map((m) => {
|
||||
return (
|
||||
<GroupMemberTile key={m.userId} groupId={this.props.groupId} member={m} />
|
||||
);
|
||||
});
|
||||
|
||||
return <TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}
|
||||
>
|
||||
{ memberTiles }
|
||||
</TruncatedList>;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.fetching) {
|
||||
if (this.state.fetching || this.state.fetchingInvitedMembers) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (<div className="mx_MemberList">
|
||||
<Spinner />
|
||||
</div>);
|
||||
} else if (this.state.members === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputBox = (
|
||||
<form autoComplete="off">
|
||||
<input className="mx_GroupMemberList_query" id="mx_GroupMemberList_query" type="text"
|
||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||
placeholder={_t('Filter group members')} />
|
||||
placeholder={_t('Filter community members')} />
|
||||
</form>
|
||||
);
|
||||
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
const joined = this.state.members ? <div className="mx_MemberList_joined">
|
||||
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.members) }
|
||||
</div> : <div />;
|
||||
|
||||
const invited = (this.state.invitedMembers && this.state.invitedMembers.length > 0) ?
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>{ _t("Invited") }</h2>
|
||||
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.invitedMembers) }
|
||||
</div> : <div />;
|
||||
|
||||
return (
|
||||
<div className="mx_MemberList">
|
||||
{ inputBox }
|
||||
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
||||
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{ this.makeGroupMemberTiles(this.state.searchQuery) }
|
||||
</TruncatedList>
|
||||
<GeminiScrollbar autoshow={true} className="mx_MemberList_outerWrapper">
|
||||
{ joined }
|
||||
{ invited }
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import { groupRoomFromApiObject } from '../../../groups';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -63,9 +62,7 @@ export default React.createClass({
|
|||
_fetchRooms: function() {
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
rooms: this._groupStore.getGroupRooms().map((apiRoom) => {
|
||||
return groupRoomFromApiObject(apiRoom);
|
||||
}),
|
||||
rooms: this._groupStore.getGroupRooms(),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -126,7 +123,7 @@ export default React.createClass({
|
|||
<form autoComplete="off">
|
||||
<input className="mx_GroupRoomList_query" id="mx_GroupRoomList_query" type="text"
|
||||
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
|
||||
placeholder={_t('Filter group rooms')} />
|
||||
placeholder={_t('Filter community rooms')} />
|
||||
</form>
|
||||
);
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ const GroupRoomTile = React.createClass({
|
|||
console.error(`Error whilst removing ${roomId} from ${groupId}`, err);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
||||
title: _t("Failed to remove room from group"),
|
||||
title: _t("Failed to remove room from community"),
|
||||
description: _t("Failed to remove '%(roomName)s' from %(groupId)s", {groupId, roomName}),
|
||||
});
|
||||
});
|
||||
|
@ -87,7 +87,7 @@ const GroupRoomTile = React.createClass({
|
|||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, {
|
||||
title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", {roomName, groupId}),
|
||||
description: _t("Removing a room from the group will also remove it from the group page."),
|
||||
description: _t("Removing a room from the community will also remove it from the community page."),
|
||||
button: _t("Remove"),
|
||||
onFinished: (success) => {
|
||||
if (success) {
|
||||
|
|
|
@ -25,7 +25,10 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
let tooltip = _t("Removed or unknown message type");
|
||||
if (this.props.mxEvent.isRedacted()) {
|
||||
tooltip = _t("Message removed by %(userId)s", {userId: this.props.mxEvent.getSender()});
|
||||
const redactedBecauseUserId = this.props.mxEvent.getUnsigned().redacted_because.sender;
|
||||
tooltip = redactedBecauseUserId ?
|
||||
_t("Message removed by %(userId)s", { userId: redactedBecauseUserId }) :
|
||||
_t("Message removed");
|
||||
}
|
||||
|
||||
const text = this.props.mxEvent.getContent().body;
|
||||
|
|
|
@ -262,6 +262,7 @@ module.exports = React.createClass({
|
|||
items={this.state.domainToAliases[localDomain] || []}
|
||||
newItem={this.state.newAlias}
|
||||
onNewItemChanged={this.onNewAliasChanged}
|
||||
canEdit={this.props.canSetAliases}
|
||||
onItemAdded={this.onLocalAliasAdded}
|
||||
onItemEdited={this.onLocalAliasChanged}
|
||||
onItemRemoved={this.onLocalAliasDeleted}
|
||||
|
|
|
@ -27,7 +27,7 @@ module.exports = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
roomId: React.PropTypes.string.isRequired,
|
||||
canSetRelatedRooms: React.PropTypes.bool.isRequired,
|
||||
canSetRelatedGroups: React.PropTypes.bool.isRequired,
|
||||
relatedGroupsEvent: React.PropTypes.instanceOf(MatrixEvent),
|
||||
},
|
||||
|
||||
|
@ -37,7 +37,7 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
canSetRelatedRooms: false,
|
||||
canSetRelatedGroups: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -63,9 +63,9 @@ module.exports = React.createClass({
|
|||
validateGroupId: function(groupId) {
|
||||
if (!GROUP_ID_REGEX.test(groupId)) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid related group ID', '', ErrorDialog, {
|
||||
title: _t('Invalid group ID'),
|
||||
description: _t('\'%(groupId)s\' is not a valid group ID', { groupId }),
|
||||
Modal.createTrackedDialog('Invalid related community ID', '', ErrorDialog, {
|
||||
title: _t('Invalid community ID'),
|
||||
description: _t('\'%(groupId)s\' is not a valid community ID', { groupId }),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
@ -105,19 +105,20 @@ module.exports = React.createClass({
|
|||
const localDomain = this.context.matrixClient.getDomain();
|
||||
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
||||
return (<div>
|
||||
<h3>{ _t('Related Groups') }</h3>
|
||||
<h3>{ _t('Related Communities') }</h3>
|
||||
<EditableItemList
|
||||
items={this.state.newGroupsList}
|
||||
className={"mx_RelatedGroupSettings"}
|
||||
newItem={this.state.newGroupId}
|
||||
canEdit={this.props.canSetRelatedGroups}
|
||||
onNewItemChanged={this.onNewGroupChanged}
|
||||
onItemAdded={this.onGroupAdded}
|
||||
onItemEdited={this.onGroupEdited}
|
||||
onItemRemoved={this.onGroupDeleted}
|
||||
itemsLabel={_t('Related groups for this room:')}
|
||||
noItemsLabel={_t('This room has no related groups')}
|
||||
itemsLabel={_t('Related communities for this room:')}
|
||||
noItemsLabel={_t('This room has no related communities')}
|
||||
placeholder={_t(
|
||||
'New group ID (e.g. +foo:%(localDomain)s)', {localDomain},
|
||||
'New community ID (e.g. +foo:%(localDomain)s)', {localDomain},
|
||||
)}
|
||||
/>
|
||||
</div>);
|
||||
|
|
|
@ -116,7 +116,9 @@ module.exports = React.createClass({
|
|||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12" />
|
||||
<EmojiText element="div" className="mx_EntityTile_name_hover" dir="auto">{ name }</EmojiText>
|
||||
<EmojiText element="div" className="mx_EntityTile_name mx_EntityTile_name_hover" dir="auto">
|
||||
{ name }
|
||||
</EmojiText>
|
||||
<PresenceLabel activeAgo={activeAgo}
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
presenceState={this.props.presenceState} />
|
||||
|
|
|
@ -30,10 +30,9 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillMount: function() {
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
leftOpacity: 1.0,
|
||||
rightOpacity: 0.3,
|
||||
middleOpacity: 0.5,
|
||||
action: 'panel_disable',
|
||||
rightDisabled: true,
|
||||
middleDisabled: true,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -43,9 +42,9 @@ module.exports = React.createClass({
|
|||
|
||||
componentWillUnmount: function() {
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
sideOpacity: 1.0,
|
||||
middleOpacity: 1.0,
|
||||
action: 'panel_disable',
|
||||
sideDisabled: false,
|
||||
middleDisabled: false,
|
||||
});
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
},
|
||||
|
|
|
@ -91,7 +91,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
if (this.props.member.userId != newProps.member.userId) {
|
||||
if (this.props.member.userId !== newProps.member.userId) {
|
||||
this._updateStateForNewMember(newProps.member);
|
||||
}
|
||||
},
|
||||
|
@ -122,12 +122,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
_disambiguateDevices: function(devices) {
|
||||
const names = Object.create(null);
|
||||
for (let i = 0; i < devices.length; i++) {
|
||||
var name = devices[i].getDisplayName();
|
||||
const name = devices[i].getDisplayName();
|
||||
const indexList = names[name] || [];
|
||||
indexList.push(i);
|
||||
names[name] = indexList;
|
||||
}
|
||||
for (name in names) {
|
||||
for (const name in names) {
|
||||
if (names[name].length > 1) {
|
||||
names[name].forEach((j)=>{
|
||||
devices[j].ambiguous = true;
|
||||
|
@ -141,7 +141,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
if (userId == this.props.member.userId) {
|
||||
if (userId === this.props.member.userId) {
|
||||
// no need to re-download the whole thing; just update our copy of
|
||||
// the list.
|
||||
|
||||
|
@ -187,7 +187,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
onAccountData: function(ev) {
|
||||
if (ev.getType() == 'm.direct') {
|
||||
if (ev.getType() === 'm.direct') {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
@ -242,7 +242,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
ignoredUsers.push(this.props.member.userId);
|
||||
}
|
||||
|
||||
this.props.matrixClient.setIgnoredUsers(ignoredUsers).then(() => this.setState({isIgnoring: !this.state.isIgnoring}));
|
||||
this.props.matrixClient.setIgnoredUsers(ignoredUsers).then(() => {
|
||||
return this.setState({isIgnoring: !this.state.isIgnoring});
|
||||
});
|
||||
},
|
||||
|
||||
onKick: function() {
|
||||
|
@ -252,7 +254,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, {
|
||||
member: this.props.member,
|
||||
action: kickLabel,
|
||||
askReason: membership == "join",
|
||||
askReason: membership === "join",
|
||||
danger: true,
|
||||
onFinished: (proceed, reason) => {
|
||||
if (!proceed) return;
|
||||
|
@ -284,15 +286,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog");
|
||||
Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, {
|
||||
member: this.props.member,
|
||||
action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"),
|
||||
askReason: this.props.member.membership != 'ban',
|
||||
danger: this.props.member.membership != 'ban',
|
||||
action: this.props.member.membership === 'ban' ? _t("Unban") : _t("Ban"),
|
||||
askReason: this.props.member.membership !== 'ban',
|
||||
danger: this.props.member.membership !== 'ban',
|
||||
onFinished: (proceed, reason) => {
|
||||
if (!proceed) return;
|
||||
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
let promise;
|
||||
if (this.props.member.membership == 'ban') {
|
||||
if (this.props.member.membership === 'ban') {
|
||||
promise = this.props.matrixClient.unban(
|
||||
this.props.member.roomId, this.props.member.userId,
|
||||
);
|
||||
|
@ -327,15 +329,11 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const powerLevelEvent = room.currentState.getStateEvents(
|
||||
"m.room.power_levels", "",
|
||||
);
|
||||
if (!powerLevelEvent) {
|
||||
return;
|
||||
}
|
||||
if (!room) return;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
const isMuted = this.state.muted;
|
||||
const powerLevels = powerLevelEvent.getContent();
|
||||
const levelToSend = (
|
||||
|
@ -350,7 +348,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
level = parseInt(level);
|
||||
|
||||
if (level !== NaN) {
|
||||
if (!isNaN(level)) {
|
||||
this.setState({ updating: this.state.updating + 1 });
|
||||
this.props.matrixClient.setPowerLevel(roomId, target, level, powerLevelEvent).then(
|
||||
function() {
|
||||
|
@ -375,19 +373,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const powerLevelEvent = room.currentState.getStateEvents(
|
||||
"m.room.power_levels", "",
|
||||
);
|
||||
if (!powerLevelEvent) {
|
||||
return;
|
||||
}
|
||||
if (!room) return;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
const me = room.getMember(this.props.matrixClient.credentials.userId);
|
||||
if (!me) {
|
||||
return;
|
||||
}
|
||||
if (!me) return;
|
||||
|
||||
const defaultLevel = powerLevelEvent.getContent().users_default;
|
||||
let modLevel = me.powerLevel - 1;
|
||||
if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults
|
||||
|
@ -400,7 +393,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// get out of sync if we force setState here!
|
||||
console.log("Mod toggle success");
|
||||
}, function(err) {
|
||||
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
} else {
|
||||
console.error("Toggle moderator error:" + err);
|
||||
|
@ -436,7 +429,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
onPowerChange: function(powerLevel) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
|
@ -497,19 +489,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
modifyLevel: false,
|
||||
};
|
||||
const room = this.props.matrixClient.getRoom(member.roomId);
|
||||
if (!room) {
|
||||
return defaultPerms;
|
||||
}
|
||||
const powerLevels = room.currentState.getStateEvents(
|
||||
"m.room.power_levels", "",
|
||||
);
|
||||
if (!powerLevels) {
|
||||
return defaultPerms;
|
||||
}
|
||||
if (!room) return defaultPerms;
|
||||
|
||||
const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevels) return defaultPerms;
|
||||
|
||||
const me = room.getMember(this.props.matrixClient.credentials.userId);
|
||||
if (!me) {
|
||||
return defaultPerms;
|
||||
}
|
||||
if (!me) return defaultPerms;
|
||||
|
||||
const them = member;
|
||||
return {
|
||||
can: this._calculateCanPermissions(
|
||||
|
@ -545,14 +532,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
can.ban = me.powerLevel >= powerLevels.ban;
|
||||
can.mute = me.powerLevel >= editPowerLevel;
|
||||
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
|
||||
can.modifyLevel = me.powerLevel > them.powerLevel;
|
||||
can.modifyLevel = me.powerLevel > them.powerLevel && me.powerLevel >= editPowerLevel;
|
||||
return can;
|
||||
},
|
||||
|
||||
_isMuted: function(member, powerLevelContent) {
|
||||
if (!powerLevelContent || !member) {
|
||||
return false;
|
||||
}
|
||||
if (!powerLevelContent || !member) return false;
|
||||
|
||||
const levelToSend = (
|
||||
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
|
||||
powerLevelContent.events_default
|
||||
|
@ -568,14 +554,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
onMemberAvatarClick: function() {
|
||||
const avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url;
|
||||
const member = this.props.member;
|
||||
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
|
||||
if(!avatarUrl) return;
|
||||
|
||||
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
|
||||
const ImageView = sdk.getComponent("elements.ImageView");
|
||||
const params = {
|
||||
src: httpUrl,
|
||||
name: this.props.member.name,
|
||||
name: member.name,
|
||||
};
|
||||
|
||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||
|
@ -589,9 +576,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
_renderDevices: function() {
|
||||
if (!this._enableDevices) {
|
||||
return null;
|
||||
}
|
||||
if (!this._enableDevices) return null;
|
||||
|
||||
const devices = this.state.devices;
|
||||
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
|
||||
|
@ -675,7 +660,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
let startChat, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||
let startChat;
|
||||
let kickButton;
|
||||
let banButton;
|
||||
let muteButton;
|
||||
let giveModButton;
|
||||
let spinner;
|
||||
|
||||
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
|
||||
const dmRoomMap = new DMRoomMap(this.props.matrixClient);
|
||||
const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId);
|
||||
|
@ -689,7 +680,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const me = room.getMember(this.props.matrixClient.credentials.userId);
|
||||
const highlight = (
|
||||
room.getUnreadNotificationCount('highlight') > 0 ||
|
||||
me.membership == "invite"
|
||||
me.membership === "invite"
|
||||
);
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
|
@ -697,7 +688,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership == "invite"}
|
||||
isInvite={me.membership === "invite"}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
|
@ -742,7 +733,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
if (this.state.can.ban) {
|
||||
let label = _t("Ban");
|
||||
if (this.props.member.membership == 'ban') {
|
||||
if (this.props.member.membership === 'ban') {
|
||||
label = _t("Unban");
|
||||
}
|
||||
banButton = (
|
||||
|
@ -788,11 +779,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
const memberName = this.props.member.name;
|
||||
|
||||
let presenceState;
|
||||
let presenceLastActiveAgo;
|
||||
let presenceCurrentlyActive;
|
||||
|
||||
if (this.props.member.user) {
|
||||
var presenceState = this.props.member.user.presence;
|
||||
var presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
|
||||
const presenceLastTs = this.props.member.user.lastPresenceTs;
|
||||
var presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
||||
presenceState = this.props.member.user.presence;
|
||||
presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
|
||||
presenceCurrentlyActive = this.props.member.user.currentlyActive;
|
||||
}
|
||||
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
|
@ -814,7 +808,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ this.props.member.userId }
|
||||
</div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ _t("Level:") } <b><PowerSelector controlled={true} value={parseInt(this.props.member.powerLevel)} disabled={!this.state.can.modifyLevel} onChange={this.onPowerChange} /></b>
|
||||
{ _t("Level:") } <b>
|
||||
<PowerSelector controlled={true} value={parseInt(this.props.member.powerLevel)} disabled={!this.state.can.modifyLevel} onChange={this.onPowerChange} />
|
||||
</b>
|
||||
</div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
<PresenceLabel activeAgo={presenceLastActiveAgo}
|
||||
|
|
|
@ -371,7 +371,7 @@ export default class MessageComposer extends React.Component {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer mx_fadable" style={{ opacity: this.props.opacity }}>
|
||||
<div className="mx_MessageComposer">
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ controls }
|
||||
|
@ -410,9 +410,6 @@ MessageComposer.propTypes = {
|
|||
// callback when a file to upload is chosen
|
||||
uploadFile: React.PropTypes.func.isRequired,
|
||||
|
||||
// opacity for dynamic UI fading effects
|
||||
opacity: React.PropTypes.number,
|
||||
|
||||
// string representing the current room app drawer state
|
||||
showApps: React.PropTypes.bool,
|
||||
};
|
||||
|
|
142
src/components/views/rooms/RoomDetailList.js
Normal file
142
src/components/views/rooms/RoomDetailList.js
Normal file
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
Copyright 2017 New Vector 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.
|
||||
*/
|
||||
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import linkifyString from 'linkifyjs/string';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { ContentRepo } from 'matrix-js-sdk';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function getDisplayAliasForRoom(room) {
|
||||
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
|
||||
}
|
||||
|
||||
const RoomDetailRow = React.createClass({
|
||||
propTypes: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
topic: PropTypes.string,
|
||||
roomId: PropTypes.string,
|
||||
avatarUrl: PropTypes.string,
|
||||
numJoinedMembers: PropTypes.number,
|
||||
canonicalAlias: PropTypes.string,
|
||||
aliases: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
worldReadable: PropTypes.bool,
|
||||
guestCanJoin: PropTypes.bool,
|
||||
}),
|
||||
|
||||
onClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
},
|
||||
|
||||
onTopicClick: function(ev) {
|
||||
// When clicking a link in the topic, prevent the event being propagated
|
||||
// to `onClick`.
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
const room = this.props.room;
|
||||
const name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
|
||||
const topic = linkifyString(sanitizeHtml(room.topic || ''));
|
||||
|
||||
const guestRead = room.worldReadable ? (
|
||||
<div className="mx_RoomDirectory_perm">{ _t('World readable') }</div>
|
||||
) : <div />;
|
||||
const guestJoin = room.guestCanJoin ? (
|
||||
<div className="mx_RoomDirectory_perm">{ _t('Guests can join') }</div>
|
||||
) : <div />;
|
||||
|
||||
const perms = (guestRead || guestJoin) ? (<div className="mx_RoomDirectory_perms">
|
||||
{ guestRead }
|
||||
{ guestJoin }
|
||||
</div>) : <div />;
|
||||
|
||||
return <tr key={room.roomId} onClick={this.onClick}>
|
||||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={24} height={24} resizeMethod='crop'
|
||||
name={name} idName={name}
|
||||
url={ContentRepo.getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
room.avatarUrl, 24, 24, "crop")} />
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomDescription">
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
{ perms }
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={this.onTopicClick}
|
||||
dangerouslySetInnerHTML={{ __html: topic }} />
|
||||
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomMemberCount">
|
||||
{ room.numJoinedMembers }
|
||||
</td>
|
||||
</tr>;
|
||||
},
|
||||
});
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'RoomDetailList',
|
||||
|
||||
propTypes: {
|
||||
rooms: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
topic: PropTypes.string,
|
||||
roomId: PropTypes.string,
|
||||
avatarUrl: PropTypes.string,
|
||||
numJoinedMembers: PropTypes.number,
|
||||
canonicalAlias: PropTypes.string,
|
||||
aliases: PropTypes.arrayOf(PropTypes.string),
|
||||
|
||||
worldReadable: PropTypes.bool,
|
||||
guestCanJoin: PropTypes.bool,
|
||||
})),
|
||||
},
|
||||
|
||||
getRows: function() {
|
||||
if (!this.props.rooms) return [];
|
||||
return this.props.rooms.map((room, index) => {
|
||||
return <RoomDetailRow key={index} room={room} />;
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
const rows = this.getRows();
|
||||
let rooms;
|
||||
if (rows.length == 0) {
|
||||
rooms = <i>{ _t('No rooms to show') }</i>;
|
||||
} else {
|
||||
rooms = <table ref="directory_table" className="mx_RoomDirectory_table">
|
||||
<tbody>
|
||||
{ this.getRows() }
|
||||
</tbody>
|
||||
</table>;
|
||||
}
|
||||
return <div className="mx_RoomDetailList">
|
||||
{ rooms }
|
||||
</div>;
|
||||
},
|
||||
});
|
|
@ -19,7 +19,6 @@ import Promise from 'bluebird';
|
|||
import React from 'react';
|
||||
import { _t, _tJsx, _td } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import ObjectUtils from '../../../ObjectUtils';
|
||||
|
@ -158,9 +157,9 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
sideOpacity: 0.3,
|
||||
middleOpacity: 0.3,
|
||||
action: 'panel_disable',
|
||||
sideDisabled: true,
|
||||
middleDisabled: true,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -171,9 +170,9 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: 'ui_opacity',
|
||||
sideOpacity: 1.0,
|
||||
middleOpacity: 1.0,
|
||||
action: 'panel_disable',
|
||||
sideDisabled: false,
|
||||
middleDisabled: false,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue