2020-01-03 03:40:18 +03:00
|
|
|
|
/*
|
|
|
|
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
|
2021-06-03 18:44:28 +03:00
|
|
|
|
import React, { createRef } from 'react';
|
|
|
|
|
import classNames from 'classnames';
|
|
|
|
|
|
2021-06-24 12:03:32 +03:00
|
|
|
|
import { _t, _td } from "../../../languageHandler";
|
|
|
|
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
|
|
|
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
2020-01-03 03:40:18 +03:00
|
|
|
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
2021-06-24 12:03:32 +03:00
|
|
|
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
2020-01-04 05:41:06 +03:00
|
|
|
|
import SdkConfig from "../../../SdkConfig";
|
2020-01-09 06:49:29 +03:00
|
|
|
|
import * as Email from "../../../email";
|
2021-06-24 12:03:32 +03:00
|
|
|
|
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
|
|
|
|
|
import { abbreviateUrl } from "../../../utils/UrlUtils";
|
2020-05-14 05:41:41 +03:00
|
|
|
|
import dis from "../../../dispatcher/dispatcher";
|
2020-01-09 06:49:29 +03:00
|
|
|
|
import IdentityAuthClient from "../../../IdentityAuthClient";
|
2020-01-09 07:19:00 +03:00
|
|
|
|
import Modal from "../../../Modal";
|
2021-06-24 12:03:32 +03:00
|
|
|
|
import { humanizeTime } from "../../../utils/humanize";
|
2021-03-25 22:56:21 +03:00
|
|
|
|
import createRoom, {
|
2021-06-24 12:03:32 +03:00
|
|
|
|
canEncryptToAllUsers,
|
|
|
|
|
findDMForUser,
|
|
|
|
|
privateShouldBeEncrypted,
|
2021-03-25 22:56:21 +03:00
|
|
|
|
} from "../../../createRoom";
|
2021-06-16 13:48:14 +03:00
|
|
|
|
import {
|
|
|
|
|
IInviteResult,
|
|
|
|
|
inviteMultipleToRoom,
|
|
|
|
|
showAnyInviteErrors,
|
|
|
|
|
showCommunityInviteDialog,
|
|
|
|
|
} from "../../../RoomInvite";
|
2021-06-24 12:03:32 +03:00
|
|
|
|
import { Key } from "../../../Keyboard";
|
|
|
|
|
import { Action } from "../../../dispatcher/actions";
|
|
|
|
|
import { DefaultTagID } from "../../../stores/room-list/models";
|
2020-07-18 00:11:34 +03:00
|
|
|
|
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
2021-06-24 12:03:32 +03:00
|
|
|
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
2020-09-16 16:45:34 +03:00
|
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
2021-06-24 12:03:32 +03:00
|
|
|
|
import { UIFeature } from "../../../settings/UIFeature";
|
2020-10-29 18:53:14 +03:00
|
|
|
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
2021-06-24 12:03:32 +03:00
|
|
|
|
import { Room } from "matrix-js-sdk/src/models/room";
|
2020-12-15 17:59:06 +03:00
|
|
|
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
2021-06-24 12:03:32 +03:00
|
|
|
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
|
|
|
|
import { mediaFromMxc } from "../../../customisations/Media";
|
|
|
|
|
import { getAddressType } from "../../../UserAddress";
|
2021-05-26 18:47:46 +03:00
|
|
|
|
import BaseAvatar from '../avatars/BaseAvatar';
|
|
|
|
|
import AccessibleButton from '../elements/AccessibleButton';
|
2021-06-02 12:42:17 +03:00
|
|
|
|
import { compare } from '../../../utils/strings';
|
2021-06-14 22:38:43 +03:00
|
|
|
|
import { IInvite3PID } from "matrix-js-sdk/src/@types/requests";
|
2021-05-27 17:51:25 +03:00
|
|
|
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
2021-06-03 18:22:59 +03:00
|
|
|
|
import { copyPlaintext, selectText } from "../../../utils/strings";
|
2021-05-27 17:51:25 +03:00
|
|
|
|
import * as ContextMenu from "../../structures/ContextMenu";
|
2021-06-03 18:22:59 +03:00
|
|
|
|
import { toRightOf } from "../../structures/ContextMenu";
|
2021-05-27 17:51:25 +03:00
|
|
|
|
import GenericTextContextMenu from "../context_menus/GenericTextContextMenu";
|
2021-07-15 11:55:58 +03:00
|
|
|
|
import { TransferCallPayload } from '../../../dispatcher/payloads/TransferCallPayload';
|
|
|
|
|
import Field from '../elements/Field';
|
|
|
|
|
import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
|
|
|
|
|
import Dialpad from '../voip/DialPad';
|
2021-07-03 11:44:03 +03:00
|
|
|
|
import QuestionDialog from "./QuestionDialog";
|
|
|
|
|
import Spinner from "../elements/Spinner";
|
|
|
|
|
import BaseDialog from "./BaseDialog";
|
2021-07-15 11:55:58 +03:00
|
|
|
|
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
2021-07-15 10:26:49 +03:00
|
|
|
|
import SpaceStore from "../../../stores/SpaceStore";
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
2020-07-24 00:58:06 +03:00
|
|
|
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
|
|
|
|
/* eslint-disable camelcase */
|
|
|
|
|
|
2021-05-27 19:00:48 +03:00
|
|
|
|
interface IRecentUser {
|
2021-07-02 01:23:03 +03:00
|
|
|
|
userId: string;
|
|
|
|
|
user: RoomMember;
|
|
|
|
|
lastActive: number;
|
2021-05-27 19:00:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-17 00:40:12 +03:00
|
|
|
|
export const KIND_DM = "dm";
|
|
|
|
|
export const KIND_INVITE = "invite";
|
2021-07-15 11:55:58 +03:00
|
|
|
|
// NB. This dialog needs the 'mx_InviteDialog_transferWrapper' wrapper class to have the correct
|
|
|
|
|
// padding on the bottom (because all modals have 24px padding on all sides), so this needs to
|
|
|
|
|
// be passed when creating the modal
|
2020-12-15 17:59:06 +03:00
|
|
|
|
export const KIND_CALL_TRANSFER = "call_transfer";
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2021-07-15 11:55:58 +03:00
|
|
|
|
enum TabId {
|
|
|
|
|
UserDirectory = 'users',
|
|
|
|
|
DialPad = 'dialpad',
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-24 13:12:32 +03:00
|
|
|
|
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
|
|
|
|
|
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
|
|
|
|
// for 3PIDs/email addresses.
|
|
|
|
|
export 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).
|
|
|
|
|
*/
|
|
|
|
|
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).
|
|
|
|
|
*/
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
public abstract getMxcAvatarUrl(): string;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-07 06:51:23 +03:00
|
|
|
|
class DirectoryMember extends Member {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private readonly _userId: string;
|
2021-07-10 17:43:46 +03:00
|
|
|
|
private readonly displayName?: string;
|
|
|
|
|
private readonly avatarUrl?: string;
|
2020-01-04 07:17:48 +03:00
|
|
|
|
|
2021-06-24 12:03:32 +03:00
|
|
|
|
// eslint-disable-next-line camelcase
|
2021-07-10 17:43:46 +03:00
|
|
|
|
constructor(userDirResult: { user_id: string, display_name?: string, avatar_url?: string }) {
|
2020-01-07 06:51:23 +03:00
|
|
|
|
super();
|
2020-01-04 07:17:48 +03:00
|
|
|
|
this._userId = userDirResult.user_id;
|
2021-05-26 18:47:46 +03:00
|
|
|
|
this.displayName = userDirResult.display_name;
|
|
|
|
|
this.avatarUrl = userDirResult.avatar_url;
|
2020-01-04 07:17:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-07 06:51:23 +03:00
|
|
|
|
// These next class members are for the Member interface
|
2020-01-04 07:17:48 +03:00
|
|
|
|
get name(): string {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
return this.displayName || this._userId;
|
2020-01-04 07:17:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get userId(): string {
|
|
|
|
|
return this._userId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMxcAvatarUrl(): string {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
return this.avatarUrl;
|
2020-01-04 07:17:48 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-09 06:49:29 +03:00
|
|
|
|
class ThreepidMember extends Member {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private readonly id: string;
|
2020-01-09 06:49:29 +03:00
|
|
|
|
|
|
|
|
|
constructor(id: string) {
|
|
|
|
|
super();
|
2021-05-26 18:47:46 +03:00
|
|
|
|
this.id = id;
|
2020-01-09 06:49:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
return this.id.includes('@');
|
2020-01-09 06:49:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// These next class members are for the Member interface
|
|
|
|
|
get name(): string {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
return this.id;
|
2020-01-09 06:49:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get userId(): string {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
return this.id;
|
2020-01-09 06:49:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMxcAvatarUrl(): string {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-15 01:28:21 +03:00
|
|
|
|
interface IDMUserTileProps {
|
2021-06-17 16:06:03 +03:00
|
|
|
|
member: Member;
|
|
|
|
|
onRemove(member: Member): void;
|
2020-12-15 01:28:21 +03:00
|
|
|
|
}
|
2020-01-07 06:51:23 +03:00
|
|
|
|
|
2020-12-15 01:28:21 +03:00
|
|
|
|
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private onRemove = (e) => {
|
2020-01-07 06:51:23 +03:00
|
|
|
|
// Stop the browser from highlighting text
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
this.props.onRemove(this.props.member);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
const avatarSize = 20;
|
2021-06-17 16:06:03 +03:00
|
|
|
|
const avatar = (this.props.member as ThreepidMember).isEmail
|
2020-01-07 06:51:23 +03:00
|
|
|
|
? <img
|
2020-01-16 23:42:34 +03:00
|
|
|
|
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
|
2020-01-07 06:51:23 +03:00
|
|
|
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
2021-07-23 12:23:45 +03:00
|
|
|
|
width={avatarSize}
|
|
|
|
|
height={avatarSize}
|
|
|
|
|
/>
|
2020-01-07 06:51:23 +03:00
|
|
|
|
: <BaseAvatar
|
2020-01-16 23:42:34 +03:00
|
|
|
|
className='mx_InviteDialog_userTile_avatar'
|
2021-03-06 04:45:09 +03:00
|
|
|
|
url={this.props.member.getMxcAvatarUrl()
|
|
|
|
|
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
|
|
|
|
|
: null}
|
2020-01-07 06:51:23 +03:00
|
|
|
|
name={this.props.member.name}
|
|
|
|
|
idName={this.props.member.userId}
|
|
|
|
|
width={avatarSize}
|
|
|
|
|
height={avatarSize} />;
|
|
|
|
|
|
2020-03-19 00:16:41 +03:00
|
|
|
|
let closeButton;
|
|
|
|
|
if (this.props.onRemove) {
|
|
|
|
|
closeButton = (
|
2020-01-07 06:51:23 +03:00
|
|
|
|
<AccessibleButton
|
2020-01-16 23:42:34 +03:00
|
|
|
|
className='mx_InviteDialog_userTile_remove'
|
2021-05-26 18:47:46 +03:00
|
|
|
|
onClick={this.onRemove}
|
2020-01-07 06:51:23 +03:00
|
|
|
|
>
|
2021-07-23 12:23:45 +03:00
|
|
|
|
<img
|
|
|
|
|
src={require("../../../../res/img/icon-pill-remove.svg")}
|
|
|
|
|
alt={_t('Remove')}
|
|
|
|
|
width={8}
|
|
|
|
|
height={8}
|
2020-12-15 01:28:21 +03:00
|
|
|
|
/>
|
2020-01-07 06:51:23 +03:00
|
|
|
|
</AccessibleButton>
|
2020-03-19 00:16:41 +03:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<span className='mx_InviteDialog_userTile'>
|
|
|
|
|
<span className='mx_InviteDialog_userTile_pill'>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ avatar }
|
|
|
|
|
<span className='mx_InviteDialog_userTile_name'>{ this.props.member.name }</span>
|
2020-03-19 00:16:41 +03:00
|
|
|
|
</span>
|
|
|
|
|
{ closeButton }
|
2020-01-07 06:51:23 +03:00
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-15 01:28:21 +03:00
|
|
|
|
interface IDMRoomTileProps {
|
2021-06-17 16:06:03 +03:00
|
|
|
|
member: Member;
|
2020-12-15 01:28:21 +03:00
|
|
|
|
lastActiveTs: number;
|
2021-06-17 16:06:03 +03:00
|
|
|
|
onToggle(member: Member): void;
|
2020-12-15 01:28:21 +03:00
|
|
|
|
highlightWord: string;
|
|
|
|
|
isSelected: boolean;
|
|
|
|
|
}
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
2020-12-15 01:28:21 +03:00
|
|
|
|
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private onClick = (e) => {
|
2020-01-03 03:40:18 +03:00
|
|
|
|
// Stop the browser from highlighting text
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
2020-01-07 06:51:23 +03:00
|
|
|
|
this.props.onToggle(this.props.member);
|
2020-01-03 03:40:18 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private highlightName(str: string) {
|
2020-01-04 06:35:30 +03:00
|
|
|
|
if (!this.props.highlightWord) return str;
|
|
|
|
|
|
|
|
|
|
// We convert things to lowercase for index searching, but pull substrings from
|
2020-01-06 22:21:59 +03:00
|
|
|
|
// the submitted text to preserve case. Note: we don't need to htmlEntities the
|
|
|
|
|
// string because React will safely encode the text for us.
|
2020-01-04 06:35:30 +03:00
|
|
|
|
const lowerStr = str.toLowerCase();
|
|
|
|
|
const filterStr = this.props.highlightWord.toLowerCase();
|
|
|
|
|
|
|
|
|
|
const result = [];
|
|
|
|
|
|
|
|
|
|
let i = 0;
|
|
|
|
|
let ii;
|
|
|
|
|
while ((ii = lowerStr.indexOf(filterStr, i)) >= 0) {
|
|
|
|
|
// Push any text we missed (first bit/middle of text)
|
|
|
|
|
if (ii > i) {
|
2020-01-06 22:21:59 +03:00
|
|
|
|
// Push any text we aren't highlighting (middle of text match, or beginning of text)
|
2021-07-20 00:43:11 +03:00
|
|
|
|
result.push(<span key={i + 'begin'}>{ str.substring(i, ii) }</span>);
|
2020-01-04 06:35:30 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i = ii; // copy over ii only if we have a match (to preserve i for end-of-text matching)
|
|
|
|
|
|
|
|
|
|
// Highlight the word the user entered
|
|
|
|
|
const substr = str.substring(i, filterStr.length + i);
|
2021-07-20 00:43:11 +03:00
|
|
|
|
result.push(<span className='mx_InviteDialog_roomTile_highlight' key={i + 'bold'}>{ substr }</span>);
|
2020-01-04 06:35:30 +03:00
|
|
|
|
i += substr.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Push any text we missed (end of text)
|
2020-02-29 00:08:43 +03:00
|
|
|
|
if (i < str.length) {
|
2021-07-20 00:43:11 +03:00
|
|
|
|
result.push(<span key={i + 'end'}>{ str.substring(i) }</span>);
|
2020-01-04 06:35:30 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-03 03:40:18 +03:00
|
|
|
|
render() {
|
|
|
|
|
let timestamp = null;
|
|
|
|
|
if (this.props.lastActiveTs) {
|
2020-01-14 07:20:01 +03:00
|
|
|
|
const humanTs = humanizeTime(this.props.lastActiveTs);
|
2021-07-20 00:43:11 +03:00
|
|
|
|
timestamp = <span className='mx_InviteDialog_roomTile_time'>{ humanTs }</span>;
|
2020-01-03 03:40:18 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-04 07:17:48 +03:00
|
|
|
|
const avatarSize = 36;
|
2021-06-17 16:06:03 +03:00
|
|
|
|
const avatar = (this.props.member as ThreepidMember).isEmail
|
2020-01-07 06:51:23 +03:00
|
|
|
|
? <img
|
|
|
|
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
2021-07-23 12:23:45 +03:00
|
|
|
|
width={avatarSize}
|
|
|
|
|
height={avatarSize}
|
|
|
|
|
/>
|
2020-01-07 06:51:23 +03:00
|
|
|
|
: <BaseAvatar
|
2021-03-06 04:45:09 +03:00
|
|
|
|
url={this.props.member.getMxcAvatarUrl()
|
|
|
|
|
? mediaFromMxc(this.props.member.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize)
|
|
|
|
|
: null}
|
2020-01-07 06:51:23 +03:00
|
|
|
|
name={this.props.member.name}
|
|
|
|
|
idName={this.props.member.userId}
|
|
|
|
|
width={avatarSize}
|
|
|
|
|
height={avatarSize} />;
|
2020-01-04 07:17:48 +03:00
|
|
|
|
|
2020-01-07 22:12:31 +03:00
|
|
|
|
let checkmark = null;
|
|
|
|
|
if (this.props.isSelected) {
|
|
|
|
|
// To reduce flickering we put the 'selected' room tile above the real avatar
|
2020-01-16 23:42:34 +03:00
|
|
|
|
checkmark = <div className='mx_InviteDialog_roomTile_selected' />;
|
2020-01-07 22:12:31 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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).
|
2020-01-07 22:20:02 +03:00
|
|
|
|
const stackedAvatar = (
|
2020-01-16 23:42:34 +03:00
|
|
|
|
<span className='mx_InviteDialog_roomTile_avatarStack'>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ avatar }
|
|
|
|
|
{ checkmark }
|
2020-01-07 22:12:31 +03:00
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
|
2021-06-17 16:06:03 +03:00
|
|
|
|
const caption = (this.props.member as ThreepidMember).isEmail
|
2020-11-03 17:56:06 +03:00
|
|
|
|
? _t("Invite by email")
|
2021-05-26 18:47:46 +03:00
|
|
|
|
: this.highlightName(this.props.member.userId);
|
2020-11-03 17:56:06 +03:00
|
|
|
|
|
2020-01-03 03:40:18 +03:00
|
|
|
|
return (
|
2021-05-26 18:47:46 +03:00
|
|
|
|
<div className='mx_InviteDialog_roomTile' onClick={this.onClick}>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ stackedAvatar }
|
2020-11-03 17:56:06 +03:00
|
|
|
|
<span className="mx_InviteDialog_roomTile_nameStack">
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<div className='mx_InviteDialog_roomTile_name'>{ this.highlightName(this.props.member.name) }</div>
|
|
|
|
|
<div className='mx_InviteDialog_roomTile_userId'>{ caption }</div>
|
2020-11-03 17:56:06 +03:00
|
|
|
|
</span>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ timestamp }
|
2020-01-03 03:40:18 +03:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-15 01:28:21 +03:00
|
|
|
|
interface IInviteDialogProps {
|
|
|
|
|
// Takes an array of user IDs/emails to invite.
|
2021-05-26 18:47:46 +03:00
|
|
|
|
onFinished: (toInvite?: string[]) => void;
|
2020-01-17 00:40:12 +03:00
|
|
|
|
|
2020-12-15 01:28:21 +03:00
|
|
|
|
// The kind of invite being performed. Assumed to be KIND_DM if
|
|
|
|
|
// not provided.
|
2021-07-02 01:23:03 +03:00
|
|
|
|
kind: string;
|
2020-01-17 00:40:12 +03:00
|
|
|
|
|
2021-03-03 14:34:29 +03:00
|
|
|
|
// The room ID this dialog is for. Only required for KIND_INVITE.
|
2021-07-02 01:23:03 +03:00
|
|
|
|
roomId: string;
|
2020-11-11 16:36:17 +03:00
|
|
|
|
|
2020-12-15 17:59:06 +03:00
|
|
|
|
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
2021-07-02 01:23:03 +03:00
|
|
|
|
call: MatrixCall;
|
2020-12-15 17:59:06 +03:00
|
|
|
|
|
2020-12-15 01:28:21 +03:00
|
|
|
|
// Initial value to populate the filter with
|
2021-07-02 01:23:03 +03:00
|
|
|
|
initialText: string;
|
2020-12-15 01:28:21 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface IInviteDialogState {
|
2021-06-17 16:06:03 +03:00
|
|
|
|
targets: Member[]; // array of Member objects (see interface above)
|
2020-12-15 01:28:21 +03:00
|
|
|
|
filterText: string;
|
2020-12-15 01:51:40 +03:00
|
|
|
|
recents: { user: Member, userId: string }[];
|
2020-12-15 01:28:21 +03:00
|
|
|
|
numRecentsShown: number;
|
2020-12-15 01:51:40 +03:00
|
|
|
|
suggestions: { user: Member, userId: string }[];
|
2020-12-15 01:28:21 +03:00
|
|
|
|
numSuggestionsShown: number;
|
2020-12-15 01:51:40 +03:00
|
|
|
|
serverResultsMixin: { user: Member, userId: string }[];
|
|
|
|
|
threepidResultsMixin: { user: Member, userId: string}[];
|
2020-12-15 01:28:21 +03:00
|
|
|
|
canUseIdentityServer: boolean;
|
|
|
|
|
tryingIdentityServer: boolean;
|
2021-03-25 22:56:21 +03:00
|
|
|
|
consultFirst: boolean;
|
2021-07-15 11:55:58 +03:00
|
|
|
|
dialPadValue: string;
|
|
|
|
|
currentTabId: TabId;
|
2020-12-15 01:28:21 +03:00
|
|
|
|
|
|
|
|
|
// These two flags are used for the 'Go' button to communicate what is going on.
|
2021-07-02 01:23:03 +03:00
|
|
|
|
busy: boolean;
|
|
|
|
|
errorText: string;
|
2020-12-15 01:28:21 +03:00
|
|
|
|
}
|
2020-01-17 00:40:12 +03:00
|
|
|
|
|
2021-03-09 05:59:41 +03:00
|
|
|
|
@replaceableComponent("views.dialogs.InviteDialog")
|
2020-12-15 01:28:21 +03:00
|
|
|
|
export default class InviteDialog extends React.PureComponent<IInviteDialogProps, IInviteDialogState> {
|
2020-01-17 00:40:12 +03:00
|
|
|
|
static defaultProps = {
|
|
|
|
|
kind: KIND_DM,
|
2020-11-11 16:36:17 +03:00
|
|
|
|
initialText: "",
|
2020-01-03 03:40:18 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-27 17:51:25 +03:00
|
|
|
|
private closeCopiedTooltip: () => void;
|
2021-07-12 11:02:46 +03:00
|
|
|
|
private debounceTimer: number = null; // actually number because we're in the browser
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private editorRef = createRef<HTMLInputElement>();
|
2021-05-26 18:38:02 +03:00
|
|
|
|
private unmounted = false;
|
2020-01-04 07:17:48 +03:00
|
|
|
|
|
2020-01-17 00:40:12 +03:00
|
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
|
2021-03-03 14:34:29 +03:00
|
|
|
|
if ((props.kind === KIND_INVITE) && !props.roomId) {
|
|
|
|
|
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
|
2020-12-15 17:59:06 +03:00
|
|
|
|
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
|
|
|
|
|
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
|
2020-01-17 00:40:12 +03:00
|
|
|
|
}
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
2020-01-30 22:27:59 +03:00
|
|
|
|
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
|
2020-01-17 00:40:25 +03:00
|
|
|
|
if (props.roomId) {
|
|
|
|
|
const room = MatrixClientPeg.get().getRoom(props.roomId);
|
|
|
|
|
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
2020-01-30 22:27:59 +03:00
|
|
|
|
room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
|
|
|
|
|
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
|
|
|
|
|
// add banned users, so we don't try to invite them
|
|
|
|
|
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
|
2020-10-29 18:53:14 +03:00
|
|
|
|
|
|
|
|
|
CountlyAnalytics.instance.trackBeginInvite(props.roomId);
|
2020-01-17 00:40:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-03 03:40:18 +03:00
|
|
|
|
this.state = {
|
2020-01-07 06:51:23 +03:00
|
|
|
|
targets: [], // array of Member objects (see interface above)
|
2020-11-11 16:36:17 +03:00
|
|
|
|
filterText: this.props.initialText,
|
2020-08-26 06:02:32 +03:00
|
|
|
|
recents: InviteDialog.buildRecents(alreadyInvited),
|
2020-01-03 03:40:18 +03:00
|
|
|
|
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
2021-05-26 18:47:46 +03:00
|
|
|
|
suggestions: this.buildSuggestions(alreadyInvited),
|
2020-01-04 05:41:06 +03:00
|
|
|
|
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
2020-12-15 01:28:21 +03:00
|
|
|
|
serverResultsMixin: [],
|
|
|
|
|
threepidResultsMixin: [],
|
2020-01-09 06:49:29 +03:00
|
|
|
|
canUseIdentityServer: !!MatrixClientPeg.get().getIdentityServerUrl(),
|
|
|
|
|
tryingIdentityServer: false,
|
2021-03-25 22:56:21 +03:00
|
|
|
|
consultFirst: false,
|
2021-07-15 11:55:58 +03:00
|
|
|
|
dialPadValue: '',
|
|
|
|
|
currentTabId: TabId.UserDirectory,
|
2020-01-15 09:32:00 +03:00
|
|
|
|
|
|
|
|
|
// These two flags are used for the 'Go' button to communicate what is going on.
|
2020-01-15 09:35:45 +03:00
|
|
|
|
busy: false,
|
|
|
|
|
errorText: null,
|
2020-01-03 03:40:18 +03:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-11 16:36:17 +03:00
|
|
|
|
componentDidMount() {
|
|
|
|
|
if (this.props.initialText) {
|
2021-05-26 18:47:46 +03:00
|
|
|
|
this.updateSuggestions(this.props.initialText);
|
2020-11-11 16:36:17 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-26 18:38:02 +03:00
|
|
|
|
componentWillUnmount() {
|
|
|
|
|
this.unmounted = true;
|
2021-05-27 17:51:25 +03:00
|
|
|
|
// 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
|
|
|
|
|
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-25 22:56:21 +03:00
|
|
|
|
private onConsultFirstChange = (ev) => {
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ consultFirst: ev.target.checked });
|
|
|
|
|
};
|
2021-03-25 22:56:21 +03:00
|
|
|
|
|
2021-05-27 19:00:48 +03:00
|
|
|
|
public static buildRecents(excludedTargetIds: Set<string>): IRecentUser[] {
|
2020-02-25 05:43:11 +03:00
|
|
|
|
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
|
|
|
|
|
2020-03-20 23:38:20 +03:00
|
|
|
|
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
2020-02-25 05:43:11 +03:00
|
|
|
|
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
|
2020-08-03 18:42:54 +03:00
|
|
|
|
const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM] || [];
|
2020-02-25 05:43:11 +03:00
|
|
|
|
const myUserId = MatrixClientPeg.get().getUserId();
|
|
|
|
|
for (const dmRoom of dmTaggedRooms) {
|
|
|
|
|
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
|
|
|
|
|
for (const member of otherMembers) {
|
|
|
|
|
if (rooms[member.userId]) continue; // already have a room
|
|
|
|
|
|
|
|
|
|
console.warn(`Adding DM room for ${member.userId} as ${dmRoom.roomId} from tag, not DM map`);
|
|
|
|
|
rooms[member.userId] = dmRoom;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-03 03:40:18 +03:00
|
|
|
|
const recents = [];
|
|
|
|
|
for (const userId in rooms) {
|
2020-01-17 00:40:25 +03:00
|
|
|
|
// Filter out user IDs that are already in the room / should be excluded
|
2020-01-30 22:27:59 +03:00
|
|
|
|
if (excludedTargetIds.has(userId)) {
|
2020-01-24 03:35:36 +03:00
|
|
|
|
console.warn(`[Invite:Recents] Excluding ${userId} from recents`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2020-01-17 00:40:25 +03:00
|
|
|
|
|
2020-01-03 03:40:18 +03:00
|
|
|
|
const room = rooms[userId];
|
|
|
|
|
const member = room.getMember(userId);
|
2020-01-24 03:35:36 +03:00
|
|
|
|
if (!member) {
|
|
|
|
|
// just skip people who don't have memberships for some reason
|
|
|
|
|
console.warn(`[Invite:Recents] ${userId} is missing a member object in their own DM (${room.roomId})`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
2020-01-29 14:14:33 +03:00
|
|
|
|
// Find the last timestamp for a message event
|
|
|
|
|
const searchTypes = ["m.room.message", "m.room.encrypted", "m.sticker"];
|
|
|
|
|
const maxSearchEvents = 20; // to prevent traversing history
|
|
|
|
|
let lastEventTs = 0;
|
|
|
|
|
if (room.timeline && room.timeline.length) {
|
|
|
|
|
for (let i = room.timeline.length - 1; i >= 0; i--) {
|
|
|
|
|
const ev = room.timeline[i];
|
|
|
|
|
if (searchTypes.includes(ev.getType())) {
|
|
|
|
|
lastEventTs = ev.getTs();
|
2020-01-29 14:23:48 +03:00
|
|
|
|
break;
|
2020-01-29 14:14:33 +03:00
|
|
|
|
}
|
|
|
|
|
if (room.timeline.length - i > maxSearchEvents) break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-01-24 03:35:36 +03:00
|
|
|
|
if (!lastEventTs) {
|
|
|
|
|
// something weird is going on with this room
|
|
|
|
|
console.warn(`[Invite:Recents] ${userId} (${room.roomId}) has a weird last timestamp: ${lastEventTs}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
2021-06-29 15:11:58 +03:00
|
|
|
|
recents.push({ userId, user: member, lastActive: lastEventTs });
|
2020-01-03 03:40:18 +03:00
|
|
|
|
}
|
2020-01-24 03:35:36 +03:00
|
|
|
|
if (!recents) console.warn("[Invite:Recents] No recents to suggest!");
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
|
|
|
|
// Sort the recents by last active to save us time later
|
|
|
|
|
recents.sort((a, b) => b.lastActive - a.lastActive);
|
|
|
|
|
|
|
|
|
|
return recents;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
2020-01-04 05:41:06 +03:00
|
|
|
|
const maxConsideredMembers = 200;
|
2020-01-31 00:01:55 +03:00
|
|
|
|
const joinedRooms = MatrixClientPeg.get().getRooms()
|
|
|
|
|
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
|
2020-01-06 22:23:35 +03:00
|
|
|
|
|
|
|
|
|
// Generates { userId: {member, rooms[]} }
|
2020-01-04 05:41:06 +03:00
|
|
|
|
const memberRooms = joinedRooms.reduce((members, room) => {
|
2020-01-14 07:04:13 +03:00
|
|
|
|
// Filter out DMs (we'll handle these in the recents section)
|
2020-01-14 07:21:18 +03:00
|
|
|
|
if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) {
|
2020-01-14 07:04:13 +03:00
|
|
|
|
return members; // Do nothing
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-30 22:27:59 +03:00
|
|
|
|
const joinedMembers = room.getJoinedMembers().filter(u => !excludedTargetIds.has(u.userId));
|
2020-01-04 05:41:06 +03:00
|
|
|
|
for (const member of joinedMembers) {
|
2020-01-17 00:40:25 +03:00
|
|
|
|
// Filter out user IDs that are already in the room / should be excluded
|
2020-01-30 22:27:59 +03:00
|
|
|
|
if (excludedTargetIds.has(member.userId)) {
|
2020-01-17 00:40:25 +03:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-04 05:41:06 +03:00
|
|
|
|
if (!members[member.userId]) {
|
|
|
|
|
members[member.userId] = {
|
|
|
|
|
member: member,
|
|
|
|
|
// Track the room size of the 'picked' member so we can use the profile of
|
|
|
|
|
// the smallest room (likely a DM).
|
|
|
|
|
pickedMemberRoomSize: room.getJoinedMemberCount(),
|
|
|
|
|
rooms: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
members[member.userId].rooms.push(room);
|
|
|
|
|
|
|
|
|
|
if (room.getJoinedMemberCount() < members[member.userId].pickedMemberRoomSize) {
|
|
|
|
|
members[member.userId].member = member;
|
|
|
|
|
members[member.userId].pickedMemberRoomSize = room.getJoinedMemberCount();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return members;
|
2020-01-06 22:23:35 +03:00
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
// Generates { userId: {member, numRooms, score} }
|
2020-12-15 01:28:21 +03:00
|
|
|
|
const memberScores = Object.values(memberRooms).reduce((scores, entry: {member: RoomMember, rooms: Room[]}) => {
|
2020-01-04 05:41:06 +03:00
|
|
|
|
const numMembersTotal = entry.rooms.reduce((c, r) => c + r.getJoinedMemberCount(), 0);
|
|
|
|
|
const maxRange = maxConsideredMembers * entry.rooms.length;
|
|
|
|
|
scores[entry.member.userId] = {
|
|
|
|
|
member: entry.member,
|
|
|
|
|
numRooms: entry.rooms.length,
|
|
|
|
|
score: Math.max(0, Math.pow(1 - (numMembersTotal / maxRange), 5)),
|
|
|
|
|
};
|
|
|
|
|
return scores;
|
2020-01-06 22:23:35 +03:00
|
|
|
|
}, {});
|
|
|
|
|
|
2020-01-18 00:36:23 +03:00
|
|
|
|
// Now that we have scores for being in rooms, boost those people who have sent messages
|
|
|
|
|
// recently, as a way to improve the quality of suggestions. We do this by checking every
|
|
|
|
|
// room to see who has sent a message in the last few hours, and giving them a score
|
|
|
|
|
// which correlates to the freshness of their message. In theory, this results in suggestions
|
|
|
|
|
// which are closer to "continue this conversation" rather than "this person exists".
|
2020-01-31 00:07:32 +03:00
|
|
|
|
const trueJoinedRooms = MatrixClientPeg.get().getRooms().filter(r => r.getMyMembership() === 'join');
|
2020-01-18 00:36:23 +03:00
|
|
|
|
const now = (new Date()).getTime();
|
2020-01-20 19:29:33 +03:00
|
|
|
|
const earliestAgeConsidered = now - (60 * 60 * 1000); // 1 hour ago
|
2020-01-18 00:36:23 +03:00
|
|
|
|
const maxMessagesConsidered = 50; // so we don't iterate over a huge amount of traffic
|
|
|
|
|
const lastSpoke = {}; // userId: timestamp
|
|
|
|
|
const lastSpokeMembers = {}; // userId: room member
|
|
|
|
|
for (const room of trueJoinedRooms) {
|
|
|
|
|
// Skip low priority rooms and DMs
|
2020-01-18 00:40:33 +03:00
|
|
|
|
const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
|
|
|
|
if (Object.keys(room.tags).includes("m.lowpriority") || isDm) {
|
2020-01-18 00:36:23 +03:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
|
|
|
|
|
for (let i = events.length - 1; i >= Math.max(0, events.length - maxMessagesConsidered); i--) {
|
|
|
|
|
const ev = events[i];
|
2020-01-30 22:27:59 +03:00
|
|
|
|
if (excludedTargetIds.has(ev.getSender())) {
|
2020-01-18 00:36:23 +03:00
|
|
|
|
continue;
|
|
|
|
|
}
|
2020-01-20 19:29:33 +03:00
|
|
|
|
if (ev.getTs() <= earliestAgeConsidered) {
|
2020-01-18 00:36:23 +03:00
|
|
|
|
break; // give up: all events from here on out are too old
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!lastSpoke[ev.getSender()] || lastSpoke[ev.getSender()] < ev.getTs()) {
|
|
|
|
|
lastSpoke[ev.getSender()] = ev.getTs();
|
|
|
|
|
lastSpokeMembers[ev.getSender()] = room.getMember(ev.getSender());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const userId in lastSpoke) {
|
|
|
|
|
const ts = lastSpoke[userId];
|
|
|
|
|
const member = lastSpokeMembers[userId];
|
|
|
|
|
if (!member) continue; // skip people we somehow don't have profiles for
|
|
|
|
|
|
|
|
|
|
// Scores from being in a room give a 'good' score of about 1.0-1.5, so for our
|
|
|
|
|
// boost we'll try and award at least +1.0 for making the list, with +4.0 being
|
|
|
|
|
// an approximate maximum for being selected.
|
|
|
|
|
const distanceFromNow = Math.abs(now - ts); // abs to account for slight future messages
|
2020-01-20 19:29:33 +03:00
|
|
|
|
const inverseTime = (now - earliestAgeConsidered) - distanceFromNow;
|
2020-01-18 00:36:23 +03:00
|
|
|
|
const scoreBoost = Math.max(1, inverseTime / (15 * 60 * 1000)); // 15min segments to keep scores sane
|
|
|
|
|
|
|
|
|
|
let record = memberScores[userId];
|
2021-06-29 15:11:58 +03:00
|
|
|
|
if (!record) record = memberScores[userId] = { score: 0 };
|
2020-01-18 00:36:23 +03:00
|
|
|
|
record.member = member;
|
|
|
|
|
record.score += scoreBoost;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-04 05:41:06 +03:00
|
|
|
|
const members = Object.values(memberScores);
|
|
|
|
|
members.sort((a, b) => {
|
|
|
|
|
if (a.score === b.score) {
|
|
|
|
|
if (a.numRooms === b.numRooms) {
|
2021-06-02 12:42:17 +03:00
|
|
|
|
return compare(a.member.userId, b.member.userId);
|
2020-01-04 05:41:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return b.numRooms - a.numRooms;
|
|
|
|
|
}
|
|
|
|
|
return b.score - a.score;
|
|
|
|
|
});
|
2020-01-14 07:04:13 +03:00
|
|
|
|
|
2021-06-29 15:11:58 +03:00
|
|
|
|
return members.map(m => ({ userId: m.member.userId, user: m.member }));
|
2020-01-04 05:41:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-16 13:48:14 +03:00
|
|
|
|
private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean {
|
|
|
|
|
this.setState({ busy: false });
|
2021-06-24 12:03:32 +03:00
|
|
|
|
const userMap = new Map<string, Member>(this.state.targets.map(member => [member.userId, member]));
|
|
|
|
|
return !showAnyInviteErrors(result.states, room, result.inviter, userMap);
|
2020-01-17 00:40:12 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private convertFilter(): Member[] {
|
2020-02-21 09:16:21 +03:00
|
|
|
|
// Check to see if there's anything to convert first
|
|
|
|
|
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
2020-02-21 09:10:43 +03:00
|
|
|
|
|
|
|
|
|
let newMember: Member;
|
|
|
|
|
if (this.state.filterText.startsWith('@')) {
|
|
|
|
|
// Assume mxid
|
2021-06-29 15:11:58 +03:00
|
|
|
|
newMember = new DirectoryMember({ user_id: this.state.filterText, display_name: null, avatar_url: null });
|
2020-09-16 16:45:34 +03:00
|
|
|
|
} else if (SettingsStore.getValue(UIFeature.IdentityServer)) {
|
2020-02-21 09:10:43 +03:00
|
|
|
|
// Assume email
|
|
|
|
|
newMember = new ThreepidMember(this.state.filterText);
|
|
|
|
|
}
|
2020-02-21 09:16:21 +03:00
|
|
|
|
const newTargets = [...(this.state.targets || []), newMember];
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ targets: newTargets, filterText: '' });
|
2020-02-21 09:10:43 +03:00
|
|
|
|
return newTargets;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private startDm = async () => {
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ busy: true });
|
2021-04-14 11:03:47 +03:00
|
|
|
|
const client = MatrixClientPeg.get();
|
2021-05-26 18:47:46 +03:00
|
|
|
|
const targets = this.convertFilter();
|
2020-02-21 09:10:43 +03:00
|
|
|
|
const targetIds = targets.map(t => t.userId);
|
2020-01-15 09:32:00 +03:00
|
|
|
|
|
|
|
|
|
// Check if there is already a DM with these people and reuse it if possible.
|
2020-11-14 01:19:34 +03:00
|
|
|
|
let existingRoom: Room;
|
|
|
|
|
if (targetIds.length === 1) {
|
2021-04-14 11:03:47 +03:00
|
|
|
|
existingRoom = findDMForUser(client, targetIds[0]);
|
2020-11-14 01:19:34 +03:00
|
|
|
|
} else {
|
|
|
|
|
existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
|
|
|
|
}
|
2020-01-15 09:32:00 +03:00
|
|
|
|
if (existingRoom) {
|
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: 'view_room',
|
|
|
|
|
room_id: existingRoom.roomId,
|
|
|
|
|
should_peek: false,
|
|
|
|
|
joining: false,
|
|
|
|
|
});
|
|
|
|
|
this.props.onFinished();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-29 15:11:58 +03:00
|
|
|
|
const createRoomOptions = { inlineErrors: true } as any; // XXX: Type out `createRoomOptions`
|
2020-01-23 19:00:55 +03:00
|
|
|
|
|
2020-05-19 13:36:44 +03:00
|
|
|
|
if (privateShouldBeEncrypted()) {
|
2020-01-23 19:00:55 +03:00
|
|
|
|
// Check whether all users have uploaded device keys before.
|
|
|
|
|
// If so, enable encryption in the new room.
|
2020-04-28 19:38:54 +03:00
|
|
|
|
const has3PidMembers = targets.some(t => t instanceof ThreepidMember);
|
2020-04-29 12:40:04 +03:00
|
|
|
|
if (!has3PidMembers) {
|
2020-04-28 18:41:10 +03:00
|
|
|
|
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
|
|
|
|
if (allHaveDeviceKeys) {
|
|
|
|
|
createRoomOptions.encryption = true;
|
|
|
|
|
}
|
2020-01-23 19:00:55 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-15 09:32:00 +03:00
|
|
|
|
// Check if it's a traditional DM and create the room if required.
|
|
|
|
|
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
2021-04-14 10:44:33 +03:00
|
|
|
|
try {
|
2021-04-14 11:03:47 +03:00
|
|
|
|
const isSelf = targetIds.length === 1 && targetIds[0] === client.getUserId();
|
2021-04-14 10:44:33 +03:00
|
|
|
|
if (targetIds.length === 1 && !isSelf) {
|
|
|
|
|
createRoomOptions.dmUserId = targetIds[0];
|
2021-04-14 11:37:06 +03:00
|
|
|
|
}
|
2021-04-14 21:59:17 +03:00
|
|
|
|
|
|
|
|
|
if (targetIds.length > 1) {
|
|
|
|
|
createRoomOptions.createOpts = targetIds.reduce(
|
|
|
|
|
(roomOptions, address) => {
|
2021-04-15 16:13:37 +03:00
|
|
|
|
const type = getAddressType(address);
|
|
|
|
|
if (type === 'email') {
|
2021-04-14 21:59:17 +03:00
|
|
|
|
const invite: IInvite3PID = {
|
|
|
|
|
id_server: client.getIdentityServerUrl(true),
|
|
|
|
|
medium: 'email',
|
|
|
|
|
address,
|
|
|
|
|
};
|
|
|
|
|
roomOptions.invite_3pid.push(invite);
|
2021-04-15 16:13:37 +03:00
|
|
|
|
} else if (type === 'mx-user-id') {
|
2021-04-14 21:59:17 +03:00
|
|
|
|
roomOptions.invite.push(address);
|
|
|
|
|
}
|
|
|
|
|
return roomOptions;
|
|
|
|
|
},
|
|
|
|
|
{ invite: [], invite_3pid: [] },
|
2021-06-29 15:11:58 +03:00
|
|
|
|
);
|
2021-04-14 10:44:33 +03:00
|
|
|
|
}
|
2021-04-14 21:59:17 +03:00
|
|
|
|
|
|
|
|
|
await createRoom(createRoomOptions);
|
|
|
|
|
this.props.onFinished();
|
2021-04-14 10:44:33 +03:00
|
|
|
|
} catch (err) {
|
2020-01-15 09:32:00 +03:00
|
|
|
|
console.error(err);
|
|
|
|
|
this.setState({
|
|
|
|
|
busy: false,
|
2021-03-25 13:11:52 +03:00
|
|
|
|
errorText: _t("We couldn't create your DM."),
|
2020-01-15 09:32:00 +03:00
|
|
|
|
});
|
2021-04-14 10:44:33 +03:00
|
|
|
|
}
|
2020-01-03 03:40:18 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private inviteUsers = async () => {
|
2020-10-29 18:53:14 +03:00
|
|
|
|
const startTime = CountlyAnalytics.getTimestamp();
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ busy: true });
|
2021-05-26 18:47:46 +03:00
|
|
|
|
this.convertFilter();
|
|
|
|
|
const targets = this.convertFilter();
|
2020-02-21 09:10:43 +03:00
|
|
|
|
const targetIds = targets.map(t => t.userId);
|
2020-01-17 00:40:12 +03:00
|
|
|
|
|
2021-03-16 05:56:56 +03:00
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
const room = cli.getRoom(this.props.roomId);
|
2020-01-17 00:40:12 +03:00
|
|
|
|
if (!room) {
|
|
|
|
|
console.error("Failed to find the room to invite users to");
|
|
|
|
|
this.setState({
|
|
|
|
|
busy: false,
|
|
|
|
|
errorText: _t("Something went wrong trying to invite the users."),
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-16 05:56:56 +03:00
|
|
|
|
try {
|
2021-06-29 15:11:58 +03:00
|
|
|
|
const result = await inviteMultipleToRoom(this.props.roomId, targetIds);
|
2020-10-29 18:53:14 +03:00
|
|
|
|
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
2021-06-16 13:48:14 +03:00
|
|
|
|
if (!this.shouldAbortAfterInviteError(result, room)) { // handles setting error message too
|
2020-01-17 00:40:12 +03:00
|
|
|
|
this.props.onFinished();
|
|
|
|
|
}
|
2021-03-16 05:56:56 +03:00
|
|
|
|
|
2021-04-09 19:05:55 +03:00
|
|
|
|
if (cli.isRoomEncrypted(this.props.roomId)) {
|
2021-03-16 05:56:56 +03:00
|
|
|
|
const visibilityEvent = room.currentState.getStateEvents(
|
|
|
|
|
"m.room.history_visibility", "",
|
|
|
|
|
);
|
|
|
|
|
const visibility = visibilityEvent && visibilityEvent.getContent() &&
|
|
|
|
|
visibilityEvent.getContent().history_visibility;
|
|
|
|
|
if (visibility == "world_readable" || visibility == "shared") {
|
|
|
|
|
const invitedUsers = [];
|
|
|
|
|
for (const [addr, state] of Object.entries(result.states)) {
|
|
|
|
|
if (state === "invited" && getAddressType(addr) === "mx-user-id") {
|
|
|
|
|
invitedUsers.push(addr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
console.log("Sharing history with", invitedUsers);
|
|
|
|
|
cli.sendSharedHistoryKeys(
|
|
|
|
|
this.props.roomId, invitedUsers,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2020-01-17 00:40:12 +03:00
|
|
|
|
console.error(err);
|
|
|
|
|
this.setState({
|
|
|
|
|
busy: false,
|
2020-01-17 00:45:17 +03:00
|
|
|
|
errorText: _t(
|
|
|
|
|
"We couldn't invite those users. Please check the users you want to invite and try again.",
|
|
|
|
|
),
|
2020-01-17 00:40:12 +03:00
|
|
|
|
});
|
2021-03-16 05:56:56 +03:00
|
|
|
|
}
|
2020-01-17 00:40:12 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private transferCall = async () => {
|
2021-07-15 11:55:58 +03:00
|
|
|
|
if (this.state.currentTabId == TabId.UserDirectory) {
|
|
|
|
|
this.convertFilter();
|
|
|
|
|
const targets = this.convertFilter();
|
|
|
|
|
const targetIds = targets.map(t => t.userId);
|
|
|
|
|
if (targetIds.length > 1) {
|
2021-03-25 22:56:21 +03:00
|
|
|
|
this.setState({
|
2021-07-15 11:55:58 +03:00
|
|
|
|
errorText: _t("A call can only be transferred to a single user."),
|
2021-03-25 22:56:21 +03:00
|
|
|
|
});
|
2021-07-15 11:55:58 +03:00
|
|
|
|
return;
|
2021-03-25 22:56:21 +03:00
|
|
|
|
}
|
2021-07-15 11:55:58 +03:00
|
|
|
|
|
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: Action.TransferCallToMatrixID,
|
|
|
|
|
call: this.props.call,
|
|
|
|
|
destination: targetIds[0],
|
|
|
|
|
consultFirst: this.state.consultFirst,
|
|
|
|
|
} as TransferCallPayload);
|
|
|
|
|
} else {
|
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: Action.TransferCallToPhoneNumber,
|
|
|
|
|
call: this.props.call,
|
|
|
|
|
destination: this.state.dialPadValue,
|
|
|
|
|
consultFirst: this.state.consultFirst,
|
|
|
|
|
} as TransferCallPayload);
|
2020-12-15 17:59:06 +03:00
|
|
|
|
}
|
2021-07-15 11:55:58 +03:00
|
|
|
|
this.props.onFinished();
|
2020-12-15 17:59:06 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private onKeyDown = (e) => {
|
2020-11-03 16:14:35 +03:00
|
|
|
|
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
|
2020-03-19 00:09:52 +03:00
|
|
|
|
e.preventDefault();
|
2021-05-26 18:47:46 +03:00
|
|
|
|
this.removeMember(this.state.targets[this.state.targets.length - 1]);
|
2020-11-03 16:14:35 +03:00
|
|
|
|
} else if (value && e.key === Key.ENTER && !hasModifiers) {
|
|
|
|
|
// when the user hits enter with something in their field try to convert it
|
|
|
|
|
e.preventDefault();
|
2021-05-26 18:47:46 +03:00
|
|
|
|
this.convertFilter();
|
2020-11-03 16:14:35 +03:00
|
|
|
|
} 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();
|
2021-05-26 18:47:46 +03:00
|
|
|
|
this.convertFilter();
|
2020-03-19 00:09:52 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-07-15 11:55:58 +03:00
|
|
|
|
private onCancel = () => {
|
|
|
|
|
this.props.onFinished([]);
|
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private updateSuggestions = async (term) => {
|
2021-06-29 15:11:58 +03:00
|
|
|
|
MatrixClientPeg.get().searchUserDirectory({ term }).then(async r => {
|
2020-11-11 16:36:17 +03:00
|
|
|
|
if (term !== this.state.filterText) {
|
|
|
|
|
// Discard the results - we were probably too slow on the server-side to make
|
|
|
|
|
// these results useful. This is a race we want to avoid because we could overwrite
|
|
|
|
|
// more accurate results.
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-01-04 07:17:48 +03:00
|
|
|
|
|
2020-11-11 16:36:17 +03:00
|
|
|
|
if (!r.results) r.results = [];
|
2020-01-23 08:33:50 +03:00
|
|
|
|
|
2020-11-11 16:36:17 +03:00
|
|
|
|
// While we're here, try and autocomplete a search result for the mxid itself
|
|
|
|
|
// if there's no matches (and the input looks like a mxid).
|
|
|
|
|
if (term[0] === '@' && term.indexOf(':') > 1) {
|
|
|
|
|
try {
|
|
|
|
|
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
|
|
|
|
if (profile) {
|
|
|
|
|
// If we have a profile, we have enough information to assume that
|
|
|
|
|
// the mxid can be invited - add it to the list. We stick it at the
|
2020-02-21 09:01:50 +03:00
|
|
|
|
// top so it is most obviously presented to the user.
|
2020-02-21 09:21:35 +03:00
|
|
|
|
r.results.splice(0, 0, {
|
2020-02-20 20:50:35 +03:00
|
|
|
|
user_id: term,
|
2020-11-11 16:36:17 +03:00
|
|
|
|
display_name: profile['displayname'],
|
|
|
|
|
avatar_url: profile['avatar_url'],
|
2020-02-20 20:50:35 +03:00
|
|
|
|
});
|
2020-01-23 08:33:50 +03:00
|
|
|
|
}
|
2020-11-11 16:36:17 +03:00
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn("Non-fatal error trying to make an invite for a user ID");
|
|
|
|
|
console.warn(e);
|
|
|
|
|
|
|
|
|
|
// Add a result anyways, just without a profile. We stick it at the
|
|
|
|
|
// top so it is most obviously presented to the user.
|
|
|
|
|
r.results.splice(0, 0, {
|
|
|
|
|
user_id: term,
|
|
|
|
|
display_name: term,
|
|
|
|
|
avatar_url: null,
|
|
|
|
|
});
|
2020-01-23 08:33:50 +03:00
|
|
|
|
}
|
2020-11-11 16:36:17 +03:00
|
|
|
|
}
|
2020-01-23 08:33:50 +03:00
|
|
|
|
|
2020-11-11 16:36:17 +03:00
|
|
|
|
this.setState({
|
|
|
|
|
serverResultsMixin: r.results.map(u => ({
|
|
|
|
|
userId: u.user_id,
|
|
|
|
|
user: new DirectoryMember(u),
|
|
|
|
|
})),
|
2020-01-04 07:17:48 +03:00
|
|
|
|
});
|
2020-11-11 16:36:17 +03:00
|
|
|
|
}).catch(e => {
|
|
|
|
|
console.error("Error searching user directory:");
|
|
|
|
|
console.error(e);
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ serverResultsMixin: [] }); // clear results because it's moderately fatal
|
2020-11-11 16:36:17 +03:00
|
|
|
|
});
|
2020-01-09 06:49:29 +03:00
|
|
|
|
|
2020-11-11 16:36:17 +03:00
|
|
|
|
// 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.
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ tryingIdentityServer: true });
|
2020-11-11 16:36:17 +03:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) {
|
|
|
|
|
// 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
|
2021-06-29 15:11:58 +03:00
|
|
|
|
threepidResultsMixin: [{ user: new ThreepidMember(term), userId: term }],
|
2020-11-11 16:36:17 +03:00
|
|
|
|
});
|
|
|
|
|
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
|
2020-01-09 06:49:29 +03:00
|
|
|
|
this.setState({
|
2020-11-11 16:36:17 +03:00
|
|
|
|
threepidResultsMixin: [...this.state.threepidResultsMixin, {
|
|
|
|
|
user: new DirectoryMember({
|
|
|
|
|
user_id: lookup.mxid,
|
|
|
|
|
display_name: profile.displayname,
|
|
|
|
|
avatar_url: profile.avatar_url,
|
|
|
|
|
}),
|
|
|
|
|
userId: lookup.mxid,
|
|
|
|
|
}],
|
2020-01-09 06:49:29 +03:00
|
|
|
|
});
|
2020-11-11 16:36:17 +03:00
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Error searching identity server:");
|
|
|
|
|
console.error(e);
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ threepidResultsMixin: [] }); // clear results because it's moderately fatal
|
2020-01-09 06:49:29 +03:00
|
|
|
|
}
|
2020-11-11 16:36:17 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private updateFilter = (e) => {
|
2020-11-11 16:36:17 +03:00
|
|
|
|
const term = e.target.value;
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ filterText: term });
|
2020-11-11 16:36:17 +03:00
|
|
|
|
|
|
|
|
|
// 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.
|
2021-05-26 18:47:46 +03:00
|
|
|
|
if (this.debounceTimer) {
|
|
|
|
|
clearTimeout(this.debounceTimer);
|
2020-11-11 16:36:17 +03:00
|
|
|
|
}
|
2021-05-26 18:47:46 +03:00
|
|
|
|
this.debounceTimer = setTimeout(() => {
|
|
|
|
|
this.updateSuggestions(term);
|
2020-01-04 07:17:48 +03:00
|
|
|
|
}, 150); // 150ms debounce (human reaction time + some)
|
2020-01-03 03:40:18 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private showMoreRecents = () => {
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN });
|
2020-01-03 03:40:18 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private showMoreSuggestions = () => {
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN });
|
2020-01-04 05:41:06 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private toggleMember = (member: Member) => {
|
2021-03-23 21:25:03 +03:00
|
|
|
|
if (!this.state.busy) {
|
|
|
|
|
let filterText = this.state.filterText;
|
2021-07-15 11:55:58 +03:00
|
|
|
|
let targets = this.state.targets.map(t => t); // cheap clone for mutation
|
2021-03-23 21:25:03 +03:00
|
|
|
|
const idx = targets.indexOf(member);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
targets.splice(idx, 1);
|
|
|
|
|
} else {
|
2021-07-15 11:55:58 +03:00
|
|
|
|
if (this.props.kind === KIND_CALL_TRANSFER && targets.length > 0) {
|
|
|
|
|
targets = [];
|
|
|
|
|
}
|
2021-03-23 21:25:03 +03:00
|
|
|
|
targets.push(member);
|
|
|
|
|
filterText = ""; // clear the filter when the user accepts a suggestion
|
|
|
|
|
}
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ targets, filterText });
|
2020-11-03 16:14:35 +03:00
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
if (this.editorRef && this.editorRef.current) {
|
|
|
|
|
this.editorRef.current.focus();
|
2021-03-23 21:25:03 +03:00
|
|
|
|
}
|
2020-11-03 16:14:35 +03:00
|
|
|
|
}
|
2020-01-03 03:40:18 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private removeMember = (member: Member) => {
|
2020-01-07 06:51:23 +03:00
|
|
|
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
|
|
|
|
const idx = targets.indexOf(member);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
targets.splice(idx, 1);
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ targets });
|
2020-01-07 06:51:23 +03:00
|
|
|
|
}
|
2020-11-03 16:14:35 +03:00
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
if (this.editorRef && this.editorRef.current) {
|
|
|
|
|
this.editorRef.current.focus();
|
2020-11-03 16:14:35 +03:00
|
|
|
|
}
|
2020-01-07 06:51:23 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private onPaste = async (e) => {
|
2020-01-29 14:22:03 +03:00
|
|
|
|
if (this.state.filterText) {
|
|
|
|
|
// if the user has already typed something, just let them
|
|
|
|
|
// paste normally.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-03 16:14:35 +03:00
|
|
|
|
// Prevent the text being pasted into the input
|
2020-01-09 07:19:00 +03:00
|
|
|
|
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 = [];
|
2020-02-21 23:26:37 +03:00
|
|
|
|
const potentialAddresses = text.split(/[\s,]+/).map(p => p.trim()).filter(p => !!p); // filter empty strings
|
2020-01-09 07:19:00 +03:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-05-26 18:38:02 +03:00
|
|
|
|
if (this.unmounted) return;
|
2020-01-09 07:19:00 +03:00
|
|
|
|
|
|
|
|
|
if (failed.length > 0) {
|
|
|
|
|
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",
|
2021-06-29 15:11:58 +03:00
|
|
|
|
{ csvNames: failed.join(", ") },
|
2020-01-09 07:19:00 +03:00
|
|
|
|
),
|
|
|
|
|
button: _t('OK'),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ targets: [...this.state.targets, ...toAdd] });
|
2020-01-09 07:19:00 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private onClickInputArea = (e) => {
|
2020-01-07 06:51:23 +03:00
|
|
|
|
// Stop the browser from highlighting text
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
if (this.editorRef && this.editorRef.current) {
|
|
|
|
|
this.editorRef.current.focus();
|
2020-01-07 06:51:23 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private onUseDefaultIdentityServerClick = (e) => {
|
2020-01-09 06:49:29 +03:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
// Update the IS in account data. Actually using it may trigger terms.
|
|
|
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
|
|
|
useDefaultIdentityServer();
|
2021-06-29 15:11:58 +03:00
|
|
|
|
this.setState({ canUseIdentityServer: true, tryingIdentityServer: false });
|
2020-01-09 07:01:53 +03:00
|
|
|
|
};
|
2020-01-09 06:49:29 +03:00
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private onManageSettingsClick = (e) => {
|
2020-01-09 06:49:29 +03:00
|
|
|
|
e.preventDefault();
|
2020-05-14 06:07:19 +03:00
|
|
|
|
dis.fire(Action.ViewUserSettings);
|
2020-03-19 12:58:49 +03:00
|
|
|
|
this.props.onFinished();
|
2020-01-09 07:01:53 +03:00
|
|
|
|
};
|
2020-01-09 06:49:29 +03:00
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private onCommunityInviteClick = (e) => {
|
2020-08-28 22:22:20 +03:00
|
|
|
|
this.props.onFinished();
|
2020-08-31 19:19:05 +03:00
|
|
|
|
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
2020-08-28 22:22:20 +03:00
|
|
|
|
};
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private renderSection(kind: "recents"|"suggestions") {
|
2020-01-04 06:35:30 +03:00
|
|
|
|
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
2020-01-04 05:41:06 +03:00
|
|
|
|
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
2021-05-26 18:47:46 +03:00
|
|
|
|
const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this);
|
2020-01-04 05:41:06 +03:00
|
|
|
|
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
2020-01-17 00:40:12 +03:00
|
|
|
|
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
2020-08-28 19:03:27 +03:00
|
|
|
|
let sectionSubname = null;
|
|
|
|
|
|
2020-08-31 19:19:05 +03:00
|
|
|
|
if (kind === 'suggestions' && CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
|
|
|
|
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
2021-06-29 15:11:58 +03:00
|
|
|
|
sectionSubname = _t("May include members not in %(communityName)s", { communityName });
|
2020-08-28 19:03:27 +03:00
|
|
|
|
}
|
2020-01-17 00:40:12 +03:00
|
|
|
|
|
2021-03-03 14:34:29 +03:00
|
|
|
|
if (this.props.kind === KIND_INVITE) {
|
2020-01-17 00:40:12 +03:00
|
|
|
|
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
|
|
|
|
}
|
2020-01-04 05:41:06 +03:00
|
|
|
|
|
2020-01-09 06:49:29 +03:00
|
|
|
|
// 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.
|
2020-02-28 23:57:56 +03:00
|
|
|
|
let priorityAdditionalMembers = []; // Shows up before our own suggestions, higher quality
|
|
|
|
|
let otherAdditionalMembers = []; // Shows up after our own suggestions, lower quality
|
2020-01-09 06:49:29 +03:00
|
|
|
|
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.
|
2020-12-15 01:28:21 +03:00
|
|
|
|
// The type of u is a pain to define but members of both mixins have the 'userId' property
|
|
|
|
|
const notAlreadyExists = (u: any): boolean => {
|
2020-01-09 06:49:29 +03:00
|
|
|
|
return !sourceMembers.some(m => m.userId === u.userId)
|
2020-02-28 23:57:56 +03:00
|
|
|
|
&& !priorityAdditionalMembers.some(m => m.userId === u.userId)
|
|
|
|
|
&& !otherAdditionalMembers.some(m => m.userId === u.userId);
|
2020-01-09 06:53:20 +03:00
|
|
|
|
};
|
2020-01-09 06:49:29 +03:00
|
|
|
|
|
2020-02-28 23:57:56 +03:00
|
|
|
|
otherAdditionalMembers = this.state.serverResultsMixin.filter(notAlreadyExists);
|
|
|
|
|
priorityAdditionalMembers = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
2020-01-04 07:17:48 +03:00
|
|
|
|
}
|
2020-02-28 23:57:56 +03:00
|
|
|
|
const hasAdditionalMembers = priorityAdditionalMembers.length > 0 || otherAdditionalMembers.length > 0;
|
2020-01-04 07:17:48 +03:00
|
|
|
|
|
2020-01-04 06:35:30 +03:00
|
|
|
|
// Hide the section if there's nothing to filter by
|
2020-02-28 23:57:56 +03:00
|
|
|
|
if (sourceMembers.length === 0 && !hasAdditionalMembers) return null;
|
2020-01-04 05:41:06 +03:00
|
|
|
|
|
2020-01-04 06:35:30 +03:00
|
|
|
|
// Do some simple filtering on the input before going much further. If we get no results, say so.
|
|
|
|
|
if (this.state.filterText) {
|
|
|
|
|
const filterBy = this.state.filterText.toLowerCase();
|
|
|
|
|
sourceMembers = sourceMembers
|
|
|
|
|
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
|
|
|
|
|
|
2020-02-28 23:57:56 +03:00
|
|
|
|
if (sourceMembers.length === 0 && !hasAdditionalMembers) {
|
2020-01-04 06:35:30 +03:00
|
|
|
|
return (
|
2020-01-16 23:42:34 +03:00
|
|
|
|
<div className='mx_InviteDialog_section'>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<h3>{ sectionName }</h3>
|
|
|
|
|
<p>{ _t("No results") }</p>
|
2020-01-04 06:35:30 +03:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-09 06:49:29 +03:00
|
|
|
|
// 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.
|
2020-02-28 23:57:56 +03:00
|
|
|
|
sourceMembers = [...priorityAdditionalMembers, ...sourceMembers, ...otherAdditionalMembers];
|
2020-01-09 06:49:29 +03:00
|
|
|
|
|
2020-01-04 05:41:06 +03:00
|
|
|
|
// 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++;
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
|
|
|
|
// .slice() will return an incomplete array but won't error on us if we go too far
|
2020-01-04 05:41:06 +03:00
|
|
|
|
const toRender = sourceMembers.slice(0, showNum);
|
|
|
|
|
const hasMore = toRender.length < sourceMembers.length;
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
|
|
|
|
let showMore = null;
|
|
|
|
|
if (hasMore) {
|
|
|
|
|
showMore = (
|
2020-01-04 05:41:06 +03:00
|
|
|
|
<AccessibleButton onClick={showMoreFn} kind="link">
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ _t("Show more") }
|
2020-01-03 03:40:18 +03:00
|
|
|
|
</AccessibleButton>
|
2020-01-03 03:44:19 +03:00
|
|
|
|
);
|
2020-01-03 03:40:18 +03:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-03 03:44:19 +03:00
|
|
|
|
const tiles = toRender.map(r => (
|
2020-01-04 06:35:30 +03:00
|
|
|
|
<DMRoomTile
|
|
|
|
|
member={r.user}
|
|
|
|
|
lastActiveTs={lastActive(r)}
|
|
|
|
|
key={r.userId}
|
2021-05-26 18:47:46 +03:00
|
|
|
|
onToggle={this.toggleMember}
|
2020-01-04 06:35:30 +03:00
|
|
|
|
highlightWord={this.state.filterText}
|
2020-01-07 22:12:31 +03:00
|
|
|
|
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
2020-01-04 06:35:30 +03:00
|
|
|
|
/>
|
2020-01-03 03:44:19 +03:00
|
|
|
|
));
|
2020-01-03 03:40:18 +03:00
|
|
|
|
return (
|
2020-01-16 23:42:34 +03:00
|
|
|
|
<div className='mx_InviteDialog_section'>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<h3>{ sectionName }</h3>
|
|
|
|
|
{ sectionSubname ? <p className="mx_InviteDialog_subname">{ sectionSubname }</p> : null }
|
|
|
|
|
{ tiles }
|
|
|
|
|
{ showMore }
|
2020-01-03 03:40:18 +03:00
|
|
|
|
</div>
|
2020-01-03 03:44:19 +03:00
|
|
|
|
);
|
2020-01-03 03:40:18 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private renderEditor() {
|
2021-07-15 11:55:58 +03:00
|
|
|
|
const hasPlaceholder = (
|
|
|
|
|
this.props.kind == KIND_CALL_TRANSFER &&
|
|
|
|
|
this.state.targets.length === 0 &&
|
|
|
|
|
this.state.filterText.length === 0
|
|
|
|
|
);
|
2020-01-07 06:51:23 +03:00
|
|
|
|
const targets = this.state.targets.map(t => (
|
2021-05-26 18:47:46 +03:00
|
|
|
|
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
|
2020-01-07 06:51:23 +03:00
|
|
|
|
));
|
|
|
|
|
const input = (
|
2020-11-03 16:14:35 +03:00
|
|
|
|
<input
|
|
|
|
|
type="text"
|
2021-05-26 18:47:46 +03:00
|
|
|
|
onKeyDown={this.onKeyDown}
|
|
|
|
|
onChange={this.updateFilter}
|
2020-01-24 03:27:37 +03:00
|
|
|
|
value={this.state.filterText}
|
2021-05-26 18:47:46 +03:00
|
|
|
|
ref={this.editorRef}
|
|
|
|
|
onPaste={this.onPaste}
|
2020-01-29 14:01:00 +03:00
|
|
|
|
autoFocus={true}
|
2021-07-15 11:55:58 +03:00
|
|
|
|
disabled={this.state.busy || (this.props.kind == KIND_CALL_TRANSFER && this.state.targets.length > 0)}
|
2020-11-03 16:14:35 +03:00
|
|
|
|
autoComplete="off"
|
2021-07-15 11:55:58 +03:00
|
|
|
|
placeholder={hasPlaceholder ? _t("Search") : null}
|
2020-01-07 06:51:23 +03:00
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
return (
|
2021-05-26 18:47:46 +03:00
|
|
|
|
<div className='mx_InviteDialog_editor' onClick={this.onClickInputArea}>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ targets }
|
|
|
|
|
{ input }
|
2020-01-07 06:51:23 +03:00
|
|
|
|
</div>
|
2020-01-07 22:20:02 +03:00
|
|
|
|
);
|
2020-01-07 06:51:23 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-26 18:47:46 +03:00
|
|
|
|
private renderIdentityServerWarning() {
|
2020-09-16 16:45:34 +03:00
|
|
|
|
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
|
|
|
|
|
!SettingsStore.getValue(UIFeature.IdentityServer)
|
|
|
|
|
) {
|
2020-01-09 06:49:29 +03:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
|
|
|
|
if (defaultIdentityServerUrl) {
|
|
|
|
|
return (
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<div className="mx_AddressPickerDialog_identityServer">{ _t(
|
2020-01-09 06:49:29 +03:00
|
|
|
|
"Use an identity server to invite by email. " +
|
|
|
|
|
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
|
|
|
|
"or manage in <settings>Settings</settings>.",
|
|
|
|
|
{
|
|
|
|
|
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
|
|
|
|
},
|
|
|
|
|
{
|
2021-07-20 00:43:11 +03:00
|
|
|
|
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{ sub }</a>,
|
|
|
|
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
|
2020-01-09 06:49:29 +03:00
|
|
|
|
},
|
2021-07-20 00:43:11 +03:00
|
|
|
|
) }</div>
|
2020-01-09 06:49:29 +03:00
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return (
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<div className="mx_AddressPickerDialog_identityServer">{ _t(
|
2020-01-09 06:49:29 +03:00
|
|
|
|
"Use an identity server to invite by email. " +
|
|
|
|
|
"Manage in <settings>Settings</settings>.",
|
|
|
|
|
{}, {
|
2021-07-20 00:43:11 +03:00
|
|
|
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{ sub }</a>,
|
2020-01-09 06:49:29 +03:00
|
|
|
|
},
|
2021-07-20 00:43:11 +03:00
|
|
|
|
) }</div>
|
2020-01-09 06:49:29 +03:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-15 11:55:58 +03:00
|
|
|
|
private onDialFormSubmit = ev => {
|
|
|
|
|
ev.preventDefault();
|
|
|
|
|
this.transferCall();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private onDialChange = ev => {
|
|
|
|
|
this.setState({ dialPadValue: ev.currentTarget.value });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private onDigitPress = digit => {
|
|
|
|
|
this.setState({ dialPadValue: this.state.dialPadValue + digit });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private onDeletePress = () => {
|
|
|
|
|
if (this.state.dialPadValue.length === 0) return;
|
|
|
|
|
this.setState({ dialPadValue: this.state.dialPadValue.slice(0, -1) });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private onTabChange = (tabId: TabId) => {
|
|
|
|
|
this.setState({ currentTabId: tabId });
|
|
|
|
|
};
|
|
|
|
|
|
2021-06-08 18:42:58 +03:00
|
|
|
|
private async onLinkClick(e) {
|
2021-05-27 17:51:25 +03:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
selectText(e.target);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-08 18:42:58 +03:00
|
|
|
|
private onCopyClick = async e => {
|
2021-05-27 17:51:25 +03:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
const target = e.target; // copy target before we go async and React throws it away
|
|
|
|
|
|
|
|
|
|
const successful = await copyPlaintext(makeUserPermalink(MatrixClientPeg.get().getUserId()));
|
|
|
|
|
const buttonRect = target.getBoundingClientRect();
|
|
|
|
|
const { close } = ContextMenu.createMenu(GenericTextContextMenu, {
|
|
|
|
|
...toRightOf(buttonRect, 2),
|
|
|
|
|
message: successful ? _t("Copied!") : _t("Failed to copy"),
|
|
|
|
|
});
|
|
|
|
|
// Drop a reference to this close handler for componentWillUnmount
|
|
|
|
|
this.closeCopiedTooltip = target.onmouseleave = close;
|
2021-06-03 18:44:28 +03:00
|
|
|
|
};
|
2021-05-27 17:51:25 +03:00
|
|
|
|
|
2020-01-03 03:40:18 +03:00
|
|
|
|
render() {
|
2020-01-15 09:32:00 +03:00
|
|
|
|
let spinner = null;
|
|
|
|
|
if (this.state.busy) {
|
|
|
|
|
spinner = <Spinner w={20} h={20} />;
|
|
|
|
|
}
|
2020-01-03 03:40:18 +03:00
|
|
|
|
|
2020-01-17 00:40:12 +03:00
|
|
|
|
let title;
|
|
|
|
|
let helpText;
|
|
|
|
|
let buttonText;
|
|
|
|
|
let goButtonFn;
|
2021-07-15 11:55:58 +03:00
|
|
|
|
let consultConnectSection;
|
2021-05-27 17:51:25 +03:00
|
|
|
|
let extraSection;
|
|
|
|
|
let footer;
|
2021-03-16 05:56:56 +03:00
|
|
|
|
let keySharingWarning = <span />;
|
2020-01-17 00:40:12 +03:00
|
|
|
|
|
2020-09-16 16:45:34 +03:00
|
|
|
|
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
|
|
|
|
|
|
2021-07-15 11:55:58 +03:00
|
|
|
|
const hasSelection = this.state.targets.length > 0
|
|
|
|
|
|| (this.state.filterText && this.state.filterText.includes('@'));
|
|
|
|
|
|
2021-03-16 05:56:56 +03:00
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
const userId = cli.getUserId();
|
2020-01-17 00:40:12 +03:00
|
|
|
|
if (this.props.kind === KIND_DM) {
|
|
|
|
|
title = _t("Direct Messages");
|
2020-09-16 16:45:34 +03:00
|
|
|
|
|
|
|
|
|
if (identityServersEnabled) {
|
|
|
|
|
helpText = _t(
|
2020-11-03 15:47:13 +03:00
|
|
|
|
"Start a conversation with someone using their name, email address or username (like <userId/>).",
|
2020-09-16 16:54:30 +03:00
|
|
|
|
{},
|
2021-06-29 15:11:58 +03:00
|
|
|
|
{ userId: () => {
|
2020-09-16 16:45:34 +03:00
|
|
|
|
return (
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>
|
2020-09-16 16:45:34 +03:00
|
|
|
|
);
|
2021-06-29 15:11:58 +03:00
|
|
|
|
} },
|
2020-09-16 16:45:34 +03:00
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
helpText = _t(
|
2020-09-16 16:57:46 +03:00
|
|
|
|
"Start a conversation with someone using their name or username (like <userId/>).",
|
|
|
|
|
{},
|
2021-06-29 15:11:58 +03:00
|
|
|
|
{ userId: () => {
|
2020-09-16 16:45:34 +03:00
|
|
|
|
return (
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>
|
2020-09-16 16:45:34 +03:00
|
|
|
|
);
|
2021-06-29 15:11:58 +03:00
|
|
|
|
} },
|
2020-09-16 16:45:34 +03:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-31 19:19:05 +03:00
|
|
|
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
2020-08-28 23:56:59 +03:00
|
|
|
|
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
2020-12-15 01:28:21 +03:00
|
|
|
|
const inviteText = _t(
|
|
|
|
|
"This won't invite them to %(communityName)s. " +
|
2020-09-16 16:57:46 +03:00
|
|
|
|
"To invite someone to %(communityName)s, click <a>here</a>",
|
2021-06-29 15:11:58 +03:00
|
|
|
|
{ communityName }, {
|
2020-09-16 17:04:13 +03:00
|
|
|
|
userId: () => {
|
|
|
|
|
return (
|
|
|
|
|
<a
|
|
|
|
|
href={makeUserPermalink(userId)}
|
|
|
|
|
rel="noreferrer noopener"
|
|
|
|
|
target="_blank"
|
2021-07-20 00:43:11 +03:00
|
|
|
|
>{ userId }</a>
|
2020-09-16 17:04:13 +03:00
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
a: (sub) => {
|
|
|
|
|
return (
|
|
|
|
|
<AccessibleButton
|
|
|
|
|
kind="link"
|
2021-05-26 18:47:46 +03:00
|
|
|
|
onClick={this.onCommunityInviteClick}
|
2021-07-20 00:43:11 +03:00
|
|
|
|
>{ sub }</AccessibleButton>
|
2020-09-16 17:04:13 +03:00
|
|
|
|
);
|
|
|
|
|
},
|
2020-09-16 16:57:46 +03:00
|
|
|
|
},
|
2020-09-16 17:04:13 +03:00
|
|
|
|
);
|
2020-09-16 16:57:46 +03:00
|
|
|
|
helpText = <React.Fragment>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ helpText } { inviteText }
|
2020-09-16 16:45:34 +03:00
|
|
|
|
</React.Fragment>;
|
2020-08-28 22:22:20 +03:00
|
|
|
|
}
|
2020-01-17 00:40:12 +03:00
|
|
|
|
buttonText = _t("Go");
|
2021-05-26 18:47:46 +03:00
|
|
|
|
goButtonFn = this.startDm;
|
2021-05-28 15:00:18 +03:00
|
|
|
|
extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
|
|
|
|
|
<span>{ _t("Some suggestions may be hidden for privacy.") }</span>
|
2021-06-08 18:42:58 +03:00
|
|
|
|
<p>{ _t("If you can't see who you’re looking for, send them your invite link below.") }</p>
|
2021-05-27 17:51:25 +03:00
|
|
|
|
</div>;
|
|
|
|
|
const link = makeUserPermalink(MatrixClientPeg.get().getUserId());
|
|
|
|
|
footer = <div className="mx_InviteDialog_footer">
|
|
|
|
|
<h3>{ _t("Or send invite link") }</h3>
|
|
|
|
|
<div className="mx_InviteDialog_footer_link">
|
|
|
|
|
<a href={link} onClick={this.onLinkClick}>
|
|
|
|
|
{ link }
|
|
|
|
|
</a>
|
|
|
|
|
<AccessibleTooltipButton
|
|
|
|
|
title={_t("Copy")}
|
|
|
|
|
onClick={this.onCopyClick}
|
|
|
|
|
className="mx_InviteDialog_footer_link_copy"
|
2021-06-03 18:44:28 +03:00
|
|
|
|
>
|
|
|
|
|
<div />
|
|
|
|
|
</AccessibleTooltipButton>
|
2021-05-27 17:51:25 +03:00
|
|
|
|
</div>
|
2021-06-29 15:11:58 +03:00
|
|
|
|
</div>;
|
2021-03-03 14:34:29 +03:00
|
|
|
|
} else if (this.props.kind === KIND_INVITE) {
|
|
|
|
|
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
2021-07-15 10:26:49 +03:00
|
|
|
|
const isSpace = SpaceStore.spacesEnabled && room?.isSpaceRoom();
|
2021-03-03 14:34:29 +03:00
|
|
|
|
title = isSpace
|
|
|
|
|
? _t("Invite to %(spaceName)s", {
|
|
|
|
|
spaceName: room.name || _t("Unnamed Space"),
|
|
|
|
|
})
|
2021-03-19 18:42:47 +03:00
|
|
|
|
: _t("Invite to %(roomName)s", {
|
|
|
|
|
roomName: room.name || _t("Unnamed Room"),
|
|
|
|
|
});
|
2021-03-01 21:10:17 +03:00
|
|
|
|
|
|
|
|
|
let helpTextUntranslated;
|
2021-03-03 14:34:29 +03:00
|
|
|
|
if (isSpace) {
|
2021-03-01 21:10:17 +03:00
|
|
|
|
if (identityServersEnabled) {
|
|
|
|
|
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
2021-03-03 14:34:29 +03:00
|
|
|
|
"(like <userId/>) or <a>share this space</a>.");
|
2021-03-01 21:10:17 +03:00
|
|
|
|
} else {
|
|
|
|
|
helpTextUntranslated = _td("Invite someone using their name, username " +
|
2021-03-03 14:34:29 +03:00
|
|
|
|
"(like <userId/>) or <a>share this space</a>.");
|
2021-03-01 21:10:17 +03:00
|
|
|
|
}
|
2021-03-03 14:34:29 +03:00
|
|
|
|
} else {
|
2021-03-01 21:10:17 +03:00
|
|
|
|
if (identityServersEnabled) {
|
|
|
|
|
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
2021-03-03 14:34:29 +03:00
|
|
|
|
"(like <userId/>) or <a>share this room</a>.");
|
2021-03-01 21:10:17 +03:00
|
|
|
|
} else {
|
|
|
|
|
helpTextUntranslated = _td("Invite someone using their name, username " +
|
2021-03-03 14:34:29 +03:00
|
|
|
|
"(like <userId/>) or <a>share this room</a>.");
|
2021-03-01 21:10:17 +03:00
|
|
|
|
}
|
2020-09-16 16:45:34 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-01 21:10:17 +03:00
|
|
|
|
helpText = _t(helpTextUntranslated, {}, {
|
|
|
|
|
userId: () =>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{ userId }</a>,
|
2021-03-01 21:10:17 +03:00
|
|
|
|
a: (sub) =>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{ sub }</a>,
|
2021-03-01 21:10:17 +03:00
|
|
|
|
});
|
|
|
|
|
|
2020-01-17 00:40:12 +03:00
|
|
|
|
buttonText = _t("Invite");
|
2021-05-26 18:47:46 +03:00
|
|
|
|
goButtonFn = this.inviteUsers;
|
2021-03-16 05:56:56 +03:00
|
|
|
|
|
2021-04-09 19:05:55 +03:00
|
|
|
|
if (cli.isRoomEncrypted(this.props.roomId)) {
|
2021-03-16 05:56:56 +03:00
|
|
|
|
const room = cli.getRoom(this.props.roomId);
|
|
|
|
|
const visibilityEvent = room.currentState.getStateEvents(
|
|
|
|
|
"m.room.history_visibility", "",
|
|
|
|
|
);
|
|
|
|
|
const visibility = visibilityEvent && visibilityEvent.getContent() &&
|
|
|
|
|
visibilityEvent.getContent().history_visibility;
|
2021-03-19 23:55:07 +03:00
|
|
|
|
if (visibility === "world_readable" || visibility === "shared") {
|
2021-03-16 05:56:56 +03:00
|
|
|
|
keySharingWarning =
|
2021-03-26 02:27:14 +03:00
|
|
|
|
<p className='mx_InviteDialog_helpText'>
|
|
|
|
|
<img
|
|
|
|
|
src={require("../../../../res/img/element-icons/info.svg")}
|
2021-07-23 12:23:45 +03:00
|
|
|
|
width={14}
|
|
|
|
|
height={14} />
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ " " + _t("Invited people will be able to read old messages.") }
|
2021-03-26 02:27:14 +03:00
|
|
|
|
</p>;
|
2021-03-16 05:56:56 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-15 17:59:06 +03:00
|
|
|
|
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
|
|
|
|
title = _t("Transfer");
|
2021-07-15 11:55:58 +03:00
|
|
|
|
|
|
|
|
|
consultConnectSection = <div className="mx_InviteDialog_transferConsultConnect">
|
2021-03-25 22:56:21 +03:00
|
|
|
|
<label>
|
|
|
|
|
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ _t("Consult first") }
|
2021-03-25 22:56:21 +03:00
|
|
|
|
</label>
|
2021-07-15 11:55:58 +03:00
|
|
|
|
<AccessibleButton
|
|
|
|
|
kind="secondary"
|
|
|
|
|
onClick={this.onCancel}
|
|
|
|
|
className='mx_InviteDialog_transferConsultConnect_pushRight'
|
|
|
|
|
>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ _t("Cancel") }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
</AccessibleButton>
|
|
|
|
|
<AccessibleButton
|
|
|
|
|
kind="primary"
|
|
|
|
|
onClick={this.transferCall}
|
|
|
|
|
className='mx_InviteDialog_transferButton'
|
|
|
|
|
disabled={!hasSelection && this.state.dialPadValue === ''}
|
|
|
|
|
>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ _t("Transfer") }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
</AccessibleButton>
|
2021-03-25 22:56:21 +03:00
|
|
|
|
</div>;
|
2020-12-15 17:59:06 +03:00
|
|
|
|
} else {
|
|
|
|
|
console.error("Unknown kind of InviteDialog: " + this.props.kind);
|
2020-01-17 00:40:12 +03:00
|
|
|
|
}
|
|
|
|
|
|
2021-07-15 11:55:58 +03:00
|
|
|
|
const goButton = this.props.kind == KIND_CALL_TRANSFER ? null : <AccessibleButton
|
|
|
|
|
kind="primary"
|
|
|
|
|
onClick={goButtonFn}
|
|
|
|
|
className='mx_InviteDialog_goButton'
|
|
|
|
|
disabled={this.state.busy || !hasSelection}
|
|
|
|
|
>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ buttonText }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
</AccessibleButton>;
|
|
|
|
|
|
|
|
|
|
const usersSection = <React.Fragment>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
<p className='mx_InviteDialog_helpText'>{ helpText }</p>
|
2021-07-15 11:55:58 +03:00
|
|
|
|
<div className='mx_InviteDialog_addressBar'>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ this.renderEditor() }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
<div className='mx_InviteDialog_buttonAndSpinner'>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ goButton }
|
|
|
|
|
{ spinner }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ keySharingWarning }
|
|
|
|
|
{ this.renderIdentityServerWarning() }
|
|
|
|
|
<div className='error'>{ this.state.errorText }</div>
|
2021-07-15 11:55:58 +03:00
|
|
|
|
<div className='mx_InviteDialog_userSections'>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ this.renderSection('recents') }
|
|
|
|
|
{ this.renderSection('suggestions') }
|
|
|
|
|
{ extraSection }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
</div>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ footer }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
</React.Fragment>;
|
|
|
|
|
|
|
|
|
|
let dialogContent;
|
|
|
|
|
if (this.props.kind === KIND_CALL_TRANSFER) {
|
|
|
|
|
const tabs = [];
|
|
|
|
|
tabs.push(new Tab(
|
|
|
|
|
TabId.UserDirectory, _td("User Directory"), 'mx_InviteDialog_userDirectoryIcon', usersSection,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
const backspaceButton = (
|
|
|
|
|
<DialPadBackspaceButton onBackspacePress={this.onDeletePress} />
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Only show the backspace button if the field has content
|
|
|
|
|
let dialPadField;
|
|
|
|
|
if (this.state.dialPadValue.length !== 0) {
|
2021-07-23 12:23:45 +03:00
|
|
|
|
dialPadField = <Field
|
|
|
|
|
className="mx_InviteDialog_dialPadField"
|
|
|
|
|
id="dialpad_number"
|
2021-07-15 11:55:58 +03:00
|
|
|
|
value={this.state.dialPadValue}
|
|
|
|
|
autoFocus={true}
|
|
|
|
|
onChange={this.onDialChange}
|
|
|
|
|
postfixComponent={backspaceButton}
|
|
|
|
|
/>;
|
|
|
|
|
} else {
|
2021-07-23 12:23:45 +03:00
|
|
|
|
dialPadField = <Field
|
|
|
|
|
className="mx_InviteDialog_dialPadField"
|
|
|
|
|
id="dialpad_number"
|
2021-07-15 11:55:58 +03:00
|
|
|
|
value={this.state.dialPadValue}
|
|
|
|
|
autoFocus={true}
|
|
|
|
|
onChange={this.onDialChange}
|
|
|
|
|
/>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dialPadSection = <div className="mx_InviteDialog_dialPad">
|
|
|
|
|
<form onSubmit={this.onDialFormSubmit}>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ dialPadField }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
</form>
|
2021-07-23 12:23:45 +03:00
|
|
|
|
<Dialpad
|
|
|
|
|
hasDial={false}
|
|
|
|
|
onDigitPress={this.onDigitPress}
|
|
|
|
|
onDeletePress={this.onDeletePress}
|
2021-07-15 11:55:58 +03:00
|
|
|
|
/>
|
|
|
|
|
</div>;
|
|
|
|
|
tabs.push(new Tab(TabId.DialPad, _td("Dial pad"), 'mx_InviteDialog_dialPadIcon', dialPadSection));
|
|
|
|
|
dialogContent = <React.Fragment>
|
2021-07-23 12:23:45 +03:00
|
|
|
|
<TabbedView
|
|
|
|
|
tabs={tabs}
|
|
|
|
|
initialTabId={this.state.currentTabId}
|
|
|
|
|
tabLocation={TabLocation.TOP}
|
|
|
|
|
onChange={this.onTabChange}
|
2021-07-15 11:55:58 +03:00
|
|
|
|
/>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ consultConnectSection }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
</React.Fragment>;
|
|
|
|
|
} else {
|
|
|
|
|
dialogContent = <React.Fragment>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ usersSection }
|
|
|
|
|
{ consultConnectSection }
|
2021-07-15 11:55:58 +03:00
|
|
|
|
</React.Fragment>;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-03 03:40:18 +03:00
|
|
|
|
return (
|
|
|
|
|
<BaseDialog
|
2021-07-15 11:55:58 +03:00
|
|
|
|
className={classNames({
|
|
|
|
|
mx_InviteDialog_transfer: this.props.kind === KIND_CALL_TRANSFER,
|
|
|
|
|
mx_InviteDialog_other: this.props.kind !== KIND_CALL_TRANSFER,
|
2021-06-03 18:44:28 +03:00
|
|
|
|
mx_InviteDialog_hasFooter: !!footer,
|
|
|
|
|
})}
|
2020-01-03 03:40:18 +03:00
|
|
|
|
hasCancel={true}
|
2020-03-19 12:58:49 +03:00
|
|
|
|
onFinished={this.props.onFinished}
|
2020-01-17 00:40:12 +03:00
|
|
|
|
title={title}
|
2020-01-03 03:40:18 +03:00
|
|
|
|
>
|
2020-01-16 23:42:34 +03:00
|
|
|
|
<div className='mx_InviteDialog_content'>
|
2021-07-20 00:43:11 +03:00
|
|
|
|
{ dialogContent }
|
2020-01-03 03:40:18 +03:00
|
|
|
|
</div>
|
|
|
|
|
</BaseDialog>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|