|
|
|
@ -47,6 +47,8 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
|
|
|
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
|
|
|
import {mediaFromMxc} from "../../../customisations/Media";
|
|
|
|
|
import {getAddressType} from "../../../UserAddress";
|
|
|
|
|
import BaseAvatar from '../avatars/BaseAvatar';
|
|
|
|
|
import AccessibleButton from '../elements/AccessibleButton';
|
|
|
|
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|
|
|
|
import {copyPlaintext, selectText} from "../../../utils/strings";
|
|
|
|
|
import * as ContextMenu from "../../structures/ContextMenu";
|
|
|
|
@ -56,6 +58,12 @@ import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
|
|
|
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
|
|
|
|
/* eslint-disable camelcase */
|
|
|
|
|
|
|
|
|
|
interface IRecentUser {
|
|
|
|
|
userId: string,
|
|
|
|
|
user: RoomMember,
|
|
|
|
|
lastActive: number,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const KIND_DM = "dm";
|
|
|
|
|
export const KIND_INVITE = "invite";
|
|
|
|
|
export const KIND_CALL_TRANSFER = "call_transfer";
|
|
|
|
@ -66,43 +74,41 @@ const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is c
|
|
|
|
|
// 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 {
|
|
|
|
|
abstract 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"); }
|
|
|
|
|
public abstract get name(): string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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"); }
|
|
|
|
|
public abstract get userId(): string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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"); }
|
|
|
|
|
public abstract getMxcAvatarUrl(): string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class DirectoryMember extends Member {
|
|
|
|
|
_userId: string;
|
|
|
|
|
_displayName: string;
|
|
|
|
|
_avatarUrl: string;
|
|
|
|
|
private readonly _userId: string;
|
|
|
|
|
private readonly displayName: string;
|
|
|
|
|
private readonly 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;
|
|
|
|
|
this.displayName = userDirResult.display_name;
|
|
|
|
|
this.avatarUrl = userDirResult.avatar_url;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// These next class members are for the Member interface
|
|
|
|
|
get name(): string {
|
|
|
|
|
return this._displayName || this._userId;
|
|
|
|
|
return this.displayName || this._userId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get userId(): string {
|
|
|
|
@ -110,32 +116,32 @@ class DirectoryMember extends Member {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMxcAvatarUrl(): string {
|
|
|
|
|
return this._avatarUrl;
|
|
|
|
|
return this.avatarUrl;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ThreepidMember extends Member {
|
|
|
|
|
_id: string;
|
|
|
|
|
private readonly id: string;
|
|
|
|
|
|
|
|
|
|
constructor(id: string) {
|
|
|
|
|
super();
|
|
|
|
|
this._id = id;
|
|
|
|
|
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('@');
|
|
|
|
|
return this.id.includes('@');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// These next class members are for the Member interface
|
|
|
|
|
get name(): string {
|
|
|
|
|
return this._id;
|
|
|
|
|
return this.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get userId(): string {
|
|
|
|
|
return this._id;
|
|
|
|
|
return this.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMxcAvatarUrl(): string {
|
|
|
|
@ -145,11 +151,11 @@ class ThreepidMember extends Member {
|
|
|
|
|
|
|
|
|
|
interface IDMUserTileProps {
|
|
|
|
|
member: RoomMember;
|
|
|
|
|
onRemove: (RoomMember) => any;
|
|
|
|
|
onRemove(member: RoomMember): void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|
|
|
|
_onRemove = (e) => {
|
|
|
|
|
private onRemove = (e) => {
|
|
|
|
|
// Stop the browser from highlighting text
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
@ -158,9 +164,6 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
|
|
|
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
|
|
|
|
|
|
|
|
|
const avatarSize = 20;
|
|
|
|
|
const avatar = this.props.member.isEmail
|
|
|
|
|
? <img
|
|
|
|
@ -182,7 +185,7 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|
|
|
|
closeButton = (
|
|
|
|
|
<AccessibleButton
|
|
|
|
|
className='mx_InviteDialog_userTile_remove'
|
|
|
|
|
onClick={this._onRemove}
|
|
|
|
|
onClick={this.onRemove}
|
|
|
|
|
>
|
|
|
|
|
<img src={require("../../../../res/img/icon-pill-remove.svg")}
|
|
|
|
|
alt={_t('Remove')} width={8} height={8}
|
|
|
|
@ -206,13 +209,13 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
|
|
|
|
interface IDMRoomTileProps {
|
|
|
|
|
member: RoomMember;
|
|
|
|
|
lastActiveTs: number;
|
|
|
|
|
onToggle: (RoomMember) => any;
|
|
|
|
|
onToggle(member: RoomMember): void;
|
|
|
|
|
highlightWord: string;
|
|
|
|
|
isSelected: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|
|
|
|
_onClick = (e) => {
|
|
|
|
|
private onClick = (e) => {
|
|
|
|
|
// Stop the browser from highlighting text
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
@ -220,7 +223,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|
|
|
|
this.props.onToggle(this.props.member);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_highlightName(str: string) {
|
|
|
|
|
private highlightName(str: string) {
|
|
|
|
|
if (!this.props.highlightWord) return str;
|
|
|
|
|
|
|
|
|
|
// We convert things to lowercase for index searching, but pull substrings from
|
|
|
|
@ -257,8 +260,6 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
|
|
|
|
|
|
|
|
|
let timestamp = null;
|
|
|
|
|
if (this.props.lastActiveTs) {
|
|
|
|
|
const humanTs = humanizeTime(this.props.lastActiveTs);
|
|
|
|
@ -296,13 +297,13 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|
|
|
|
|
|
|
|
|
const caption = this.props.member.isEmail
|
|
|
|
|
? _t("Invite by email")
|
|
|
|
|
: this._highlightName(this.props.member.userId);
|
|
|
|
|
: this.highlightName(this.props.member.userId);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className='mx_InviteDialog_roomTile' onClick={this._onClick}>
|
|
|
|
|
<div className='mx_InviteDialog_roomTile' onClick={this.onClick}>
|
|
|
|
|
{stackedAvatar}
|
|
|
|
|
<span className="mx_InviteDialog_roomTile_nameStack">
|
|
|
|
|
<div className='mx_InviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</div>
|
|
|
|
|
<div className='mx_InviteDialog_roomTile_name'>{this.highlightName(this.props.member.name)}</div>
|
|
|
|
|
<div className='mx_InviteDialog_roomTile_userId'>{caption}</div>
|
|
|
|
|
</span>
|
|
|
|
|
{timestamp}
|
|
|
|
@ -313,7 +314,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
|
|
|
|
|
|
|
|
|
interface IInviteDialogProps {
|
|
|
|
|
// Takes an array of user IDs/emails to invite.
|
|
|
|
|
onFinished: (toInvite?: string[]) => any;
|
|
|
|
|
onFinished: (toInvite?: string[]) => void;
|
|
|
|
|
|
|
|
|
|
// The kind of invite being performed. Assumed to be KIND_DM if
|
|
|
|
|
// not provided.
|
|
|
|
@ -355,8 +356,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private closeCopiedTooltip: () => void;
|
|
|
|
|
_debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
|
|
|
|
_editorRef: any = null;
|
|
|
|
|
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
|
|
|
|
private editorRef = createRef<HTMLInputElement>();
|
|
|
|
|
private unmounted = false;
|
|
|
|
|
|
|
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
@ -384,7 +386,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
filterText: this.props.initialText,
|
|
|
|
|
recents: InviteDialog.buildRecents(alreadyInvited),
|
|
|
|
|
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
|
|
|
|
suggestions: this._buildSuggestions(alreadyInvited),
|
|
|
|
|
suggestions: this.buildSuggestions(alreadyInvited),
|
|
|
|
|
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
|
|
|
|
serverResultsMixin: [],
|
|
|
|
|
threepidResultsMixin: [],
|
|
|
|
@ -396,16 +398,18 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
busy: false,
|
|
|
|
|
errorText: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this._editorRef = createRef();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
|
if (this.props.initialText) {
|
|
|
|
|
this._updateSuggestions(this.props.initialText);
|
|
|
|
|
this.updateSuggestions(this.props.initialText);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
|
this.unmounted = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
|
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
|
|
|
|
|
// the tooltip otherwise, such as pressing Escape or clicking X really quickly
|
|
|
|
@ -416,7 +420,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
this.setState({consultFirst: ev.target.checked});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
|
|
|
|
|
public static buildRecents(excludedTargetIds: Set<string>): IRecentUser[] {
|
|
|
|
|
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
|
|
|
|
|
|
|
|
|
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
|
|
|
@ -479,7 +483,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
return recents;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
|
|
|
|
private buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
|
|
|
|
const maxConsideredMembers = 200;
|
|
|
|
|
const joinedRooms = MatrixClientPeg.get().getRooms()
|
|
|
|
|
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
|
|
|
|
@ -597,7 +601,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_shouldAbortAfterInviteError(result): boolean {
|
|
|
|
|
private shouldAbortAfterInviteError(result): boolean {
|
|
|
|
|
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
|
|
|
|
if (failedUsers.length > 0) {
|
|
|
|
|
console.log("Failed to invite users: ", result);
|
|
|
|
@ -612,7 +616,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_convertFilter(): Member[] {
|
|
|
|
|
private convertFilter(): Member[] {
|
|
|
|
|
// Check to see if there's anything to convert first
|
|
|
|
|
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
|
|
|
|
|
|
|
|
@ -629,10 +633,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
return newTargets;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_startDm = async () => {
|
|
|
|
|
private startDm = async () => {
|
|
|
|
|
this.setState({busy: true});
|
|
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
|
const targets = this._convertFilter();
|
|
|
|
|
const targets = this.convertFilter();
|
|
|
|
|
const targetIds = targets.map(t => t.userId);
|
|
|
|
|
|
|
|
|
|
// Check if there is already a DM with these people and reuse it if possible.
|
|
|
|
@ -706,11 +710,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_inviteUsers = async () => {
|
|
|
|
|
private inviteUsers = async () => {
|
|
|
|
|
const startTime = CountlyAnalytics.getTimestamp();
|
|
|
|
|
this.setState({busy: true});
|
|
|
|
|
this._convertFilter();
|
|
|
|
|
const targets = this._convertFilter();
|
|
|
|
|
this.convertFilter();
|
|
|
|
|
const targets = this.convertFilter();
|
|
|
|
|
const targetIds = targets.map(t => t.userId);
|
|
|
|
|
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
@ -727,7 +731,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
try {
|
|
|
|
|
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
|
|
|
|
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
|
|
|
|
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
|
|
|
|
|
if (!this.shouldAbortAfterInviteError(result)) { // handles setting error message too
|
|
|
|
|
this.props.onFinished();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -761,9 +765,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_transferCall = async () => {
|
|
|
|
|
this._convertFilter();
|
|
|
|
|
const targets = this._convertFilter();
|
|
|
|
|
private transferCall = async () => {
|
|
|
|
|
this.convertFilter();
|
|
|
|
|
const targets = this.convertFilter();
|
|
|
|
|
const targetIds = targets.map(t => t.userId);
|
|
|
|
|
if (targetIds.length > 1) {
|
|
|
|
|
this.setState({
|
|
|
|
@ -802,26 +806,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_onKeyDown = (e) => {
|
|
|
|
|
private onKeyDown = (e) => {
|
|
|
|
|
if (this.state.busy) return;
|
|
|
|
|
const value = e.target.value.trim();
|
|
|
|
|
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
|
|
|
|
|
if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) {
|
|
|
|
|
// when the field is empty and the user hits backspace remove the right-most target
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this._removeMember(this.state.targets[this.state.targets.length - 1]);
|
|
|
|
|
this.removeMember(this.state.targets[this.state.targets.length - 1]);
|
|
|
|
|
} else if (value && e.key === Key.ENTER && !hasModifiers) {
|
|
|
|
|
// when the user hits enter with something in their field try to convert it
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this._convertFilter();
|
|
|
|
|
this.convertFilter();
|
|
|
|
|
} else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) {
|
|
|
|
|
// when the user hits space and their input looks like an e-mail/MXID then try to convert it
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this._convertFilter();
|
|
|
|
|
this.convertFilter();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_updateSuggestions = async (term) => {
|
|
|
|
|
private updateSuggestions = async (term) => {
|
|
|
|
|
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
|
|
|
|
if (term !== this.state.filterText) {
|
|
|
|
|
// Discard the results - we were probably too slow on the server-side to make
|
|
|
|
@ -930,30 +934,30 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_updateFilter = (e) => {
|
|
|
|
|
private updateFilter = (e) => {
|
|
|
|
|
const term = e.target.value;
|
|
|
|
|
this.setState({filterText: term});
|
|
|
|
|
|
|
|
|
|
// Debounce server lookups to reduce spam. We don't clear the existing server
|
|
|
|
|
// results because they might still be vaguely accurate, likewise for races which
|
|
|
|
|
// could happen here.
|
|
|
|
|
if (this._debounceTimer) {
|
|
|
|
|
clearTimeout(this._debounceTimer);
|
|
|
|
|
if (this.debounceTimer) {
|
|
|
|
|
clearTimeout(this.debounceTimer);
|
|
|
|
|
}
|
|
|
|
|
this._debounceTimer = setTimeout(() => {
|
|
|
|
|
this._updateSuggestions(term);
|
|
|
|
|
this.debounceTimer = setTimeout(() => {
|
|
|
|
|
this.updateSuggestions(term);
|
|
|
|
|
}, 150); // 150ms debounce (human reaction time + some)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_showMoreRecents = () => {
|
|
|
|
|
private showMoreRecents = () => {
|
|
|
|
|
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_showMoreSuggestions = () => {
|
|
|
|
|
private showMoreSuggestions = () => {
|
|
|
|
|
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_toggleMember = (member: Member) => {
|
|
|
|
|
private toggleMember = (member: Member) => {
|
|
|
|
|
if (!this.state.busy) {
|
|
|
|
|
let filterText = this.state.filterText;
|
|
|
|
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
|
|
|
@ -966,13 +970,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
}
|
|
|
|
|
this.setState({targets, filterText});
|
|
|
|
|
|
|
|
|
|
if (this._editorRef && this._editorRef.current) {
|
|
|
|
|
this._editorRef.current.focus();
|
|
|
|
|
if (this.editorRef && this.editorRef.current) {
|
|
|
|
|
this.editorRef.current.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_removeMember = (member: Member) => {
|
|
|
|
|
private removeMember = (member: Member) => {
|
|
|
|
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
|
|
|
|
const idx = targets.indexOf(member);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
@ -980,12 +984,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
this.setState({targets});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._editorRef && this._editorRef.current) {
|
|
|
|
|
this._editorRef.current.focus();
|
|
|
|
|
if (this.editorRef && this.editorRef.current) {
|
|
|
|
|
this.editorRef.current.focus();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_onPaste = async (e) => {
|
|
|
|
|
private onPaste = async (e) => {
|
|
|
|
|
if (this.state.filterText) {
|
|
|
|
|
// if the user has already typed something, just let them
|
|
|
|
|
// paste normally.
|
|
|
|
@ -1039,6 +1043,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
failed.push(address);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (this.unmounted) return;
|
|
|
|
|
|
|
|
|
|
if (failed.length > 0) {
|
|
|
|
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
|
|
|
@ -1055,17 +1060,17 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
this.setState({targets: [...this.state.targets, ...toAdd]});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_onClickInputArea = (e) => {
|
|
|
|
|
private onClickInputArea = (e) => {
|
|
|
|
|
// Stop the browser from highlighting text
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
if (this._editorRef && this._editorRef.current) {
|
|
|
|
|
this._editorRef.current.focus();
|
|
|
|
|
if (this.editorRef && this.editorRef.current) {
|
|
|
|
|
this.editorRef.current.focus();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_onUseDefaultIdentityServerClick = (e) => {
|
|
|
|
|
private onUseDefaultIdentityServerClick = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
// Update the IS in account data. Actually using it may trigger terms.
|
|
|
|
@ -1074,21 +1079,21 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_onManageSettingsClick = (e) => {
|
|
|
|
|
private onManageSettingsClick = (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dis.fire(Action.ViewUserSettings);
|
|
|
|
|
this.props.onFinished();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_onCommunityInviteClick = (e) => {
|
|
|
|
|
private onCommunityInviteClick = (e) => {
|
|
|
|
|
this.props.onFinished();
|
|
|
|
|
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_renderSection(kind: "recents"|"suggestions") {
|
|
|
|
|
private renderSection(kind: "recents"|"suggestions") {
|
|
|
|
|
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
|
|
|
|
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
|
|
|
|
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
|
|
|
|
|
const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this);
|
|
|
|
|
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
|
|
|
|
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
|
|
|
|
let sectionSubname = null;
|
|
|
|
@ -1168,7 +1173,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
member={r.user}
|
|
|
|
|
lastActiveTs={lastActive(r)}
|
|
|
|
|
key={r.userId}
|
|
|
|
|
onToggle={this._toggleMember}
|
|
|
|
|
onToggle={this.toggleMember}
|
|
|
|
|
highlightWord={this.state.filterText}
|
|
|
|
|
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
|
|
|
|
/>
|
|
|
|
@ -1183,32 +1188,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_renderEditor() {
|
|
|
|
|
private renderEditor() {
|
|
|
|
|
const targets = this.state.targets.map(t => (
|
|
|
|
|
<DMUserTile member={t} onRemove={!this.state.busy && this._removeMember} key={t.userId} />
|
|
|
|
|
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
|
|
|
|
|
));
|
|
|
|
|
const input = (
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
onKeyDown={this._onKeyDown}
|
|
|
|
|
onChange={this._updateFilter}
|
|
|
|
|
onKeyDown={this.onKeyDown}
|
|
|
|
|
onChange={this.updateFilter}
|
|
|
|
|
value={this.state.filterText}
|
|
|
|
|
ref={this._editorRef}
|
|
|
|
|
onPaste={this._onPaste}
|
|
|
|
|
ref={this.editorRef}
|
|
|
|
|
onPaste={this.onPaste}
|
|
|
|
|
autoFocus={true}
|
|
|
|
|
disabled={this.state.busy}
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<div className='mx_InviteDialog_editor' onClick={this._onClickInputArea}>
|
|
|
|
|
<div className='mx_InviteDialog_editor' onClick={this.onClickInputArea}>
|
|
|
|
|
{targets}
|
|
|
|
|
{input}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_renderIdentityServerWarning() {
|
|
|
|
|
private renderIdentityServerWarning() {
|
|
|
|
|
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
|
|
|
|
|
!SettingsStore.getValue(UIFeature.IdentityServer)
|
|
|
|
|
) {
|
|
|
|
@ -1226,8 +1231,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>,
|
|
|
|
|
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
|
|
|
|
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
|
|
|
|
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
|
|
|
|
},
|
|
|
|
|
)}</div>
|
|
|
|
|
);
|
|
|
|
@ -1237,7 +1242,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
"Use an identity server to invite by email. " +
|
|
|
|
|
"Manage in <settings>Settings</settings>.",
|
|
|
|
|
{}, {
|
|
|
|
|
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
|
|
|
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
|
|
|
|
},
|
|
|
|
|
)}</div>
|
|
|
|
|
);
|
|
|
|
@ -1329,7 +1334,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
return (
|
|
|
|
|
<AccessibleButton
|
|
|
|
|
kind="link"
|
|
|
|
|
onClick={this._onCommunityInviteClick}
|
|
|
|
|
onClick={this.onCommunityInviteClick}
|
|
|
|
|
>{sub}</AccessibleButton>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
@ -1340,7 +1345,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
</React.Fragment>;
|
|
|
|
|
}
|
|
|
|
|
buttonText = _t("Go");
|
|
|
|
|
goButtonFn = this._startDm;
|
|
|
|
|
goButtonFn = this.startDm;
|
|
|
|
|
extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
|
|
|
|
|
<span>{ _t("Some suggestions may be hidden for privacy.") }</span>
|
|
|
|
|
<p>{ _t("If you can’t see who you’re looking for, send them your invite link below.") }</p>
|
|
|
|
@ -1397,7 +1402,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
buttonText = _t("Invite");
|
|
|
|
|
goButtonFn = this._inviteUsers;
|
|
|
|
|
goButtonFn = this.inviteUsers;
|
|
|
|
|
|
|
|
|
|
if (cli.isRoomEncrypted(this.props.roomId)) {
|
|
|
|
|
const room = cli.getRoom(this.props.roomId);
|
|
|
|
@ -1419,7 +1424,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
|
|
|
|
title = _t("Transfer");
|
|
|
|
|
buttonText = _t("Transfer");
|
|
|
|
|
goButtonFn = this._transferCall;
|
|
|
|
|
goButtonFn = this.transferCall;
|
|
|
|
|
footer = <div>
|
|
|
|
|
<label>
|
|
|
|
|
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
|
|
|
@ -1442,7 +1447,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
<div className='mx_InviteDialog_content'>
|
|
|
|
|
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
|
|
|
|
<div className='mx_InviteDialog_addressBar'>
|
|
|
|
|
{this._renderEditor()}
|
|
|
|
|
{this.renderEditor()}
|
|
|
|
|
<div className='mx_InviteDialog_buttonAndSpinner'>
|
|
|
|
|
<AccessibleButton
|
|
|
|
|
kind="primary"
|
|
|
|
@ -1456,11 +1461,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{keySharingWarning}
|
|
|
|
|
{this._renderIdentityServerWarning()}
|
|
|
|
|
{this.renderIdentityServerWarning()}
|
|
|
|
|
<div className='error'>{this.state.errorText}</div>
|
|
|
|
|
<div className='mx_InviteDialog_userSections'>
|
|
|
|
|
{this._renderSection('recents')}
|
|
|
|
|
{this._renderSection('suggestions')}
|
|
|
|
|
{this.renderSection('recents')}
|
|
|
|
|
{this.renderSection('suggestions')}
|
|
|
|
|
{extraSection}
|
|
|
|
|
</div>
|
|
|
|
|
{footer}
|
|
|
|
|