mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
Merge remote-tracking branch 'upstream/develop' into ts/entity-and-member-tile
This commit is contained in:
commit
f0dbc974fa
@ -79,7 +79,7 @@
|
||||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.20",
|
||||
"matrix-js-sdk": "12.0.0",
|
||||
"matrix-widget-api": "^0.1.0-beta.14",
|
||||
"matrix-widget-api": "^0.1.0-beta.15",
|
||||
"minimist": "^1.2.5",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
@ -172,7 +172,7 @@
|
||||
"jest": {
|
||||
"testEnvironment": "./__test-utils__/environment.js",
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.[jt]s"
|
||||
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||
],
|
||||
"setupFiles": [
|
||||
"jest-canvas-mock"
|
||||
|
@ -71,7 +71,7 @@ limitations under the License.
|
||||
&::before {
|
||||
background-color: #ffffff;
|
||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
mask-size: 90%;
|
||||
mask-size: 80%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
|
@ -295,6 +295,7 @@ limitations under the License.
|
||||
|
||||
.mx_InviteDialog_content {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,3 +317,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ limitations under the License.
|
||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 90%;
|
||||
mask-size: 80%;
|
||||
}
|
||||
|
||||
&.mx_cryptoEvent_icon::after {
|
||||
|
@ -45,7 +45,7 @@ limitations under the License.
|
||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 90%;
|
||||
mask-size: 80%;
|
||||
}
|
||||
|
||||
// transparent-looking border surrounding the shield for when overlain over avatars
|
||||
@ -59,7 +59,7 @@ limitations under the License.
|
||||
}
|
||||
// shrink the infill of the badge
|
||||
&::before {
|
||||
mask-size: 65%;
|
||||
mask-size: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -345,7 +345,7 @@ $hover-select-border: 4px;
|
||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 90%;
|
||||
mask-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,20 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
declare module "diff-dom" {
|
||||
enum Action {
|
||||
AddElement = "addElement",
|
||||
AddTextElement = "addTextElement",
|
||||
RemoveTextElement = "removeTextElement",
|
||||
RemoveElement = "removeElement",
|
||||
ReplaceElement = "replaceElement",
|
||||
ModifyTextElement = "modifyTextElement",
|
||||
AddAttribute = "addAttribute",
|
||||
RemoveAttribute = "removeAttribute",
|
||||
ModifyAttribute = "modifyAttribute",
|
||||
}
|
||||
|
||||
export interface IDiff {
|
||||
action: Action;
|
||||
action: string;
|
||||
name: string;
|
||||
text?: string;
|
||||
route: number[];
|
||||
|
@ -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 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 from './utils/MultiInviter';
|
||||
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 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;
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
|
||||
import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
@ -28,6 +28,7 @@ import AccessSecretStorageDialog from './components/views/dialogs/security/Acces
|
||||
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
|
||||
|
||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||
// only meant to act as a cache to avoid prompting the user multiple times
|
||||
@ -244,7 +245,7 @@ async function onSecretRequested(
|
||||
deviceId: string,
|
||||
requestId: string,
|
||||
name: string,
|
||||
deviceTrust: IDeviceTrustLevel,
|
||||
deviceTrust: DeviceTrustLevel,
|
||||
): Promise<string> {
|
||||
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
||||
const client = MatrixClientPeg.get();
|
||||
|
@ -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";
|
||||
|
@ -179,7 +179,7 @@ export default class MessageContextMenu extends React.Component {
|
||||
pinnedIds.push(eventId);
|
||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||
event_ids: [
|
||||
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
|
||||
...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []),
|
||||
eventId,
|
||||
],
|
||||
});
|
||||
|
@ -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';
|
||||
|
@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
||||
render() {
|
||||
const cli = this.context;
|
||||
const room = this.props.room;
|
||||
const inRoomChannel = cli.crypto._inRoomVerificationRequests;
|
||||
const inRoomChannel = cli.crypto.inRoomVerificationRequests;
|
||||
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||
|
||||
return (<div>
|
||||
|
@ -32,9 +32,17 @@ import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import { humanizeTime } from "../../../utils/humanize";
|
||||
import createRoom, {
|
||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||
canEncryptToAllUsers,
|
||||
ensureDMExists,
|
||||
findDMForUser,
|
||||
privateShouldBeEncrypted,
|
||||
} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {
|
||||
IInviteResult,
|
||||
inviteMultipleToRoom,
|
||||
showAnyInviteErrors,
|
||||
showCommunityInviteDialog,
|
||||
} from "../../../RoomInvite";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
@ -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,6 +110,7 @@ class DirectoryMember extends Member {
|
||||
private readonly displayName: string;
|
||||
private readonly avatarUrl: string;
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
|
||||
state: IState = {
|
||||
disabledButtonIds: [],
|
||||
disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
|
||||
.map(b => b.id),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -20,7 +20,7 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { UserAddressType } from '../../../UserAddress.js';
|
||||
import { UserAddressType } from '../../../UserAddress';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -2,6 +2,7 @@
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -21,9 +22,8 @@ import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import rate_limited_func from "../../../ratelimitedfunc";
|
||||
import rateLimitedFunction from "../../../ratelimitedfunc";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
@ -31,6 +31,18 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import TruncatedList from '../elements/TruncatedList';
|
||||
import Spinner from "../elements/Spinner";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import EntityTile from "./EntityTile";
|
||||
import MemberTile from "./MemberTile";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
@ -40,41 +52,59 @@ const SHOW_MORE_INCREMENT = 100;
|
||||
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
||||
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
loading: boolean;
|
||||
members: Array<RoomMember>;
|
||||
filteredJoinedMembers: Array<RoomMember>;
|
||||
filteredInvitedMembers: Array<RoomMember | MatrixEvent>;
|
||||
canInvite: boolean;
|
||||
truncateAtJoined: number;
|
||||
truncateAtInvited: number;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.MemberList")
|
||||
export default class MemberList extends React.Component {
|
||||
export default class MemberList extends React.Component<IProps, IState> {
|
||||
private showPresence = true;
|
||||
private mounted = false;
|
||||
private collator: Intl.Collator;
|
||||
private sortNames = new Map<RoomMember, string>(); // RoomMember -> sortName
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
// show an empty list
|
||||
this.state = this._getMembersState([]);
|
||||
this.state = this.getMembersState([]);
|
||||
} else {
|
||||
this.state = this._getMembersState(this.roomMembers());
|
||||
this.state = this.getMembersState(this.roomMembers());
|
||||
}
|
||||
|
||||
cli.on("Room", this.onRoom); // invites & joining after peek
|
||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||
this._showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
||||
this._showPresence = enablePresenceByHsUrl[hsUrl];
|
||||
}
|
||||
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
this._mounted = true;
|
||||
this.mounted = true;
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
this.showMembersAccordingToMembershipWithLL();
|
||||
cli.on("Room.myMembership", this.onMyMembership);
|
||||
} else {
|
||||
this._listenForMembersChanges();
|
||||
this.listenForMembersChanges();
|
||||
}
|
||||
}
|
||||
|
||||
_listenForMembersChanges() {
|
||||
private listenForMembersChanges(): void {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
@ -89,7 +119,7 @@ export default class MemberList extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
this.mounted = false;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
@ -103,7 +133,7 @@ export default class MemberList extends React.Component {
|
||||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateList.cancelPendingCall();
|
||||
this.updateList.cancelPendingCall();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,7 +141,7 @@ export default class MemberList extends React.Component {
|
||||
* show a spinner and load the members if the user is joined,
|
||||
* or show the members available so far if the user is invited
|
||||
*/
|
||||
async _showMembersAccordingToMembershipWithLL() {
|
||||
private async showMembersAccordingToMembershipWithLL(): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
@ -122,31 +152,31 @@ export default class MemberList extends React.Component {
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
} catch (ex) {/* already logged in RoomView */}
|
||||
if (this._mounted) {
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
this._listenForMembersChanges();
|
||||
if (this.mounted) {
|
||||
this.setState(this.getMembersState(this.roomMembers()));
|
||||
this.listenForMembersChanges();
|
||||
}
|
||||
} else {
|
||||
// show the members we already have loaded
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
this.setState(this.getMembersState(this.roomMembers()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get canInvite() {
|
||||
private get canInvite(): boolean {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
return room && room.canInvite(cli.getUserId());
|
||||
}
|
||||
|
||||
_getMembersState(members) {
|
||||
// set the state after determining _showPresence to make sure it's
|
||||
// taken into account while rerendering
|
||||
private getMembersState(members: Array<RoomMember>): IState {
|
||||
// set the state after determining showPresence to make sure it's
|
||||
// taken into account while rendering
|
||||
return {
|
||||
loading: false,
|
||||
members: members,
|
||||
filteredJoinedMembers: this._filterMembers(members, 'join'),
|
||||
filteredInvitedMembers: this._filterMembers(members, 'invite'),
|
||||
filteredJoinedMembers: this.filterMembers(members, 'join'),
|
||||
filteredInvitedMembers: this.filterMembers(members, 'invite'),
|
||||
canInvite: this.canInvite,
|
||||
|
||||
// ideally we'd size this to the page height, but
|
||||
@ -157,72 +187,72 @@ export default class MemberList extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
onUserPresenceChange = (event, user) => {
|
||||
private onUserPresenceChange = (event: MatrixEvent, user: User): void => {
|
||||
// Attach a SINGLE listener for global presence changes then locate the
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
// ever attaching their own listener.
|
||||
const tile = this.refs[user.userId];
|
||||
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
|
||||
if (tile) {
|
||||
this._updateList(); // reorder the membership list
|
||||
this.updateList(); // reorder the membership list
|
||||
}
|
||||
};
|
||||
|
||||
onRoom = room => {
|
||||
private onRoom = (room: Room): void => {
|
||||
if (room.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
// We listen for room events because when we accept an invite
|
||||
// we need to wait till the room is fully populated with state
|
||||
// before refreshing the member list else we get a stale list.
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
this.showMembersAccordingToMembershipWithLL();
|
||||
};
|
||||
|
||||
onMyMembership = (room, membership, oldMembership) => {
|
||||
private onMyMembership = (room: Room, membership: string, oldMembership: string): void => {
|
||||
if (room.roomId === this.props.roomId && membership === "join") {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
this.showMembersAccordingToMembershipWithLL();
|
||||
}
|
||||
};
|
||||
|
||||
onRoomStateMember = (ev, state, member) => {
|
||||
private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
this.updateList();
|
||||
};
|
||||
|
||||
onRoomMemberName = (ev, member) => {
|
||||
private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
this.updateList();
|
||||
};
|
||||
|
||||
onRoomStateEvent = (event, state) => {
|
||||
private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => {
|
||||
if (event.getRoomId() === this.props.roomId &&
|
||||
event.getType() === "m.room.third_party_invite") {
|
||||
this._updateList();
|
||||
this.updateList();
|
||||
}
|
||||
|
||||
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
||||
};
|
||||
|
||||
_updateList = rate_limited_func(() => {
|
||||
this._updateListNow();
|
||||
private updateList = rateLimitedFunction(() => {
|
||||
this.updateListNow();
|
||||
}, 500);
|
||||
|
||||
_updateListNow() {
|
||||
// console.log("Updating memberlist");
|
||||
const newState = {
|
||||
private updateListNow(): void {
|
||||
const members = this.roomMembers()
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
members: this.roomMembers(),
|
||||
};
|
||||
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
|
||||
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery);
|
||||
this.setState(newState);
|
||||
members: members,
|
||||
filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery),
|
||||
filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery),
|
||||
});
|
||||
}
|
||||
|
||||
getMembersWithUser() {
|
||||
private getMembersWithUser(): Array<RoomMember> {
|
||||
if (!this.props.roomId) return [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
@ -230,15 +260,18 @@ export default class MemberList extends React.Component {
|
||||
|
||||
const allMembers = Object.values(room.currentState.members);
|
||||
|
||||
allMembers.forEach(function(member) {
|
||||
allMembers.forEach((member) => {
|
||||
// work around a race where you might have a room member object
|
||||
// before the user object exists. This may or may not cause
|
||||
// https://github.com/vector-im/vector-web/issues/186
|
||||
if (member.user === null) {
|
||||
if (!member.user) {
|
||||
member.user = cli.getUser(member.userId);
|
||||
}
|
||||
|
||||
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
|
||||
this.sortNames.set(
|
||||
member,
|
||||
(member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""),
|
||||
);
|
||||
|
||||
// XXX: this user may have no lastPresenceTs value!
|
||||
// the right solution here is to fix the race rather than leave it as 0
|
||||
@ -247,7 +280,7 @@ export default class MemberList extends React.Component {
|
||||
return allMembers;
|
||||
}
|
||||
|
||||
roomMembers() {
|
||||
private roomMembers(): Array<RoomMember> {
|
||||
const allMembers = this.getMembersWithUser();
|
||||
const filteredAndSortedMembers = allMembers.filter((m) => {
|
||||
return (
|
||||
@ -255,23 +288,21 @@ export default class MemberList extends React.Component {
|
||||
);
|
||||
});
|
||||
const language = SettingsStore.getValue("language");
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', ignorePunctuation: false });
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
return filteredAndSortedMembers;
|
||||
}
|
||||
|
||||
_createOverflowTileJoined = (overflowCount, totalCount) => {
|
||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
|
||||
private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||
return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList);
|
||||
};
|
||||
|
||||
_createOverflowTileInvited = (overflowCount, totalCount) => {
|
||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
|
||||
private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||
return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList);
|
||||
};
|
||||
|
||||
_createOverflowTile = (overflowCount, totalCount, onClick) => {
|
||||
private createOverflowTile = (overflowCount: number, totalCount: number, onClick: () => void): JSX.Element=> {
|
||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
@ -281,31 +312,48 @@ export default class MemberList extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
_showMoreJoinedMemberList = () => {
|
||||
private showMoreJoinedMemberList = (): void => {
|
||||
this.setState({
|
||||
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
||||
});
|
||||
};
|
||||
|
||||
_showMoreInvitedMemberList = () => {
|
||||
private showMoreInvitedMemberList = (): void => {
|
||||
this.setState({
|
||||
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
||||
});
|
||||
};
|
||||
|
||||
memberString(member) {
|
||||
/**
|
||||
* SHOULD ONLY BE USED BY TESTS
|
||||
*/
|
||||
public memberString(member: RoomMember): string {
|
||||
if (!member) {
|
||||
return "(null)";
|
||||
} else {
|
||||
const u = member.user;
|
||||
return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "<null>") + ", " + (u ? u.getLastActiveTs() : "<null>") + ", " + (u ? u.currentlyActive : "<null>") + ", " + (u ? u.presence : "<null>") + ")";
|
||||
return (
|
||||
"(" +
|
||||
member.name +
|
||||
", " +
|
||||
member.powerLevel +
|
||||
", " +
|
||||
(u ? u.lastActiveAgo : "<null>") +
|
||||
", " +
|
||||
(u ? u.getLastActiveTs() : "<null>") +
|
||||
", " +
|
||||
(u ? u.currentlyActive : "<null>") +
|
||||
", " +
|
||||
(u ? u.presence : "<null>") +
|
||||
")"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// returns negative if a comes before b,
|
||||
// returns 0 if a and b are equivalent in ordering
|
||||
// returns positive if a comes after b.
|
||||
memberSort = (memberA, memberB) => {
|
||||
private memberSort = (memberA: RoomMember, memberB: RoomMember): number => {
|
||||
// order by presence, with "active now" first.
|
||||
// ...and then by power level
|
||||
// ...and then by last active
|
||||
@ -325,7 +373,7 @@ export default class MemberList extends React.Component {
|
||||
if (!userA && userB) return 1;
|
||||
|
||||
// First by presence
|
||||
if (this._showPresence) {
|
||||
if (this.showPresence) {
|
||||
const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
|
||||
const presenceIndex = p => {
|
||||
const order = ['active', 'online', 'offline'];
|
||||
@ -349,31 +397,31 @@ export default class MemberList extends React.Component {
|
||||
}
|
||||
|
||||
// Third by last active
|
||||
if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
||||
if (this.showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
||||
// console.log("Comparing on last active timestamp - returning");
|
||||
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
||||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
return this.collator.compare(memberA.sortName, memberB.sortName);
|
||||
return this.collator.compare(this.sortNames.get(memberA), this.sortNames.get(memberB));
|
||||
};
|
||||
|
||||
onSearchQueryChanged = searchQuery => {
|
||||
private onSearchQueryChanged = (searchQuery: string): void => {
|
||||
this.setState({
|
||||
searchQuery,
|
||||
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery),
|
||||
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery),
|
||||
filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery),
|
||||
filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery),
|
||||
});
|
||||
};
|
||||
|
||||
_onPending3pidInviteClick = inviteEvent => {
|
||||
private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
|
||||
dis.dispatch({
|
||||
action: 'view_3pid_invite',
|
||||
event: inviteEvent,
|
||||
});
|
||||
};
|
||||
|
||||
_filterMembers(members, membership, query) {
|
||||
private filterMembers(members: Array<RoomMember>, membership: string, query?: string): Array<RoomMember> {
|
||||
return members.filter((m) => {
|
||||
if (query) {
|
||||
query = query.toLowerCase();
|
||||
@ -389,7 +437,7 @@ export default class MemberList extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
_getPending3PidInvites() {
|
||||
private getPending3PidInvites(): Array<MatrixEvent> {
|
||||
// include 3pid invites (m.room.third_party_invite) state events.
|
||||
// The HS may have already converted these into m.room.member invites so
|
||||
// we shouldn't add them if the 3pid invite state key (token) is in the
|
||||
@ -409,42 +457,40 @@ export default class MemberList extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_makeMemberTiles(members) {
|
||||
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
|
||||
private makeMemberTiles(members: Array<RoomMember | MatrixEvent>) {
|
||||
return members.map((m) => {
|
||||
if (m.userId) {
|
||||
if (m instanceof RoomMember) {
|
||||
// Is a Matrix invite
|
||||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this._showPresence} />;
|
||||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
|
||||
} else {
|
||||
// Is a 3pid invite
|
||||
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
|
||||
onClick={() => this._onPending3pidInviteClick(m)} />;
|
||||
onClick={() => this.onPending3pidInviteClick(m)} />;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getChildrenJoined = (start, end) => this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
|
||||
|
||||
_getChildCountJoined = () => this.state.filteredJoinedMembers.length;
|
||||
|
||||
_getChildrenInvited = (start, end) => {
|
||||
let targets = this.state.filteredInvitedMembers;
|
||||
if (end > this.state.filteredInvitedMembers.length) {
|
||||
targets = targets.concat(this._getPending3PidInvites());
|
||||
}
|
||||
|
||||
return this._makeMemberTiles(targets.slice(start, end));
|
||||
private getChildrenJoined = (start: number, end: number): Array<JSX.Element> => {
|
||||
return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end))
|
||||
};
|
||||
|
||||
_getChildCountInvited = () => {
|
||||
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length;
|
||||
private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length;
|
||||
|
||||
private getChildrenInvited = (start: number, end: number): Array<JSX.Element> => {
|
||||
let targets = this.state.filteredInvitedMembers;
|
||||
if (end > this.state.filteredInvitedMembers.length) {
|
||||
targets = targets.concat(this.getPending3PidInvites());
|
||||
}
|
||||
|
||||
return this.makeMemberTiles(targets.slice(start, end));
|
||||
};
|
||||
|
||||
private getChildCountInvited = (): number => {
|
||||
return this.state.filteredInvitedMembers.length + (this.getPending3PidInvites() || []).length;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <BaseCard
|
||||
className="mx_MemberList"
|
||||
onClose={this.props.onClose}
|
||||
@ -454,9 +500,6 @@ export default class MemberList extends React.Component {
|
||||
</BaseCard>;
|
||||
}
|
||||
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
let inviteButton;
|
||||
@ -470,22 +513,30 @@ export default class MemberList extends React.Component {
|
||||
inviteButtonText = _t("Invite to this space");
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
inviteButton =
|
||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!this.state.canInvite}>
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
className="mx_MemberList_invite"
|
||||
onClick={this.onInviteButtonClick}
|
||||
disabled={!this.state.canInvite}
|
||||
>
|
||||
<span>{ inviteButtonText }</span>
|
||||
</AccessibleButton>;
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let invitedHeader;
|
||||
let invitedSection;
|
||||
if (this._getChildCountInvited() > 0) {
|
||||
if (this.getChildCountInvited() > 0) {
|
||||
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
||||
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited}
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
invitedSection = (
|
||||
<TruncatedList
|
||||
className="mx_MemberList_section mx_MemberList_invited"
|
||||
truncateAt={this.state.truncateAtInvited}
|
||||
createOverflowElement={this.createOverflowTileInvited}
|
||||
getChildren={this.getChildrenInvited}
|
||||
getChildCount={this.getChildCountInvited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const footer = (
|
||||
@ -517,17 +568,19 @@ export default class MemberList extends React.Component {
|
||||
previousPhase={previousPhase}
|
||||
>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
<TruncatedList
|
||||
className="mx_MemberList_section mx_MemberList_joined"
|
||||
truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this.createOverflowTileJoined}
|
||||
getChildren={this.getChildrenJoined}
|
||||
getChildCount={this.getChildCountJoined} />
|
||||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
</BaseCard>;
|
||||
}
|
||||
|
||||
onInviteButtonClick = () => {
|
||||
onInviteButtonClick = (): void => {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||
async _getUpdatedStatus() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const pkCache = cli.getCrossSigningCacheCallbacks();
|
||||
const crossSigning = cli.crypto._crossSigningInfo;
|
||||
const secretStorage = cli.crypto._secretStorage;
|
||||
const crossSigning = cli.crypto.crossSigningInfo;
|
||||
const secretStorage = cli.crypto.secretStorage;
|
||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||
|
@ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||
|
||||
async _getUpdatedDiagnostics() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secretStorage = cli.crypto._secretStorage;
|
||||
const secretStorage = cli.crypto.secretStorage;
|
||||
|
||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
||||
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
|
||||
|
@ -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.",
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
// The following interfaces take their names and member names from seshat and the spec
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export interface MatrixEvent {
|
||||
export interface IMatrixEvent {
|
||||
type: string;
|
||||
sender: string;
|
||||
content: {};
|
||||
@ -27,37 +27,37 @@ export interface MatrixEvent {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export interface MatrixProfile {
|
||||
export interface IMatrixProfile {
|
||||
avatar_url: string;
|
||||
displayname: string;
|
||||
}
|
||||
|
||||
export interface CrawlerCheckpoint {
|
||||
export interface ICrawlerCheckpoint {
|
||||
roomId: string;
|
||||
token: string;
|
||||
fullCrawl?: boolean;
|
||||
direction: string;
|
||||
}
|
||||
|
||||
export interface ResultContext {
|
||||
events_before: [MatrixEvent];
|
||||
events_after: [MatrixEvent];
|
||||
profile_info: Map<string, MatrixProfile>;
|
||||
export interface IResultContext {
|
||||
events_before: [IMatrixEvent];
|
||||
events_after: [IMatrixEvent];
|
||||
profile_info: Map<string, IMatrixProfile>;
|
||||
}
|
||||
|
||||
export interface ResultsElement {
|
||||
export interface IResultsElement {
|
||||
rank: number;
|
||||
result: MatrixEvent;
|
||||
context: ResultContext;
|
||||
result: IMatrixEvent;
|
||||
context: IResultContext;
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
export interface ISearchResult {
|
||||
count: number;
|
||||
results: [ResultsElement];
|
||||
results: [IResultsElement];
|
||||
highlights: [string];
|
||||
}
|
||||
|
||||
export interface SearchArgs {
|
||||
export interface ISearchArgs {
|
||||
search_term: string;
|
||||
before_limit: number;
|
||||
after_limit: number;
|
||||
@ -65,19 +65,19 @@ export interface SearchArgs {
|
||||
room_id?: string;
|
||||
}
|
||||
|
||||
export interface EventAndProfile {
|
||||
event: MatrixEvent;
|
||||
profile: MatrixProfile;
|
||||
export interface IEventAndProfile {
|
||||
event: IMatrixEvent;
|
||||
profile: IMatrixProfile;
|
||||
}
|
||||
|
||||
export interface LoadArgs {
|
||||
export interface ILoadArgs {
|
||||
roomId: string;
|
||||
limit: number;
|
||||
fromEvent?: string;
|
||||
direction?: string;
|
||||
}
|
||||
|
||||
export interface IndexStats {
|
||||
export interface IIndexStats {
|
||||
size: number;
|
||||
eventCount: number;
|
||||
roomCount: number;
|
||||
@ -119,13 +119,13 @@ export default abstract class BaseEventIndexManager {
|
||||
* Queue up an event to be added to the index.
|
||||
*
|
||||
* @param {MatrixEvent} ev The event that should be added to the index.
|
||||
* @param {MatrixProfile} profile The profile of the event sender at the
|
||||
* @param {IMatrixProfile} profile The profile of the event sender at the
|
||||
* time of the event receival.
|
||||
*
|
||||
* @return {Promise} A promise that will resolve when the was queued up for
|
||||
* addition.
|
||||
*/
|
||||
async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<void> {
|
||||
async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
@ -160,10 +160,10 @@ export default abstract class BaseEventIndexManager {
|
||||
/**
|
||||
* Get statistical information of the index.
|
||||
*
|
||||
* @return {Promise<IndexStats>} A promise that will resolve to the index
|
||||
* @return {Promise<IIndexStats>} A promise that will resolve to the index
|
||||
* statistics.
|
||||
*/
|
||||
async getStats(): Promise<IndexStats> {
|
||||
async getStats(): Promise<IIndexStats> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
@ -203,13 +203,13 @@ export default abstract class BaseEventIndexManager {
|
||||
/**
|
||||
* Search the event index using the given term for matching events.
|
||||
*
|
||||
* @param {SearchArgs} searchArgs The search configuration for the search,
|
||||
* @param {ISearchArgs} searchArgs The search configuration for the search,
|
||||
* sets the search term and determines the search result contents.
|
||||
*
|
||||
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
||||
* @return {Promise<[ISearchResult]>} A promise that will resolve to an array
|
||||
* of search results once the search is done.
|
||||
*/
|
||||
async searchEventIndex(searchArgs: SearchArgs): Promise<SearchResult> {
|
||||
async searchEventIndex(searchArgs: ISearchArgs): Promise<ISearchResult> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
@ -218,12 +218,12 @@ export default abstract class BaseEventIndexManager {
|
||||
*
|
||||
* This is used to add a batch of events to the index.
|
||||
*
|
||||
* @param {[EventAndProfile]} events The list of events and profiles that
|
||||
* @param {[IEventAndProfile]} events The list of events and profiles that
|
||||
* should be added to the event index.
|
||||
* @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that
|
||||
* @param {[ICrawlerCheckpoint]} checkpoint A new crawler checkpoint that
|
||||
* should be stored in the index which should be used to continue crawling
|
||||
* the room.
|
||||
* @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used
|
||||
* @param {[ICrawlerCheckpoint]} oldCheckpoint The checkpoint that was used
|
||||
* to fetch the current batch of events. This checkpoint will be removed
|
||||
* from the index.
|
||||
*
|
||||
@ -231,9 +231,9 @@ export default abstract class BaseEventIndexManager {
|
||||
* were already added to the index, false otherwise.
|
||||
*/
|
||||
async addHistoricEvents(
|
||||
events: [EventAndProfile],
|
||||
checkpoint: CrawlerCheckpoint | null,
|
||||
oldCheckpoint: CrawlerCheckpoint | null,
|
||||
events: IEventAndProfile[],
|
||||
checkpoint: ICrawlerCheckpoint | null,
|
||||
oldCheckpoint: ICrawlerCheckpoint | null,
|
||||
): Promise<boolean> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
@ -241,36 +241,36 @@ export default abstract class BaseEventIndexManager {
|
||||
/**
|
||||
* Add a new crawler checkpoint to the index.
|
||||
*
|
||||
* @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added
|
||||
* @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be added
|
||||
* to the index.
|
||||
*
|
||||
* @return {Promise} A promise that will resolve once the checkpoint has
|
||||
* been stored.
|
||||
*/
|
||||
async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> {
|
||||
async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new crawler checkpoint to the index.
|
||||
*
|
||||
* @param {CrawlerCheckpoint} checkpoint The checkpoint that should be
|
||||
* @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be
|
||||
* removed from the index.
|
||||
*
|
||||
* @return {Promise} A promise that will resolve once the checkpoint has
|
||||
* been removed.
|
||||
*/
|
||||
async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> {
|
||||
async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the stored checkpoints from the index.
|
||||
*
|
||||
* @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an
|
||||
* @return {Promise<[ICrawlerCheckpoint]>} A promise that will resolve to an
|
||||
* array of crawler checkpoints once they have been loaded from the index.
|
||||
*/
|
||||
async loadCheckpoints(): Promise<[CrawlerCheckpoint]> {
|
||||
async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
@ -286,11 +286,11 @@ export default abstract class BaseEventIndexManager {
|
||||
* @param {string} args.direction The direction to which we should continue
|
||||
* loading events from. This is used only if fromEvent is used as well.
|
||||
*
|
||||
* @return {Promise<[EventAndProfile]>} A promise that will resolve to an
|
||||
* @return {Promise<[IEventAndProfile]>} A promise that will resolve to an
|
||||
* array of Matrix events that contain mxc URLs accompanied with the
|
||||
* historic profile of the sender.
|
||||
*/
|
||||
async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> {
|
||||
async loadFileEvents(args: ILoadArgs): Promise<IEventAndProfile[]> {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { sleep } from "../utils/promise";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { SettingLevel } from "../settings/SettingLevel";
|
||||
import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager";
|
||||
import { ICrawlerCheckpoint, ILoadArgs, ISearchArgs } from "./BaseEventIndexManager";
|
||||
|
||||
// The time in ms that the crawler will wait loop iterations if there
|
||||
// have not been any checkpoints to consume in the last iteration.
|
||||
@ -45,9 +45,9 @@ interface ICrawler {
|
||||
* Event indexing class that wraps the platform specific event indexing.
|
||||
*/
|
||||
export default class EventIndex extends EventEmitter {
|
||||
private crawlerCheckpoints: CrawlerCheckpoint[] = [];
|
||||
private crawlerCheckpoints: ICrawlerCheckpoint[] = [];
|
||||
private crawler: ICrawler = null;
|
||||
private currentCheckpoint: CrawlerCheckpoint = null;
|
||||
private currentCheckpoint: ICrawlerCheckpoint = null;
|
||||
|
||||
public async init() {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
@ -111,14 +111,14 @@ export default class EventIndex extends EventEmitter {
|
||||
const timeline = room.getLiveTimeline();
|
||||
const token = timeline.getPaginationToken("b");
|
||||
|
||||
const backCheckpoint: CrawlerCheckpoint = {
|
||||
const backCheckpoint: ICrawlerCheckpoint = {
|
||||
roomId: room.roomId,
|
||||
token: token,
|
||||
direction: "b",
|
||||
fullCrawl: true,
|
||||
};
|
||||
|
||||
const forwardCheckpoint: CrawlerCheckpoint = {
|
||||
const forwardCheckpoint: ICrawlerCheckpoint = {
|
||||
roomId: room.roomId,
|
||||
token: token,
|
||||
direction: "f",
|
||||
@ -668,13 +668,13 @@ export default class EventIndex extends EventEmitter {
|
||||
/**
|
||||
* Search the event index using the given term for matching events.
|
||||
*
|
||||
* @param {SearchArgs} searchArgs The search configuration for the search,
|
||||
* @param {ISearchArgs} searchArgs The search configuration for the search,
|
||||
* sets the search term and determines the search result contents.
|
||||
*
|
||||
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
||||
* of search results once the search is done.
|
||||
*/
|
||||
public async search(searchArgs: SearchArgs) {
|
||||
public async search(searchArgs: ISearchArgs) {
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
return indexManager.searchEventIndex(searchArgs);
|
||||
}
|
||||
@ -709,7 +709,7 @@ export default class EventIndex extends EventEmitter {
|
||||
const client = MatrixClientPeg.get();
|
||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||
|
||||
const loadArgs: LoadArgs = {
|
||||
const loadArgs: ILoadArgs = {
|
||||
roomId: room.roomId,
|
||||
limit: limit,
|
||||
};
|
||||
|
@ -86,8 +86,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
|
||||
body.append('cross_signing_key', client.getCrossSigningId());
|
||||
|
||||
// add cross-signing status information
|
||||
const crossSigning = client.crypto._crossSigningInfo;
|
||||
const secretStorage = client.crypto._secretStorage;
|
||||
const crossSigning = client.crypto.crossSigningInfo;
|
||||
const secretStorage = client.crypto.secretStorage;
|
||||
|
||||
body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
|
||||
body.append("cross_signing_supported_by_hs",
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||
import React, { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
|
||||
import { Action, DiffDOM, IDiff } from "diff-dom";
|
||||
import { DiffDOM, IDiff } from "diff-dom";
|
||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
|
||||
@ -149,7 +149,7 @@ function stringAsTextNode(string: string): Text {
|
||||
function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void {
|
||||
const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route);
|
||||
switch (diff.action) {
|
||||
case Action.ReplaceElement: {
|
||||
case "replaceElement": {
|
||||
const container = document.createElement("span");
|
||||
const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue));
|
||||
const insNode = wrapInsertion(diffTreeToDOM(diff.newValue));
|
||||
@ -158,17 +158,17 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
||||
refNode.parentNode.replaceChild(container, refNode);
|
||||
break;
|
||||
}
|
||||
case Action.RemoveTextElement: {
|
||||
case "removeTextElement": {
|
||||
const delNode = wrapDeletion(stringAsTextNode(diff.value));
|
||||
refNode.parentNode.replaceChild(delNode, refNode);
|
||||
break;
|
||||
}
|
||||
case Action.RemoveElement: {
|
||||
case "removeElement": {
|
||||
const delNode = wrapDeletion(diffTreeToDOM(diff.element));
|
||||
refNode.parentNode.replaceChild(delNode, refNode);
|
||||
break;
|
||||
}
|
||||
case Action.ModifyTextElement: {
|
||||
case "modifyTextElement": {
|
||||
const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue);
|
||||
diffMathPatch.diff_cleanupSemantic(textDiffs);
|
||||
const container = document.createElement("span");
|
||||
@ -184,12 +184,12 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
||||
refNode.parentNode.replaceChild(container, refNode);
|
||||
break;
|
||||
}
|
||||
case Action.AddElement: {
|
||||
case "addElement": {
|
||||
const insNode = wrapInsertion(diffTreeToDOM(diff.element));
|
||||
insertBefore(refParentNode, refNode, insNode);
|
||||
break;
|
||||
}
|
||||
case Action.AddTextElement: {
|
||||
case "addTextElement": {
|
||||
// XXX: sometimes diffDOM says insert a newline when there shouldn't be one
|
||||
// but we must insert the node anyway so that we don't break the route child IDs.
|
||||
// See https://github.com/fiduswriter/diffDOM/issues/100
|
||||
@ -199,9 +199,9 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
||||
}
|
||||
// e.g. when changing a the href of a link,
|
||||
// show the link with old href as removed and with the new href as added
|
||||
case Action.RemoveAttribute:
|
||||
case Action.AddAttribute:
|
||||
case Action.ModifyAttribute: {
|
||||
case "removeAttribute":
|
||||
case "addAttribute":
|
||||
case "modifyAttribute": {
|
||||
const delNode = wrapDeletion(refNode.cloneNode(true));
|
||||
const updatedNode = refNode.cloneNode(true) as HTMLElement;
|
||||
if (diff.action === "addAttribute" || diff.action === "modifyAttribute") {
|
||||
|
@ -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 { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import {getAddressType} from '../UserAddress';
|
||||
import { AddressType, getAddressType } from '../UserAddress';
|
||||
import GroupStore from '../stores/GroupStore';
|
||||
import { _t } from "../languageHandler";
|
||||
import * as sdk from "../index";
|
||||
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.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));
|
||||
}
|
||||
}
|
@ -1,21 +1,36 @@
|
||||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactTestUtils from 'react-dom/test-utils';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import * as TestUtils from '../../../test-utils';
|
||||
|
||||
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
|
||||
import sdk from '../../../skinned-sdk';
|
||||
|
||||
import {Room, RoomMember, User} from 'matrix-js-sdk';
|
||||
|
||||
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { compare } from "../../../../src/utils/strings";
|
||||
import MemberList from "../../../../src/components/views/rooms/MemberList";
|
||||
|
||||
function generateRoomId() {
|
||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||
}
|
||||
|
||||
|
||||
describe('MemberList', () => {
|
||||
function createRoom(opts) {
|
||||
const room = new Room(generateRoomId(), null, client.getUserId());
|
||||
@ -97,13 +112,19 @@ describe('MemberList', () => {
|
||||
memberListRoom.currentState.members[member.userId] = member;
|
||||
}
|
||||
|
||||
const MemberList = sdk.getComponent('views.rooms.MemberList');
|
||||
const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList);
|
||||
const gatherWrappedRef = (r) => {
|
||||
memberList = r;
|
||||
};
|
||||
root = ReactDOM.render(<WrappedMemberList roomId={memberListRoom.roomId}
|
||||
wrappedRef={gatherWrappedRef} />, parentDiv);
|
||||
root = ReactDOM.render(
|
||||
(
|
||||
<WrappedMemberList
|
||||
roomId={memberListRoom.roomId}
|
||||
wrappedRef={gatherWrappedRef}
|
||||
/>
|
||||
),
|
||||
parentDiv,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
@ -213,8 +234,8 @@ describe('MemberList', () => {
|
||||
});
|
||||
|
||||
// Bypass all the event listeners and skip to the good part
|
||||
memberList._showPresence = enablePresence;
|
||||
memberList._updateListNow();
|
||||
memberList.showPresence = enablePresence;
|
||||
memberList.updateListNow();
|
||||
|
||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||
@ -225,7 +246,7 @@ describe('MemberList', () => {
|
||||
|
||||
// Bypass all the event listeners and skip to the good part
|
||||
memberList._showPresence = enablePresence;
|
||||
memberList._updateListNow();
|
||||
memberList.updateListNow();
|
||||
|
||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||
@ -254,8 +275,8 @@ describe('MemberList', () => {
|
||||
});
|
||||
|
||||
// Bypass all the event listeners and skip to the good part
|
||||
memberList._showPresence = enablePresence;
|
||||
memberList._updateListNow();
|
||||
memberList.showPresence = enablePresence;
|
||||
memberList.updateListNow();
|
||||
|
||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||
@ -273,8 +294,8 @@ describe('MemberList', () => {
|
||||
});
|
||||
|
||||
// Bypass all the event listeners and skip to the good part
|
||||
memberList._showPresence = enablePresence;
|
||||
memberList._updateListNow();
|
||||
memberList.showPresence = enablePresence;
|
||||
memberList.updateListNow();
|
||||
|
||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
@ -1334,6 +1334,7 @@
|
||||
|
||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
|
||||
version "3.2.3"
|
||||
uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4
|
||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
|
||||
|
||||
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
|
||||
@ -5772,10 +5773,10 @@ matrix-react-test-utils@^0.2.3:
|
||||
"@babel/traverse" "^7.13.17"
|
||||
walk "^2.3.14"
|
||||
|
||||
matrix-widget-api@^0.1.0-beta.14:
|
||||
version "0.1.0-beta.14"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70"
|
||||
integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ==
|
||||
matrix-widget-api@^0.1.0-beta.15:
|
||||
version "0.1.0-beta.15"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745"
|
||||
integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg==
|
||||
dependencies:
|
||||
"@types/events" "^3.0.0"
|
||||
events "^3.2.0"
|
||||
|
Loading…
Reference in New Issue
Block a user