Merge branches 'develop' and 't3chguy/rightpanel' of github.com:matrix-org/matrix-react-sdk into t3chguy/rightpanel
This commit is contained in:
commit
5a5a0d2233
31 changed files with 1333 additions and 102 deletions
|
@ -55,7 +55,7 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
_getState: function(props) {
|
||||
if (props.member) {
|
||||
if (props.member && props.member.name) {
|
||||
return {
|
||||
name: props.member.name,
|
||||
title: props.title || props.member.userId,
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import sdk from "../../../index";
|
||||
|
@ -25,24 +25,56 @@ import {RoomMember} from "matrix-js-sdk/lib/matrix";
|
|||
import * as humanize from "humanize";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/lib/content-repo";
|
||||
import * as Email from "../../../email";
|
||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||
import dis from "../../../dispatcher";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
|
||||
// TODO: [TravisR] Make this generic for all kinds of invites
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||
|
||||
class DirectoryMember {
|
||||
// This is the interface that is expected by various components in this file. It is a bit
|
||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// for 3PIDs/email addresses.
|
||||
//
|
||||
// XXX: We should use TypeScript interfaces instead of this weird "abstract" class.
|
||||
class Member {
|
||||
/**
|
||||
* The display name of this Member. For users this should be their profile's display
|
||||
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||
*/
|
||||
get name(): string { throw new Error("Member class not implemented"); }
|
||||
|
||||
/**
|
||||
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
||||
* be the 3PID address (email).
|
||||
*/
|
||||
get userId(): string { throw new Error("Member class not implemented"); }
|
||||
|
||||
/**
|
||||
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
||||
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
||||
*/
|
||||
getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); }
|
||||
}
|
||||
|
||||
class DirectoryMember extends Member {
|
||||
_userId: string;
|
||||
_displayName: string;
|
||||
_avatarUrl: string;
|
||||
|
||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
this._displayName = userDirResult.display_name;
|
||||
this._avatarUrl = userDirResult.avatar_url;
|
||||
}
|
||||
|
||||
// These next members are to implement the contract expected by DMRoomTile
|
||||
// These next class members are for the Member interface
|
||||
get name(): string {
|
||||
return this._displayName || this._userId;
|
||||
}
|
||||
|
@ -56,13 +88,93 @@ class DirectoryMember {
|
|||
}
|
||||
}
|
||||
|
||||
class ThreepidMember extends Member {
|
||||
_id: string;
|
||||
|
||||
constructor(id: string) {
|
||||
super();
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
// This is a getter that would be falsey on all other implementations. Until we have
|
||||
// better type support in the react-sdk we can use this trick to determine the kind
|
||||
// of 3PID we're dealing with, if any.
|
||||
get isEmail(): boolean {
|
||||
return this._id.includes('@');
|
||||
}
|
||||
|
||||
// These next class members are for the Member interface
|
||||
get name(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get userId(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
getMxcAvatarUrl(): string {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class DMUserTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||
onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed
|
||||
};
|
||||
|
||||
_onRemove = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.onRemove(this.props.member);
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const avatarSize = 20;
|
||||
const avatar = this.props.member.isEmail
|
||||
? <img
|
||||
className='mx_DMInviteDialog_userTile_avatar mx_DMInviteDialog_userTile_threepidAvatar'
|
||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
width={avatarSize} height={avatarSize} />
|
||||
: <BaseAvatar
|
||||
className='mx_DMInviteDialog_userTile_avatar'
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||
avatarSize, avatarSize, "crop")}
|
||||
name={this.props.member.name}
|
||||
idName={this.props.member.userId}
|
||||
width={avatarSize}
|
||||
height={avatarSize} />;
|
||||
|
||||
return (
|
||||
<span className='mx_DMInviteDialog_userTile'>
|
||||
<span className='mx_DMInviteDialog_userTile_pill'>
|
||||
{avatar}
|
||||
<span className='mx_DMInviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||
</span>
|
||||
<AccessibleButton
|
||||
className='mx_DMInviteDialog_userTile_remove'
|
||||
onClick={this._onRemove}
|
||||
>
|
||||
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||
</AccessibleButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DMRoomTile extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// Has properties to match RoomMember: userId (str), name (str), getMxcAvatarUrl(): string
|
||||
member: PropTypes.object.isRequired,
|
||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
||||
lastActiveTs: PropTypes.number,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
onToggle: PropTypes.func.isRequired, // takes 1 argument, the member being toggled
|
||||
highlightWord: PropTypes.string,
|
||||
isSelected: PropTypes.bool,
|
||||
};
|
||||
|
||||
_onClick = (e) => {
|
||||
|
@ -70,7 +182,7 @@ class DMRoomTile extends React.PureComponent {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.onToggle(this.props.member.userId);
|
||||
this.props.onToggle(this.props.member);
|
||||
};
|
||||
|
||||
_highlightName(str: string) {
|
||||
|
@ -121,19 +233,37 @@ class DMRoomTile extends React.PureComponent {
|
|||
}
|
||||
|
||||
const avatarSize = 36;
|
||||
const avatarUrl = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||
avatarSize, avatarSize, "crop");
|
||||
const avatar = this.props.member.isEmail
|
||||
? <img
|
||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||
width={avatarSize} height={avatarSize} />
|
||||
: <BaseAvatar
|
||||
url={getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), this.props.member.getMxcAvatarUrl(),
|
||||
avatarSize, avatarSize, "crop")}
|
||||
name={this.props.member.name}
|
||||
idName={this.props.member.userId}
|
||||
width={avatarSize}
|
||||
height={avatarSize} />;
|
||||
|
||||
let checkmark = null;
|
||||
if (this.props.isSelected) {
|
||||
// To reduce flickering we put the 'selected' room tile above the real avatar
|
||||
checkmark = <div className='mx_DMInviteDialog_roomTile_selected' />;
|
||||
}
|
||||
|
||||
// To reduce flickering we put the checkmark on top of the actual avatar (prevents
|
||||
// the browser from reloading the image source when the avatar remounts).
|
||||
const stackedAvatar = (
|
||||
<span className='mx_DMInviteDialog_roomTile_avatarStack'>
|
||||
{avatar}
|
||||
{checkmark}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_roomTile' onClick={this._onClick}>
|
||||
<BaseAvatar
|
||||
url={avatarUrl}
|
||||
name={this.props.member.name}
|
||||
idName={this.props.member.userId}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
/>
|
||||
{stackedAvatar}
|
||||
<span className='mx_DMInviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</span>
|
||||
<span className='mx_DMInviteDialog_roomTile_userId'>{this._highlightName(this.props.member.userId)}</span>
|
||||
{timestamp}
|
||||
|
@ -149,19 +279,25 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
};
|
||||
|
||||
_debounceTimer: number = null;
|
||||
_editorRef: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
targets: [], // string[] of mxids/email addresses
|
||||
targets: [], // array of Member objects (see interface above)
|
||||
filterText: "",
|
||||
recents: this._buildRecents(),
|
||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||
suggestions: this._buildSuggestions(),
|
||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||
serverResultsMixin: [], // { user: DirectoryMember, userId: string }[], like recents and suggestions
|
||||
threepidResultsMixin: [], // { user: ThreepidMember, userId: string}[], like recents and suggestions
|
||||
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
||||
tryingIdentityServer: false,
|
||||
};
|
||||
|
||||
this._editorRef = createRef();
|
||||
}
|
||||
|
||||
_buildRecents(): {userId: string, user: RoomMember, lastActive: number} {
|
||||
|
@ -245,7 +381,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
}
|
||||
|
||||
_startDm = () => {
|
||||
this.props.onFinished(this.state.targets);
|
||||
this.props.onFinished(this.state.targets.map(t => t.userId));
|
||||
};
|
||||
|
||||
_cancel = () => {
|
||||
|
@ -262,7 +398,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
if (this._debounceTimer) {
|
||||
clearTimeout(this._debounceTimer);
|
||||
}
|
||||
this._debounceTimer = setTimeout(() => {
|
||||
this._debounceTimer = setTimeout(async () => {
|
||||
MatrixClientPeg.get().searchUserDirectory({term}).then(r => {
|
||||
if (term !== this.state.filterText) {
|
||||
// Discard the results - we were probably too slow on the server-side to make
|
||||
|
@ -281,6 +417,62 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
console.error(e);
|
||||
this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal
|
||||
});
|
||||
|
||||
// Whenever we search the directory, also try to search the identity server. It's
|
||||
// all debounced the same anyways.
|
||||
if (!this.state.canUseIdentityServer) {
|
||||
// The user doesn't have an identity server set - warn them of that.
|
||||
this.setState({tryingIdentityServer: true});
|
||||
return;
|
||||
}
|
||||
if (term.indexOf('@') > 0 && Email.looksValid(term)) {
|
||||
// Start off by suggesting the plain email while we try and resolve it
|
||||
// to a real account.
|
||||
this.setState({
|
||||
// per above: the userId is a lie here - it's just a regular identifier
|
||||
threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}],
|
||||
});
|
||||
try {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const token = await authClient.getAccessToken();
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||
'email',
|
||||
term,
|
||||
undefined, // callback
|
||||
token,
|
||||
);
|
||||
if (term !== this.state.filterText) return; // abandon hope
|
||||
|
||||
if (!lookup || !lookup.mxid) {
|
||||
// We weren't able to find anyone - we're already suggesting the plain email
|
||||
// as an alternative, so do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
// We append the user suggestion to give the user an option to click
|
||||
// the email anyways, and so we don't cause things to jump around. In
|
||||
// theory, the user would see the user pop up and think "ah yes, that
|
||||
// person!"
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
||||
if (term !== this.state.filterText || !profile) return; // abandon hope
|
||||
this.setState({
|
||||
threepidResultsMixin: [...this.state.threepidResultsMixin, {
|
||||
user: new DirectoryMember({
|
||||
user_id: lookup.mxid,
|
||||
display_name: profile.displayname,
|
||||
avatar_url: profile.avatar_url,
|
||||
}),
|
||||
userId: lookup.mxid,
|
||||
}],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error searching identity server:");
|
||||
console.error(e);
|
||||
this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal
|
||||
}
|
||||
}
|
||||
}, 150); // 150ms debounce (human reaction time + some)
|
||||
};
|
||||
|
||||
|
@ -292,14 +484,112 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
||||
};
|
||||
|
||||
_toggleMember = (userId) => {
|
||||
_toggleMember = (member: Member) => {
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(userId);
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) targets.splice(idx, 1);
|
||||
else targets.push(userId);
|
||||
else targets.push(member);
|
||||
this.setState({targets});
|
||||
};
|
||||
|
||||
_removeMember = (member: Member) => {
|
||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||
const idx = targets.indexOf(member);
|
||||
if (idx >= 0) {
|
||||
targets.splice(idx, 1);
|
||||
this.setState({targets});
|
||||
}
|
||||
};
|
||||
|
||||
_onPaste = async (e) => {
|
||||
// Prevent the text being pasted into the textarea
|
||||
e.preventDefault();
|
||||
|
||||
// Process it as a list of addresses to add instead
|
||||
const text = e.clipboardData.getData("text");
|
||||
const possibleMembers = [
|
||||
// If we can avoid hitting the profile endpoint, we should.
|
||||
...this.state.recents,
|
||||
...this.state.suggestions,
|
||||
...this.state.serverResultsMixin,
|
||||
...this.state.threepidResultsMixin,
|
||||
];
|
||||
const toAdd = [];
|
||||
const failed = [];
|
||||
const potentialAddresses = text.split(/[\s,]+/);
|
||||
for (const address of potentialAddresses) {
|
||||
const member = possibleMembers.find(m => m.userId === address);
|
||||
if (member) {
|
||||
toAdd.push(member.user);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address.indexOf('@') > 0 && Email.looksValid(address)) {
|
||||
toAdd.push(new ThreepidMember(address));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address[0] !== '@') {
|
||||
failed.push(address); // not a user ID
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(address);
|
||||
const displayName = profile ? profile.displayname : null;
|
||||
const avatarUrl = profile ? profile.avatar_url : null;
|
||||
toAdd.push(new DirectoryMember({
|
||||
user_id: address,
|
||||
display_name: displayName,
|
||||
avatar_url: avatarUrl,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error("Error looking up profile for " + address);
|
||||
console.error(e);
|
||||
failed.push(address);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Invite Paste Fail', '', QuestionDialog, {
|
||||
title: _t('Failed to find the following users'),
|
||||
description: _t(
|
||||
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s",
|
||||
{csvNames: failed.join(", ")},
|
||||
),
|
||||
button: _t('OK'),
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({targets: [...this.state.targets, ...toAdd]});
|
||||
};
|
||||
|
||||
_onClickInputArea = (e) => {
|
||||
// Stop the browser from highlighting text
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this._editorRef && this._editorRef.current) {
|
||||
this._editorRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_onUseDefaultIdentityServerClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Update the IS in account data. Actually using it may trigger terms.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useDefaultIdentityServer();
|
||||
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
||||
};
|
||||
|
||||
_onManageSettingsClick = (e) => {
|
||||
e.preventDefault();
|
||||
dis.dispatch({ action: 'view_user_settings' });
|
||||
this._cancel();
|
||||
};
|
||||
|
||||
_renderSection(kind: "recents"|"suggestions") {
|
||||
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||
|
@ -307,17 +597,27 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||
const sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||
|
||||
// Mix in the server results if we have any, but only if we're searching
|
||||
if (this.state.filterText && this.state.serverResultsMixin && kind === 'suggestions') {
|
||||
// only pick out the server results that aren't already covered though
|
||||
const uniqueServerResults = this.state.serverResultsMixin
|
||||
.filter(u => !sourceMembers.some(m => m.userId === u.userId));
|
||||
// Mix in the server results if we have any, but only if we're searching. We track the additional
|
||||
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
|
||||
// the right members in them.
|
||||
let additionalMembers = [];
|
||||
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
|
||||
if (this.state.filterText && hasMixins && kind === 'suggestions') {
|
||||
// We don't want to duplicate members though, so just exclude anyone we've already seen.
|
||||
const notAlreadyExists = (u: Member): boolean => {
|
||||
return !sourceMembers.some(m => m.userId === u.userId)
|
||||
&& !additionalMembers.some(m => m.userId === u.userId);
|
||||
};
|
||||
|
||||
sourceMembers = sourceMembers.concat(uniqueServerResults);
|
||||
const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueServerResults);
|
||||
|
||||
const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueThreepidResults);
|
||||
}
|
||||
|
||||
// Hide the section if there's nothing to filter by
|
||||
if (!sourceMembers || sourceMembers.length === 0) return null;
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) return null;
|
||||
|
||||
// Do some simple filtering on the input before going much further. If we get no results, say so.
|
||||
if (this.state.filterText) {
|
||||
|
@ -325,7 +625,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
sourceMembers = sourceMembers
|
||||
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
|
||||
|
||||
if (sourceMembers.length === 0) {
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
|
@ -335,6 +635,10 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
// Now we mix in the additional members. Again, we presume these have already been filtered. We
|
||||
// also assume they are more relevant than our suggestions and prepend them to the list.
|
||||
sourceMembers = [...additionalMembers, ...sourceMembers];
|
||||
|
||||
// If we're going to hide one member behind 'show more', just use up the space of the button
|
||||
// with the member's tile instead.
|
||||
if (showNum === sourceMembers.length - 1) showNum++;
|
||||
|
@ -360,6 +664,7 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
key={r.userId}
|
||||
onToggle={this._toggleMember}
|
||||
highlightWord={this.state.filterText}
|
||||
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
||||
/>
|
||||
));
|
||||
return (
|
||||
|
@ -371,23 +676,65 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Field = sdk.getComponent("elements.Field");
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
// Dev note: The use of Field is temporary/incomplete pending https://github.com/vector-im/riot-web/issues/11197
|
||||
// For now, we just list who the targets are.
|
||||
const editor = (
|
||||
<div className='mx_DMInviteDialog_editor'>
|
||||
<Field
|
||||
id="inviteTargets"
|
||||
value={this.state.filterText}
|
||||
onChange={this._updateFilter}
|
||||
/>
|
||||
_renderEditor() {
|
||||
const targets = this.state.targets.map(t => (
|
||||
<DMUserTile member={t} onRemove={this._removeMember} key={t.userId} />
|
||||
));
|
||||
const input = (
|
||||
<textarea
|
||||
key={"input"}
|
||||
rows={1}
|
||||
onChange={this._updateFilter}
|
||||
defaultValue={this.state.filterText}
|
||||
ref={this._editorRef}
|
||||
onPaste={this._onPaste}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className='mx_DMInviteDialog_editor' onClick={this._onClickInputArea}>
|
||||
{targets}
|
||||
{input}
|
||||
</div>
|
||||
);
|
||||
const targets = this.state.targets.map(t => <div key={t}>{t}</div>);
|
||||
}
|
||||
|
||||
_renderIdentityServerWarning() {
|
||||
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||
if (defaultIdentityServerUrl) {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||
"or manage in <settings>Settings</settings>.",
|
||||
{
|
||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
{
|
||||
default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{_t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
||||
},
|
||||
)}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
return (
|
||||
|
@ -406,9 +753,9 @@ export default class DMInviteDialog extends React.PureComponent {
|
|||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
)}
|
||||
</p>
|
||||
{targets}
|
||||
<div className='mx_DMInviteDialog_addressBar'>
|
||||
{editor}
|
||||
{this._renderEditor()}
|
||||
{this._renderIdentityServerWarning()}
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this._startDm}
|
||||
|
|
|
@ -159,6 +159,14 @@ module.exports = createReactClass({
|
|||
<E2EIcon status={this.props.e2eStatus} /> :
|
||||
undefined;
|
||||
|
||||
const joinRules = this.props.room && this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
||||
const joinRule = joinRules && joinRules.getContent().join_rule;
|
||||
const joinRuleClass = classNames("mx_RoomHeader_PrivateIcon",
|
||||
{"mx_RoomHeader_isPrivate": joinRule === "invite"});
|
||||
const privateIcon = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
|
||||
<div className={joinRuleClass} /> :
|
||||
undefined;
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
|
||||
}
|
||||
|
@ -303,6 +311,7 @@ module.exports = createReactClass({
|
|||
<div className="mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
{ e2eIcon }
|
||||
{ privateIcon }
|
||||
{ name }
|
||||
{ topicElement }
|
||||
{ cancelButton }
|
||||
|
|
|
@ -56,7 +56,11 @@ module.exports = createReactClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
|
||||
const joinRule = joinRules && joinRules.getContent().join_rule;
|
||||
|
||||
return ({
|
||||
joinRule,
|
||||
hover: false,
|
||||
badgeHover: false,
|
||||
contextMenuPosition: null, // DOM bounding box, null if non-shown
|
||||
|
@ -104,6 +108,12 @@ module.exports = createReactClass({
|
|||
});
|
||||
},
|
||||
|
||||
onJoinRule: function(ev) {
|
||||
if (ev.getType() !== "m.room.join_rules") return;
|
||||
if (ev.getRoomId() !== this.props.room.roomId) return;
|
||||
this.setState({ joinRule: ev.getContent().join_rule });
|
||||
},
|
||||
|
||||
onAccountData: function(accountDataEvent) {
|
||||
if (accountDataEvent.getType() === 'm.push_rules') {
|
||||
this.setState({
|
||||
|
@ -143,6 +153,7 @@ module.exports = createReactClass({
|
|||
const cli = MatrixClientPeg.get();
|
||||
cli.on("accountData", this.onAccountData);
|
||||
cli.on("Room.name", this.onRoomName);
|
||||
cli.on("RoomState.events", this.onJoinRule);
|
||||
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
|
@ -159,6 +170,7 @@ module.exports = createReactClass({
|
|||
if (cli) {
|
||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
cli.removeListener("RoomState.events", this.onJoinRule);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
@ -292,6 +304,8 @@ module.exports = createReactClass({
|
|||
|
||||
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
|
||||
|
||||
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
|
@ -303,6 +317,7 @@ module.exports = createReactClass({
|
|||
'mx_RoomTile_noBadges': !badges,
|
||||
'mx_RoomTile_transparent': this.props.transparent,
|
||||
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
|
||||
'mx_RoomTile_isPrivate': this.state.joinRule == "invite" && !dmUserId,
|
||||
});
|
||||
|
||||
const avatarClasses = classNames({
|
||||
|
@ -366,8 +381,6 @@ module.exports = createReactClass({
|
|||
|
||||
let ariaLabel = name;
|
||||
|
||||
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
|
||||
|
||||
let dmIndicator;
|
||||
let dmOnline;
|
||||
if (dmUserId) {
|
||||
|
@ -381,7 +394,10 @@ module.exports = createReactClass({
|
|||
|
||||
const { room } = this.props;
|
||||
const member = room.getMember(dmUserId);
|
||||
if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
|
||||
if (
|
||||
member && member.membership === "join" && room.getJoinedMemberCount() === 2 &&
|
||||
SettingsStore.isFeatureEnabled("feature_presence_in_room_list")
|
||||
) {
|
||||
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
|
||||
dmOnline = <UserOnlineDot userId={dmUserId} />;
|
||||
}
|
||||
|
@ -410,6 +426,11 @@ module.exports = createReactClass({
|
|||
);
|
||||
}
|
||||
|
||||
let privateIcon = null;
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
privateIcon = <div className="mx_RoomTile_PrivateIcon" />;
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<AccessibleButton
|
||||
tabIndex="0"
|
||||
|
@ -428,6 +449,7 @@ module.exports = createReactClass({
|
|||
{ dmIndicator }
|
||||
</div>
|
||||
</div>
|
||||
{ privateIcon }
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
<div className="mx_RoomTile_labelContainer">
|
||||
{ label }
|
||||
|
|
|
@ -140,7 +140,7 @@ _td("Book");
|
|||
_td("Pencil");
|
||||
_td("Paperclip");
|
||||
_td("Scissors");
|
||||
_td("Padlock");
|
||||
_td("Lock");
|
||||
_td("Key");
|
||||
_td("Hammer");
|
||||
_td("Telephone");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue