Merge pull request #6201 from matrix-org/t3chguy/fix/15051
This commit is contained in:
commit
71414904fc
10 changed files with 252 additions and 142 deletions
|
@ -294,6 +294,7 @@ limitations under the License.
|
|||
flex-direction: column;
|
||||
|
||||
.mx_InviteDialog_content {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -317,3 +318,42 @@ limitations under the License.
|
|||
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_multiInviterError {
|
||||
> h4 {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $secondary-fg-color;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
> div {
|
||||
.mx_InviteDialog_multiInviterError_entry {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.mx_InviteDialog_multiInviterError_entry_userProfile {
|
||||
.mx_InviteDialog_multiInviterError_entry_name {
|
||||
margin-left: 6px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_multiInviterError_entry_userId {
|
||||
margin-left: 6px;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $tertiary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_multiInviterError_entry_error {
|
||||
margin-left: 32px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $notice-primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016 - 2021 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.
|
||||
|
@ -16,15 +14,26 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import MultiInviter from './utils/MultiInviter';
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import MultiInviter, { CompletionStates } from './utils/MultiInviter';
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './';
|
||||
import { _t } from './languageHandler';
|
||||
import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||
import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog";
|
||||
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
||||
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
||||
import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore";
|
||||
import BaseAvatar from "./components/views/avatars/BaseAvatar";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
|
||||
export interface IInviteResult {
|
||||
states: CompletionStates;
|
||||
inviter: MultiInviter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invites multiple addresses to a room
|
||||
|
@ -32,15 +41,15 @@ import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
|||
* no option to cancel.
|
||||
*
|
||||
* @param {string} roomId The ID of the room to invite to
|
||||
* @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||
* @returns {Promise} Promise
|
||||
*/
|
||||
export function inviteMultipleToRoom(roomId, addrs) {
|
||||
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
|
||||
const inviter = new MultiInviter(roomId);
|
||||
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
|
||||
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
||||
}
|
||||
|
||||
export function showStartChatInviteDialog(initialText) {
|
||||
export function showStartChatInviteDialog(initialText = ""): void {
|
||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
|
@ -49,7 +58,7 @@ export function showStartChatInviteDialog(initialText) {
|
|||
);
|
||||
}
|
||||
|
||||
export function showRoomInviteDialog(roomId, initialText = "") {
|
||||
export function showRoomInviteDialog(roomId: string, initialText = ""): void {
|
||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||
Modal.createTrackedDialog(
|
||||
"Invite Users", "", InviteDialog, {
|
||||
|
@ -61,14 +70,14 @@ export function showRoomInviteDialog(roomId, initialText = "") {
|
|||
);
|
||||
}
|
||||
|
||||
export function showCommunityRoomInviteDialog(roomId, communityName) {
|
||||
export function showCommunityRoomInviteDialog(roomId: string, communityName: string): void {
|
||||
Modal.createTrackedDialog(
|
||||
'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
}
|
||||
|
||||
export function showCommunityInviteDialog(communityId) {
|
||||
export function showCommunityInviteDialog(communityId: string): void {
|
||||
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
|
||||
if (chat) {
|
||||
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
|
||||
|
@ -83,7 +92,7 @@ export function showCommunityInviteDialog(communityId) {
|
|||
* @param {MatrixEvent} event The event to check
|
||||
* @returns {boolean} True if valid, false otherwise
|
||||
*/
|
||||
export function isValid3pidInvite(event) {
|
||||
export function isValid3pidInvite(event: MatrixEvent): boolean {
|
||||
if (!event || event.getType() !== "m.room.third_party_invite") return false;
|
||||
|
||||
// any events without these keys are not valid 3pid invites, so we ignore them
|
||||
|
@ -96,7 +105,7 @@ export function isValid3pidInvite(event) {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function inviteUsersToRoom(roomId, userIds) {
|
||||
export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
|
||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
showAnyInviteErrors(result.states, room, result.inviter);
|
||||
|
@ -110,9 +119,14 @@ export function inviteUsersToRoom(roomId, userIds) {
|
|||
});
|
||||
}
|
||||
|
||||
export function showAnyInviteErrors(addrs, room, inviter) {
|
||||
export function showAnyInviteErrors(
|
||||
states: CompletionStates,
|
||||
room: Room,
|
||||
inviter: MultiInviter,
|
||||
userMap?: Map<string, Member>,
|
||||
): boolean {
|
||||
// Show user any errors
|
||||
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
|
||||
const failedUsers = Object.keys(states).filter(a => states[a] === 'error');
|
||||
if (failedUsers.length === 1 && inviter.fatal) {
|
||||
// Just get the first message because there was a fatal problem on the first
|
||||
// user. This usually means that no other users were attempted, making it
|
||||
|
@ -126,19 +140,47 @@ export function showAnyInviteErrors(addrs, room, inviter) {
|
|||
} else {
|
||||
const errorList = [];
|
||||
for (const addr of failedUsers) {
|
||||
if (addrs[addr] === "error") {
|
||||
if (states[addr] === "error") {
|
||||
const reason = inviter.getErrorText(addr);
|
||||
errorList.push(addr + ": " + reason);
|
||||
}
|
||||
}
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (errorList.length > 0) {
|
||||
// React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
|
||||
const description = <div>{errorList.map(e => <div key={e}>{e}</div>)}</div>;
|
||||
const description = <div className="mx_InviteDialog_multiInviterError">
|
||||
<h4>{ _t("We sent the others, but the below people couldn't be invited to <RoomName/>", {}, {
|
||||
RoomName: () => <b>{ room.name }</b>,
|
||||
}) }</h4>
|
||||
<div>
|
||||
{ failedUsers.map(addr => {
|
||||
const user = userMap?.get(addr) || cli.getUser(addr);
|
||||
const name = (user as Member).name || (user as User).rawDisplayName;
|
||||
const avatarUrl = (user as Member).getMxcAvatarUrl?.() || (user as User).avatarUrl;
|
||||
return <div key={addr} className="mx_InviteDialog_multiInviterError_entry">
|
||||
<div className="mx_InviteDialog_multiInviterError_entry_userProfile">
|
||||
<BaseAvatar
|
||||
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null}
|
||||
name={name}
|
||||
idName={user.userId}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span className="mx_InviteDialog_multiInviterError_entry_name">{ name }</span>
|
||||
<span className="mx_InviteDialog_multiInviterError_entry_userId">{ user.userId }</span>
|
||||
</div>
|
||||
<div className="mx_InviteDialog_multiInviterError_entry_error">
|
||||
{ inviter.getErrorText(addr) }
|
||||
</div>
|
||||
</div>;
|
||||
}) }
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
|
||||
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
||||
Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, {
|
||||
title: _t("Some invites couldn't be sent"),
|
||||
description,
|
||||
});
|
||||
return false;
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017 - 2021 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.
|
||||
|
@ -14,15 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
export const addressTypes = [
|
||||
'mx-user-id', 'mx-room-id', 'email',
|
||||
];
|
||||
export const addressTypes = ['mx-user-id', 'mx-room-id', 'email'];
|
||||
|
||||
export enum AddressType {
|
||||
Email = "email",
|
||||
MatrixUserId = "mx-user-id",
|
||||
MatrixRoomId = "mx-room-id",
|
||||
}
|
||||
|
||||
// PropType definition for an object describing
|
||||
// an address that can be invited to a room (which
|
||||
|
@ -40,18 +44,13 @@ export const UserAddressType = PropTypes.shape({
|
|||
isKnown: PropTypes.bool,
|
||||
});
|
||||
|
||||
export function getAddressType(inputText) {
|
||||
const isEmailAddress = emailRegex.test(inputText);
|
||||
const isUserId = mxUserIdRegex.test(inputText);
|
||||
const isRoomId = mxRoomIdRegex.test(inputText);
|
||||
|
||||
// sanity check the input for user IDs
|
||||
if (isEmailAddress) {
|
||||
return 'email';
|
||||
} else if (isUserId) {
|
||||
return 'mx-user-id';
|
||||
} else if (isRoomId) {
|
||||
return 'mx-room-id';
|
||||
export function getAddressType(inputText: string): AddressType | null {
|
||||
if (emailRegex.test(inputText)) {
|
||||
return AddressType.Email;
|
||||
} else if (mxUserIdRegex.test(inputText)) {
|
||||
return AddressType.MatrixUserId;
|
||||
} else if (mxRoomIdRegex.test(inputText)) {
|
||||
return AddressType.MatrixRoomId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
|
@ -48,7 +48,7 @@ import createRoom, {IOpts} from "../../createRoom";
|
|||
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import ThemeController from "../../settings/controllers/ThemeController";
|
||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
import { startAnyRegistrationFlow } from "../../Registration";
|
||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||
|
|
|
@ -24,7 +24,7 @@ import { _t, _td } from '../../../languageHandler';
|
|||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import * as Email from '../../../email';
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
|
|
|
@ -17,37 +17,45 @@ limitations under the License.
|
|||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import * as Email from "../../../email";
|
||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
|
||||
import { abbreviateUrl } from "../../../utils/UrlUtils";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import { humanizeTime } from "../../../utils/humanize";
|
||||
import createRoom, {
|
||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||
canEncryptToAllUsers,
|
||||
ensureDMExists,
|
||||
findDMForUser,
|
||||
privateShouldBeEncrypted,
|
||||
} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
import {
|
||||
IInviteResult,
|
||||
inviteMultipleToRoom,
|
||||
showAnyInviteErrors,
|
||||
showCommunityInviteDialog,
|
||||
} from "../../../RoomInvite";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {getAddressType} from "../../../UserAddress";
|
||||
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 { compare } from '../../../utils/strings';
|
||||
|
@ -74,10 +82,10 @@ export const KIND_CALL_TRANSFER = "call_transfer";
|
|||
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
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
abstract class Member {
|
||||
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).
|
||||
|
@ -102,7 +110,8 @@ class DirectoryMember extends Member {
|
|||
private readonly displayName: string;
|
||||
private readonly avatarUrl: string;
|
||||
|
||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||
// eslint-disable-next-line camelcase
|
||||
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
this.displayName = userDirResult.display_name;
|
||||
|
@ -601,19 +610,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||
}
|
||||
|
||||
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);
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
|
||||
csvUsers: failedUsers.join(", "),
|
||||
}),
|
||||
});
|
||||
return true; // abort
|
||||
}
|
||||
return false;
|
||||
private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean {
|
||||
this.setState({ busy: false });
|
||||
const userMap = new Map<string, Member>(this.state.targets.map(member => [member.userId, member]));
|
||||
return !showAnyInviteErrors(result.states, room, result.inviter, userMap);
|
||||
}
|
||||
|
||||
private convertFilter(): Member[] {
|
||||
|
@ -731,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, room)) { // handles setting error message too
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { UserAddressType } from '../../../UserAddress.js';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { UserAddressType } from '../../../UserAddress';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.elements.AddressTile")
|
||||
export default class AddressTile extends React.Component {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import Flair from '../elements/Flair';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
|
|
@ -396,7 +396,8 @@
|
|||
"Failed to invite": "Failed to invite",
|
||||
"Operation failed": "Operation failed",
|
||||
"Failed to invite users to the room:": "Failed to invite users to the room:",
|
||||
"Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
|
||||
"We sent the others, but the below people couldn't be invited to <RoomName/>": "We sent the others, but the below people couldn't be invited to <RoomName/>",
|
||||
"Some invites couldn't be sent": "Some invites couldn't be sent",
|
||||
"You need to be logged in.": "You need to be logged in.",
|
||||
"You need to be able to invite users to do that.": "You need to be able to invite users to do that.",
|
||||
"Unable to create widget.": "Unable to create widget.",
|
||||
|
@ -2277,7 +2278,6 @@
|
|||
"Confirm to continue": "Confirm to continue",
|
||||
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
|
||||
"Invite by email": "Invite by email",
|
||||
"Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s",
|
||||
"We couldn't create your DM.": "We couldn't create your DM.",
|
||||
"Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.",
|
||||
"We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2016 - 2021 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.
|
||||
|
@ -15,23 +14,51 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import {getAddressType} from '../UserAddress';
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { AddressType, getAddressType } from '../UserAddress';
|
||||
import GroupStore from '../stores/GroupStore';
|
||||
import {_t} from "../languageHandler";
|
||||
import * as sdk from "../index";
|
||||
import { _t } from "../languageHandler";
|
||||
import Modal from "../Modal";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import {defer} from "./promise";
|
||||
import { defer, IDeferred } from "./promise";
|
||||
import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog";
|
||||
|
||||
export enum InviteState {
|
||||
Invited = "invited",
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
interface IError {
|
||||
errorText: string;
|
||||
errcode: string;
|
||||
}
|
||||
|
||||
const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
|
||||
|
||||
export type CompletionStates = Record<string, InviteState>;
|
||||
|
||||
/**
|
||||
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
||||
*/
|
||||
export default class MultiInviter {
|
||||
private readonly roomId?: string;
|
||||
private readonly groupId?: string;
|
||||
|
||||
private canceled = false;
|
||||
private addresses: string[] = [];
|
||||
private busy = false;
|
||||
private _fatal = false;
|
||||
private completionStates: CompletionStates = {}; // State of each address (invited or error)
|
||||
private errors: Record<string, IError> = {}; // { address: {errorText, errcode} }
|
||||
private deferred: IDeferred<CompletionStates> = null;
|
||||
private reason: string = null;
|
||||
|
||||
/**
|
||||
* @param {string} targetId The ID of the room or group to invite to
|
||||
*/
|
||||
constructor(targetId) {
|
||||
constructor(targetId: string) {
|
||||
if (targetId[0] === '+') {
|
||||
this.roomId = null;
|
||||
this.groupId = targetId;
|
||||
|
@ -39,41 +66,38 @@ export default class MultiInviter {
|
|||
this.roomId = targetId;
|
||||
this.groupId = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.canceled = false;
|
||||
this.addrs = [];
|
||||
this.busy = false;
|
||||
this.completionStates = {}; // State of each address (invited or error)
|
||||
this.errors = {}; // { address: {errorText, errcode} }
|
||||
this.deferred = null;
|
||||
public get fatal() {
|
||||
return this._fatal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite users to this room. This may only be called once per
|
||||
* instance of the class.
|
||||
*
|
||||
* @param {array} addrs Array of addresses to invite
|
||||
* @param {array} addresses Array of addresses to invite
|
||||
* @param {string} reason Reason for inviting (optional)
|
||||
* @returns {Promise} Resolved when all invitations in the queue are complete
|
||||
*/
|
||||
invite(addrs, reason) {
|
||||
if (this.addrs.length > 0) {
|
||||
public invite(addresses, reason?: string): Promise<CompletionStates> {
|
||||
if (this.addresses.length > 0) {
|
||||
throw new Error("Already inviting/invited");
|
||||
}
|
||||
this.addrs.push(...addrs);
|
||||
this.addresses.push(...addresses);
|
||||
this.reason = reason;
|
||||
|
||||
for (const addr of this.addrs) {
|
||||
for (const addr of this.addresses) {
|
||||
if (getAddressType(addr) === null) {
|
||||
this.completionStates[addr] = 'error';
|
||||
this.completionStates[addr] = InviteState.Error;
|
||||
this.errors[addr] = {
|
||||
errcode: 'M_INVALID',
|
||||
errorText: _t('Unrecognised address'),
|
||||
};
|
||||
}
|
||||
}
|
||||
this.deferred = defer();
|
||||
this._inviteMore(0);
|
||||
this.deferred = defer<CompletionStates>();
|
||||
this.inviteMore(0);
|
||||
|
||||
return this.deferred.promise;
|
||||
}
|
||||
|
@ -81,33 +105,36 @@ export default class MultiInviter {
|
|||
/**
|
||||
* Stops inviting. Causes promises returned by invite() to be rejected.
|
||||
*/
|
||||
cancel() {
|
||||
public cancel(): void {
|
||||
if (!this.busy) return;
|
||||
|
||||
this._canceled = true;
|
||||
this.canceled = true;
|
||||
this.deferred.reject(new Error('canceled'));
|
||||
}
|
||||
|
||||
getCompletionState(addr) {
|
||||
public getCompletionState(addr: string): InviteState {
|
||||
return this.completionStates[addr];
|
||||
}
|
||||
|
||||
getErrorText(addr) {
|
||||
public getErrorText(addr: string): string {
|
||||
return this.errors[addr] ? this.errors[addr].errorText : null;
|
||||
}
|
||||
|
||||
async _inviteToRoom(roomId, addr, ignoreProfile) {
|
||||
private async inviteToRoom(roomId: string, addr: string, ignoreProfile = false): Promise<{}> {
|
||||
const addrType = getAddressType(addr);
|
||||
|
||||
if (addrType === 'email') {
|
||||
if (addrType === AddressType.Email) {
|
||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
} else if (addrType === AddressType.MatrixUserId) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) throw new Error("Room not found");
|
||||
|
||||
const member = room.getMember(addr);
|
||||
if (member && ['join', 'invite'].includes(member.membership)) {
|
||||
throw {errcode: "RIOT.ALREADY_IN_ROOM", error: "Member already invited"};
|
||||
throw new new MatrixError({
|
||||
errcode: "RIOT.ALREADY_IN_ROOM",
|
||||
error: "Member already invited",
|
||||
});
|
||||
}
|
||||
|
||||
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
||||
|
@ -124,28 +151,28 @@ export default class MultiInviter {
|
|||
}
|
||||
}
|
||||
|
||||
_doInvite(address, ignoreProfile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
private doInvite(address: string, ignoreProfile = false): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
console.log(`Inviting ${address}`);
|
||||
|
||||
let doInvite;
|
||||
if (this.groupId !== null) {
|
||||
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
|
||||
} else {
|
||||
doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile);
|
||||
doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile);
|
||||
}
|
||||
|
||||
doInvite.then(() => {
|
||||
if (this._canceled) {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.completionStates[address] = 'invited';
|
||||
this.completionStates[address] = InviteState.Invited;
|
||||
delete this.errors[address];
|
||||
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
if (this._canceled) {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -161,7 +188,7 @@ export default class MultiInviter {
|
|||
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
||||
// we're being throttled so wait a bit & try again
|
||||
setTimeout(() => {
|
||||
this._doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
}, 5000);
|
||||
return;
|
||||
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) {
|
||||
|
@ -171,7 +198,7 @@ export default class MultiInviter {
|
|||
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) {
|
||||
// Invite without the profile check
|
||||
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
|
||||
this._doInvite(address, true).then(resolve, reject);
|
||||
this.doInvite(address, true).then(resolve, reject);
|
||||
} else if (err.errcode === "M_BAD_STATE") {
|
||||
errorText = _t("The user must be unbanned before they can be invited.");
|
||||
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
|
||||
|
@ -180,14 +207,14 @@ export default class MultiInviter {
|
|||
errorText = _t('Unknown server error');
|
||||
}
|
||||
|
||||
this.completionStates[address] = 'error';
|
||||
this.errors[address] = {errorText, errcode: err.errcode};
|
||||
this.completionStates[address] = InviteState.Error;
|
||||
this.errors[address] = { errorText, errcode: err.errcode };
|
||||
|
||||
this.busy = !fatal;
|
||||
this.fatal = fatal;
|
||||
this._fatal = fatal;
|
||||
|
||||
if (fatal) {
|
||||
reject();
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
@ -195,22 +222,22 @@ export default class MultiInviter {
|
|||
});
|
||||
}
|
||||
|
||||
_inviteMore(nextIndex, ignoreProfile) {
|
||||
if (this._canceled) {
|
||||
private inviteMore(nextIndex: number, ignoreProfile = false): void {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextIndex === this.addrs.length) {
|
||||
if (nextIndex === this.addresses.length) {
|
||||
this.busy = false;
|
||||
if (Object.keys(this.errors).length > 0 && !this.groupId) {
|
||||
// There were problems inviting some people - see if we can invite them
|
||||
// without caring if they exist or not.
|
||||
const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
|
||||
const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode));
|
||||
const unknownProfileUsers = Object.keys(this.errors)
|
||||
.filter(a => UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode));
|
||||
|
||||
if (unknownProfileUsers.length > 0) {
|
||||
const inviteUnknowns = () => {
|
||||
const promises = unknownProfileUsers.map(u => this._doInvite(u, true));
|
||||
const promises = unknownProfileUsers.map(u => this.doInvite(u, true));
|
||||
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
||||
};
|
||||
|
||||
|
@ -219,15 +246,17 @@ export default class MultiInviter {
|
|||
return;
|
||||
}
|
||||
|
||||
const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
|
||||
console.log("Showing failed to invite dialog...");
|
||||
Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, {
|
||||
unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}),
|
||||
unknownProfileUsers: unknownProfileUsers.map(u => ({
|
||||
userId: u,
|
||||
errorText: this.errors[u].errorText,
|
||||
})),
|
||||
onInviteAnyways: () => inviteUnknowns(),
|
||||
onGiveUp: () => {
|
||||
// Fake all the completion states because we already warned the user
|
||||
for (const addr of unknownProfileUsers) {
|
||||
this.completionStates[addr] = 'invited';
|
||||
this.completionStates[addr] = InviteState.Invited;
|
||||
}
|
||||
this.deferred.resolve(this.completionStates);
|
||||
},
|
||||
|
@ -239,25 +268,25 @@ export default class MultiInviter {
|
|||
return;
|
||||
}
|
||||
|
||||
const addr = this.addrs[nextIndex];
|
||||
const addr = this.addresses[nextIndex];
|
||||
|
||||
// don't try to invite it if it's an invalid address
|
||||
// (it will already be marked as an error though,
|
||||
// so no need to do so again)
|
||||
if (getAddressType(addr) === null) {
|
||||
this._inviteMore(nextIndex + 1);
|
||||
this.inviteMore(nextIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// don't re-invite (there's no way in the UI to do this, but
|
||||
// for sanity's sake)
|
||||
if (this.completionStates[addr] === 'invited') {
|
||||
this._inviteMore(nextIndex + 1);
|
||||
if (this.completionStates[addr] === InviteState.Invited) {
|
||||
this.inviteMore(nextIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
this._doInvite(addr, ignoreProfile).then(() => {
|
||||
this._inviteMore(nextIndex + 1, ignoreProfile);
|
||||
this.doInvite(addr, ignoreProfile).then(() => {
|
||||
this.inviteMore(nextIndex + 1, ignoreProfile);
|
||||
}).catch(() => this.deferred.resolve(this.completionStates));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue