Merge remote-tracking branch 'upstream/develop' into feature/pinned-click
This commit is contained in:
commit
430ae0e79f
23 changed files with 587 additions and 397 deletions
|
@ -172,7 +172,7 @@
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "./__test-utils__/environment.js",
|
"testEnvironment": "./__test-utils__/environment.js",
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
"<rootDir>/test/**/*-test.[jt]s"
|
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||||
],
|
],
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"jest-canvas-mock"
|
"jest-canvas-mock"
|
||||||
|
|
|
@ -295,6 +295,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_InviteDialog_content {
|
.mx_InviteDialog_content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,3 +317,42 @@ limitations under the License.
|
||||||
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
||||||
padding: 0;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,20 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare module "diff-dom" {
|
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 {
|
export interface IDiff {
|
||||||
action: Action;
|
action: string;
|
||||||
name: string;
|
name: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
route: number[];
|
route: number[];
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import MultiInviter from './utils/MultiInviter';
|
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 Modal from './Modal';
|
||||||
import * as sdk from './';
|
import * as sdk from './';
|
||||||
import { _t } from './languageHandler';
|
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 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
|
* Invites multiple addresses to a room
|
||||||
|
@ -32,15 +41,15 @@ import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
||||||
* no option to cancel.
|
* no option to cancel.
|
||||||
*
|
*
|
||||||
* @param {string} roomId The ID of the room to invite to
|
* @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
|
* @returns {Promise} Promise
|
||||||
*/
|
*/
|
||||||
export function inviteMultipleToRoom(roomId, addrs) {
|
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
|
||||||
const inviter = new MultiInviter(roomId);
|
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.
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||||
Modal.createTrackedDialog(
|
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.
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
"Invite Users", "", InviteDialog, {
|
"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(
|
Modal.createTrackedDialog(
|
||||||
'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
|
'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
|
||||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showCommunityInviteDialog(communityId) {
|
export function showCommunityInviteDialog(communityId: string): void {
|
||||||
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
|
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
|
||||||
if (chat) {
|
if (chat) {
|
||||||
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
|
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
|
||||||
|
@ -83,7 +92,7 @@ export function showCommunityInviteDialog(communityId) {
|
||||||
* @param {MatrixEvent} event The event to check
|
* @param {MatrixEvent} event The event to check
|
||||||
* @returns {boolean} True if valid, false otherwise
|
* @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;
|
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
|
// any events without these keys are not valid 3pid invites, so we ignore them
|
||||||
|
@ -96,7 +105,7 @@ export function isValid3pidInvite(event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inviteUsersToRoom(roomId, userIds) {
|
export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
|
||||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
showAnyInviteErrors(result.states, room, result.inviter);
|
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
|
// 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) {
|
if (failedUsers.length === 1 && inviter.fatal) {
|
||||||
// Just get the first message because there was a fatal problem on the first
|
// 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
|
// user. This usually means that no other users were attempted, making it
|
||||||
|
@ -126,19 +140,47 @@ export function showAnyInviteErrors(addrs, room, inviter) {
|
||||||
} else {
|
} else {
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
for (const addr of failedUsers) {
|
for (const addr of failedUsers) {
|
||||||
if (addrs[addr] === "error") {
|
if (states[addr] === "error") {
|
||||||
const reason = inviter.getErrorText(addr);
|
const reason = inviter.getErrorText(addr);
|
||||||
errorList.push(addr + ": " + reason);
|
errorList.push(addr + ": " + reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
if (errorList.length > 0) {
|
if (errorList.length > 0) {
|
||||||
// React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
|
// 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");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
|
Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, {
|
||||||
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
title: _t("Some invites couldn't be sent"),
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './index';
|
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 RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import SecurityCustomisations from "./customisations/Security";
|
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
|
// 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
|
// only meant to act as a cache to avoid prompting the user multiple times
|
||||||
|
@ -244,7 +245,7 @@ async function onSecretRequested(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
name: string,
|
name: string,
|
||||||
deviceTrust: IDeviceTrustLevel,
|
deviceTrust: DeviceTrustLevel,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
|
@ -38,76 +38,89 @@ function textForMemberEvent(ev): () => string | null {
|
||||||
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||||
const prevContent = ev.getPrevContent();
|
const prevContent = ev.getPrevContent();
|
||||||
const content = ev.getContent();
|
const content = ev.getContent();
|
||||||
|
const reason = content.reason;
|
||||||
|
|
||||||
const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
|
||||||
switch (content.membership) {
|
switch (content.membership) {
|
||||||
case 'invite': {
|
case 'invite': {
|
||||||
const threePidContent = content.third_party_invite;
|
const threePidContent = content.third_party_invite;
|
||||||
if (threePidContent) {
|
if (threePidContent) {
|
||||||
if (threePidContent.display_name) {
|
if (threePidContent.display_name) {
|
||||||
return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
return () => _t('%(targetName)s accepted the invitation for %(displayName)s', {
|
||||||
targetName,
|
targetName,
|
||||||
displayName: threePidContent.display_name,
|
displayName: threePidContent.display_name,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return () => _t('%(targetName)s accepted an invitation.', {targetName});
|
return () => _t('%(targetName)s accepted an invitation', { targetName });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s invited %(targetName)s', { senderName, targetName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'ban':
|
case 'ban':
|
||||||
return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
return () => reason
|
||||||
|
? _t('%(senderName)s banned %(targetName)s: %(reason)s', { senderName, targetName, reason })
|
||||||
|
: _t('%(senderName)s banned %(targetName)s', { senderName, targetName });
|
||||||
case 'join':
|
case 'join':
|
||||||
if (prevContent && prevContent.membership === 'join') {
|
if (prevContent && prevContent.membership === 'join') {
|
||||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||||
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s', {
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (!prevContent.displayname && content.displayname) {
|
} else if (!prevContent.displayname && content.displayname) {
|
||||||
return () => _t('%(senderName)s set their display name to %(displayName)s.', {
|
return () => _t('%(senderName)s set their display name to %(displayName)s', {
|
||||||
senderName: ev.getSender(),
|
senderName: ev.getSender(),
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.displayname && !content.displayname) {
|
} else if (prevContent.displayname && !content.displayname) {
|
||||||
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s)', {
|
||||||
senderName,
|
senderName,
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.avatar_url && !content.avatar_url) {
|
} else if (prevContent.avatar_url && !content.avatar_url) {
|
||||||
return () => _t('%(senderName)s removed their profile picture.', {senderName});
|
return () => _t('%(senderName)s removed their profile picture', { senderName });
|
||||||
} else if (prevContent.avatar_url && content.avatar_url &&
|
} else if (prevContent.avatar_url && content.avatar_url &&
|
||||||
prevContent.avatar_url !== content.avatar_url) {
|
prevContent.avatar_url !== content.avatar_url) {
|
||||||
return () => _t('%(senderName)s changed their profile picture.', {senderName});
|
return () => _t('%(senderName)s changed their profile picture', { senderName });
|
||||||
} else if (!prevContent.avatar_url && content.avatar_url) {
|
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||||
return () => _t('%(senderName)s set a profile picture.', {senderName});
|
return () => _t('%(senderName)s set a profile picture', { senderName });
|
||||||
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||||
// This is a null rejoin, it will only be visible if the Labs option is enabled
|
// This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
|
||||||
return () => _t("%(senderName)s made no change.", {senderName});
|
return () => _t("%(senderName)s made no change", { senderName });
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||||
return () => _t('%(targetName)s joined the room.', {targetName});
|
return () => _t('%(targetName)s joined the room', { targetName });
|
||||||
}
|
}
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (ev.getSender() === ev.getStateKey()) {
|
if (ev.getSender() === ev.getStateKey()) {
|
||||||
if (prevContent.membership === "invite") {
|
if (prevContent.membership === "invite") {
|
||||||
return () => _t('%(targetName)s rejected the invitation.', {targetName});
|
return () => _t('%(targetName)s rejected the invitation', { targetName });
|
||||||
} else {
|
} else {
|
||||||
return () => _t('%(targetName)s left the room.', {targetName});
|
return () => reason
|
||||||
|
? _t('%(targetName)s left the room: %(reason)s', { targetName, reason })
|
||||||
|
: _t('%(targetName)s left the room', { targetName });
|
||||||
}
|
}
|
||||||
} else if (prevContent.membership === "ban") {
|
} else if (prevContent.membership === "ban") {
|
||||||
return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s unbanned %(targetName)s', { senderName, targetName });
|
||||||
} else if (prevContent.membership === "invite") {
|
} else if (prevContent.membership === "invite") {
|
||||||
return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
return () => reason
|
||||||
|
? _t('%(senderName)s withdrew %(targetName)s\'s invitation: %(reason)s', {
|
||||||
senderName,
|
senderName,
|
||||||
targetName,
|
targetName,
|
||||||
}) + ' ' + getReason();
|
reason,
|
||||||
|
})
|
||||||
|
: _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName })
|
||||||
} else if (prevContent.membership === "join") {
|
} else if (prevContent.membership === "join") {
|
||||||
return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
return () => reason
|
||||||
|
? _t('%(senderName)s kicked %(targetName)s: %(reason)s', {
|
||||||
|
senderName,
|
||||||
|
targetName,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
: _t('%(senderName)s kicked %(targetName)s', { senderName, targetName });
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||||
const mxUserIdRegex = /^@\S+:\S+$/;
|
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||||
const mxRoomIdRegex = /^!\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
|
// PropType definition for an object describing
|
||||||
// an address that can be invited to a room (which
|
// an address that can be invited to a room (which
|
||||||
|
@ -40,18 +44,13 @@ export const UserAddressType = PropTypes.shape({
|
||||||
isKnown: PropTypes.bool,
|
isKnown: PropTypes.bool,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getAddressType(inputText) {
|
export function getAddressType(inputText: string): AddressType | null {
|
||||||
const isEmailAddress = emailRegex.test(inputText);
|
if (emailRegex.test(inputText)) {
|
||||||
const isUserId = mxUserIdRegex.test(inputText);
|
return AddressType.Email;
|
||||||
const isRoomId = mxRoomIdRegex.test(inputText);
|
} else if (mxUserIdRegex.test(inputText)) {
|
||||||
|
return AddressType.MatrixUserId;
|
||||||
// sanity check the input for user IDs
|
} else if (mxRoomIdRegex.test(inputText)) {
|
||||||
if (isEmailAddress) {
|
return AddressType.MatrixRoomId;
|
||||||
return 'email';
|
|
||||||
} else if (isUserId) {
|
|
||||||
return 'mx-user-id';
|
|
||||||
} else if (isRoomId) {
|
|
||||||
return 'mx-room-id';
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
|
@ -48,7 +48,7 @@ import createRoom, {IOpts} from "../../createRoom";
|
||||||
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import ThemeController from "../../settings/controllers/ThemeController";
|
import ThemeController from "../../settings/controllers/ThemeController";
|
||||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
import { startAnyRegistrationFlow } from "../../Registration";
|
||||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { _t, _td } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
import { addressTypes, getAddressType } from '../../../UserAddress';
|
||||||
import GroupStore from '../../../stores/GroupStore';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import * as Email from '../../../email';
|
import * as Email from '../../../email';
|
||||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||||
|
|
|
@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
||||||
render() {
|
render() {
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
const room = this.props.room;
|
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();
|
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||||
|
|
||||||
return (<div>
|
return (<div>
|
||||||
|
|
|
@ -17,37 +17,45 @@ limitations under the License.
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {_t, _td} from "../../../languageHandler";
|
import { _t, _td } from "../../../languageHandler";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
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 SdkConfig from "../../../SdkConfig";
|
||||||
import * as Email from "../../../email";
|
import * as Email from "../../../email";
|
||||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
|
||||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
import { abbreviateUrl } from "../../../utils/UrlUtils";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import {humanizeTime} from "../../../utils/humanize";
|
import { humanizeTime } from "../../../utils/humanize";
|
||||||
import createRoom, {
|
import createRoom, {
|
||||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
canEncryptToAllUsers,
|
||||||
|
ensureDMExists,
|
||||||
|
findDMForUser,
|
||||||
|
privateShouldBeEncrypted,
|
||||||
} from "../../../createRoom";
|
} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
import {
|
||||||
import {Key} from "../../../Keyboard";
|
IInviteResult,
|
||||||
import {Action} from "../../../dispatcher/actions";
|
inviteMultipleToRoom,
|
||||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
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 RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
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 { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import {getAddressType} from "../../../UserAddress";
|
import { getAddressType } from "../../../UserAddress";
|
||||||
import BaseAvatar from '../avatars/BaseAvatar';
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { compare } from '../../../utils/strings';
|
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 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
|
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
|
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
|
||||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||||
// for 3PIDs/email addresses.
|
// 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
|
* 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).
|
* 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 displayName: string;
|
||||||
private readonly avatarUrl: 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();
|
super();
|
||||||
this._userId = userDirResult.user_id;
|
this._userId = userDirResult.user_id;
|
||||||
this.displayName = userDirResult.display_name;
|
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}));
|
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldAbortAfterInviteError(result): boolean {
|
private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean {
|
||||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
this.setState({ busy: false });
|
||||||
if (failedUsers.length > 0) {
|
const userMap = new Map<string, Member>(this.state.targets.map(member => [member.userId, member]));
|
||||||
console.log("Failed to invite users: ", result);
|
return !showAnyInviteErrors(result.states, room, result.inviter, userMap);
|
||||||
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 convertFilter(): Member[] {
|
private convertFilter(): Member[] {
|
||||||
|
@ -731,7 +731,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
try {
|
try {
|
||||||
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
||||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
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();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,9 @@ import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { UserAddressType } from '../../../UserAddress.js';
|
import { UserAddressType } from '../../../UserAddress';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.AddressTile")
|
@replaceableComponent("views.elements.AddressTile")
|
||||||
export default class AddressTile extends React.Component {
|
export default class AddressTile extends React.Component {
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Flair from '../elements/Flair.js';
|
import Flair from '../elements/Flair';
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017, 2018 New Vector 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -20,17 +21,28 @@ import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||||
import rate_limited_func from "../../../ratelimitedfunc";
|
import rateLimitedFunction from "../../../ratelimitedfunc";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import * as sdk from "../../../index";
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
|
||||||
import BaseCard from "../right_panel/BaseCard";
|
import BaseCard from "../right_panel/BaseCard";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
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_MEMBERS = 30;
|
||||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||||
|
@ -40,41 +52,59 @@ const SHOW_MORE_INCREMENT = 100;
|
||||||
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
||||||
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
|
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")
|
@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) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli.hasLazyLoadMembersEnabled()) {
|
if (cli.hasLazyLoadMembersEnabled()) {
|
||||||
// show an empty list
|
// show an empty list
|
||||||
this.state = this._getMembersState([]);
|
this.state = this.getMembersState([]);
|
||||||
} else {
|
} else {
|
||||||
this.state = this._getMembersState(this.roomMembers());
|
this.state = this.getMembersState(this.roomMembers());
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.on("Room", this.onRoom); // invites & joining after peek
|
cli.on("Room", this.onRoom); // invites & joining after peek
|
||||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||||
this._showPresence = true;
|
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
||||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
|
||||||
this._showPresence = enablePresenceByHsUrl[hsUrl];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this._mounted = true;
|
this.mounted = true;
|
||||||
if (cli.hasLazyLoadMembersEnabled()) {
|
if (cli.hasLazyLoadMembersEnabled()) {
|
||||||
this._showMembersAccordingToMembershipWithLL();
|
this.showMembersAccordingToMembershipWithLL();
|
||||||
cli.on("Room.myMembership", this.onMyMembership);
|
cli.on("Room.myMembership", this.onMyMembership);
|
||||||
} else {
|
} else {
|
||||||
this._listenForMembersChanges();
|
this.listenForMembersChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_listenForMembersChanges() {
|
private listenForMembersChanges(): void {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.members", this.onRoomStateMember);
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
|
@ -89,7 +119,7 @@ export default class MemberList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._mounted = false;
|
this.mounted = false;
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
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
|
// 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,
|
* show a spinner and load the members if the user is joined,
|
||||||
* or show the members available so far if the user is invited
|
* or show the members available so far if the user is invited
|
||||||
*/
|
*/
|
||||||
async _showMembersAccordingToMembershipWithLL() {
|
private async showMembersAccordingToMembershipWithLL(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli.hasLazyLoadMembersEnabled()) {
|
if (cli.hasLazyLoadMembersEnabled()) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
@ -122,31 +152,31 @@ export default class MemberList extends React.Component {
|
||||||
try {
|
try {
|
||||||
await room.loadMembersIfNeeded();
|
await room.loadMembersIfNeeded();
|
||||||
} catch (ex) {/* already logged in RoomView */}
|
} catch (ex) {/* already logged in RoomView */}
|
||||||
if (this._mounted) {
|
if (this.mounted) {
|
||||||
this.setState(this._getMembersState(this.roomMembers()));
|
this.setState(this.getMembersState(this.roomMembers()));
|
||||||
this._listenForMembersChanges();
|
this.listenForMembersChanges();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// show the members we already have loaded
|
// 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 cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
return room && room.canInvite(cli.getUserId());
|
return room && room.canInvite(cli.getUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
_getMembersState(members) {
|
private getMembersState(members: Array<RoomMember>): IState {
|
||||||
// set the state after determining _showPresence to make sure it's
|
// set the state after determining showPresence to make sure it's
|
||||||
// taken into account while rerendering
|
// taken into account while rendering
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
members: members,
|
members: members,
|
||||||
filteredJoinedMembers: this._filterMembers(members, 'join'),
|
filteredJoinedMembers: this.filterMembers(members, 'join'),
|
||||||
filteredInvitedMembers: this._filterMembers(members, 'invite'),
|
filteredInvitedMembers: this.filterMembers(members, 'invite'),
|
||||||
canInvite: this.canInvite,
|
canInvite: this.canInvite,
|
||||||
|
|
||||||
// ideally we'd size this to the page height, but
|
// 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
|
// Attach a SINGLE listener for global presence changes then locate the
|
||||||
// member tile and re-render it. This is more efficient than every tile
|
// member tile and re-render it. This is more efficient than every tile
|
||||||
// ever attaching their own listener.
|
// ever attaching their own listener.
|
||||||
const tile = this.refs[user.userId];
|
const tile = this.refs[user.userId];
|
||||||
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
|
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
|
||||||
if (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) {
|
if (room.roomId !== this.props.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// We listen for room events because when we accept an invite
|
// We listen for room events because when we accept an invite
|
||||||
// we need to wait till the room is fully populated with state
|
// we need to wait till the room is fully populated with state
|
||||||
// before refreshing the member list else we get a stale list.
|
// 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") {
|
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) {
|
if (member.roomId !== this.props.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._updateList();
|
this.updateList();
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomMemberName = (ev, member) => {
|
private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => {
|
||||||
if (member.roomId !== this.props.roomId) {
|
if (member.roomId !== this.props.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._updateList();
|
this.updateList();
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomStateEvent = (event, state) => {
|
private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => {
|
||||||
if (event.getRoomId() === this.props.roomId &&
|
if (event.getRoomId() === this.props.roomId &&
|
||||||
event.getType() === "m.room.third_party_invite") {
|
event.getType() === "m.room.third_party_invite") {
|
||||||
this._updateList();
|
this.updateList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateList = rate_limited_func(() => {
|
private updateList = rateLimitedFunction(() => {
|
||||||
this._updateListNow();
|
this.updateListNow();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
_updateListNow() {
|
private updateListNow(): void {
|
||||||
// console.log("Updating memberlist");
|
const members = this.roomMembers()
|
||||||
const newState = {
|
|
||||||
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
members: this.roomMembers(),
|
members: members,
|
||||||
};
|
filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery),
|
||||||
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
|
filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery),
|
||||||
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery);
|
});
|
||||||
this.setState(newState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMembersWithUser() {
|
private getMembersWithUser(): Array<RoomMember> {
|
||||||
if (!this.props.roomId) return [];
|
if (!this.props.roomId) return [];
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(this.props.roomId);
|
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);
|
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
|
// work around a race where you might have a room member object
|
||||||
// before the user object exists. This may or may not cause
|
// before the user object exists. This may or may not cause
|
||||||
// https://github.com/vector-im/vector-web/issues/186
|
// https://github.com/vector-im/vector-web/issues/186
|
||||||
if (member.user === null) {
|
if (!member.user) {
|
||||||
member.user = cli.getUser(member.userId);
|
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!
|
// XXX: this user may have no lastPresenceTs value!
|
||||||
// the right solution here is to fix the race rather than leave it as 0
|
// 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;
|
return allMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
roomMembers() {
|
private roomMembers(): Array<RoomMember> {
|
||||||
const allMembers = this.getMembersWithUser();
|
const allMembers = this.getMembersWithUser();
|
||||||
const filteredAndSortedMembers = allMembers.filter((m) => {
|
const filteredAndSortedMembers = allMembers.filter((m) => {
|
||||||
return (
|
return (
|
||||||
|
@ -255,23 +288,21 @@ export default class MemberList extends React.Component {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const language = SettingsStore.getValue("language");
|
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);
|
filteredAndSortedMembers.sort(this.memberSort);
|
||||||
return filteredAndSortedMembers;
|
return filteredAndSortedMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createOverflowTileJoined = (overflowCount, totalCount) => {
|
private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
|
return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList);
|
||||||
};
|
};
|
||||||
|
|
||||||
_createOverflowTileInvited = (overflowCount, totalCount) => {
|
private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
|
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.
|
// 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 });
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
return (
|
return (
|
||||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
|
@ -281,31 +312,48 @@ export default class MemberList extends React.Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_showMoreJoinedMemberList = () => {
|
private showMoreJoinedMemberList = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_showMoreInvitedMemberList = () => {
|
private showMoreInvitedMemberList = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
memberString(member) {
|
/**
|
||||||
|
* SHOULD ONLY BE USED BY TESTS
|
||||||
|
*/
|
||||||
|
public memberString(member: RoomMember): string {
|
||||||
if (!member) {
|
if (!member) {
|
||||||
return "(null)";
|
return "(null)";
|
||||||
} else {
|
} else {
|
||||||
const u = member.user;
|
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 negative if a comes before b,
|
||||||
// returns 0 if a and b are equivalent in ordering
|
// returns 0 if a and b are equivalent in ordering
|
||||||
// returns positive if a comes after b.
|
// returns positive if a comes after b.
|
||||||
memberSort = (memberA, memberB) => {
|
private memberSort = (memberA: RoomMember, memberB: RoomMember): number => {
|
||||||
// order by presence, with "active now" first.
|
// order by presence, with "active now" first.
|
||||||
// ...and then by power level
|
// ...and then by power level
|
||||||
// ...and then by last active
|
// ...and then by last active
|
||||||
|
@ -325,7 +373,7 @@ export default class MemberList extends React.Component {
|
||||||
if (!userA && userB) return 1;
|
if (!userA && userB) return 1;
|
||||||
|
|
||||||
// First by presence
|
// First by presence
|
||||||
if (this._showPresence) {
|
if (this.showPresence) {
|
||||||
const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
|
const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
|
||||||
const presenceIndex = p => {
|
const presenceIndex = p => {
|
||||||
const order = ['active', 'online', 'offline'];
|
const order = ['active', 'online', 'offline'];
|
||||||
|
@ -349,31 +397,31 @@ export default class MemberList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Third by last active
|
// 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");
|
// console.log("Comparing on last active timestamp - returning");
|
||||||
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fourth by name (alphabetical)
|
// 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({
|
this.setState({
|
||||||
searchQuery,
|
searchQuery,
|
||||||
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery),
|
filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery),
|
||||||
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery),
|
filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPending3pidInviteClick = inviteEvent => {
|
private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_3pid_invite',
|
action: 'view_3pid_invite',
|
||||||
event: inviteEvent,
|
event: inviteEvent,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_filterMembers(members, membership, query) {
|
private filterMembers(members: Array<RoomMember>, membership: string, query?: string): Array<RoomMember> {
|
||||||
return members.filter((m) => {
|
return members.filter((m) => {
|
||||||
if (query) {
|
if (query) {
|
||||||
query = query.toLowerCase();
|
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.
|
// include 3pid invites (m.room.third_party_invite) state events.
|
||||||
// The HS may have already converted these into m.room.member invites so
|
// 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
|
// 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) {
|
private makeMemberTiles(members: Array<RoomMember | MatrixEvent>) {
|
||||||
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
|
||||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
|
||||||
|
|
||||||
return members.map((m) => {
|
return members.map((m) => {
|
||||||
if (m.userId) {
|
if (m instanceof RoomMember) {
|
||||||
// Is a Matrix invite
|
// 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 {
|
} else {
|
||||||
// Is a 3pid invite
|
// Is a 3pid invite
|
||||||
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
|
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));
|
private getChildrenJoined = (start: number, end: number): Array<JSX.Element> => {
|
||||||
|
return 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));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_getChildCountInvited = () => {
|
private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length;
|
||||||
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).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() {
|
render() {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
return <BaseCard
|
return <BaseCard
|
||||||
className="mx_MemberList"
|
className="mx_MemberList"
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
|
@ -454,9 +500,6 @@ export default class MemberList extends React.Component {
|
||||||
</BaseCard>;
|
</BaseCard>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
|
||||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
let inviteButton;
|
let inviteButton;
|
||||||
|
@ -470,22 +513,30 @@ export default class MemberList extends React.Component {
|
||||||
inviteButtonText = _t("Invite to this space");
|
inviteButtonText = _t("Invite to this space");
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
inviteButton = (
|
||||||
inviteButton =
|
<AccessibleButton
|
||||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!this.state.canInvite}>
|
className="mx_MemberList_invite"
|
||||||
|
onClick={this.onInviteButtonClick}
|
||||||
|
disabled={!this.state.canInvite}
|
||||||
|
>
|
||||||
<span>{ inviteButtonText }</span>
|
<span>{ inviteButtonText }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let invitedHeader;
|
let invitedHeader;
|
||||||
let invitedSection;
|
let invitedSection;
|
||||||
if (this._getChildCountInvited() > 0) {
|
if (this.getChildCountInvited() > 0) {
|
||||||
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
||||||
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited}
|
invitedSection = (
|
||||||
createOverflowElement={this._createOverflowTileInvited}
|
<TruncatedList
|
||||||
getChildren={this._getChildrenInvited}
|
className="mx_MemberList_section mx_MemberList_invited"
|
||||||
getChildCount={this._getChildCountInvited}
|
truncateAt={this.state.truncateAtInvited}
|
||||||
/>;
|
createOverflowElement={this.createOverflowTileInvited}
|
||||||
|
getChildren={this.getChildrenInvited}
|
||||||
|
getChildCount={this.getChildCountInvited}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const footer = (
|
const footer = (
|
||||||
|
@ -517,17 +568,19 @@ export default class MemberList extends React.Component {
|
||||||
previousPhase={previousPhase}
|
previousPhase={previousPhase}
|
||||||
>
|
>
|
||||||
<div className="mx_MemberList_wrapper">
|
<div className="mx_MemberList_wrapper">
|
||||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
<TruncatedList
|
||||||
createOverflowElement={this._createOverflowTileJoined}
|
className="mx_MemberList_section mx_MemberList_joined"
|
||||||
getChildren={this._getChildrenJoined}
|
truncateAt={this.state.truncateAtJoined}
|
||||||
getChildCount={this._getChildCountJoined} />
|
createOverflowElement={this.createOverflowTileJoined}
|
||||||
|
getChildren={this.getChildrenJoined}
|
||||||
|
getChildCount={this.getChildCountJoined} />
|
||||||
{ invitedHeader }
|
{ invitedHeader }
|
||||||
{ invitedSection }
|
{ invitedSection }
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>;
|
</BaseCard>;
|
||||||
}
|
}
|
||||||
|
|
||||||
onInviteButtonClick = () => {
|
onInviteButtonClick = (): void => {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
dis.dispatch({action: 'require_registration'});
|
dis.dispatch({action: 'require_registration'});
|
||||||
return;
|
return;
|
|
@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
async _getUpdatedStatus() {
|
async _getUpdatedStatus() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const pkCache = cli.getCrossSigningCacheCallbacks();
|
const pkCache = cli.getCrossSigningCacheCallbacks();
|
||||||
const crossSigning = cli.crypto._crossSigningInfo;
|
const crossSigning = cli.crypto.crossSigningInfo;
|
||||||
const secretStorage = cli.crypto._secretStorage;
|
const secretStorage = cli.crypto.secretStorage;
|
||||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||||
|
|
|
@ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
|
|
||||||
async _getUpdatedDiagnostics() {
|
async _getUpdatedDiagnostics() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const secretStorage = cli.crypto._secretStorage;
|
const secretStorage = cli.crypto.secretStorage;
|
||||||
|
|
||||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
||||||
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
|
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
|
||||||
|
|
|
@ -396,7 +396,8 @@
|
||||||
"Failed to invite": "Failed to invite",
|
"Failed to invite": "Failed to invite",
|
||||||
"Operation failed": "Operation failed",
|
"Operation failed": "Operation failed",
|
||||||
"Failed to invite users to the room:": "Failed to invite users to the room:",
|
"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 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.",
|
"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.",
|
"Unable to create widget.": "Unable to create widget.",
|
||||||
|
@ -489,24 +490,27 @@
|
||||||
"Converts the room to a DM": "Converts the room to a DM",
|
"Converts the room to a DM": "Converts the room to a DM",
|
||||||
"Converts the DM to a room": "Converts the DM to a room",
|
"Converts the DM to a room": "Converts the DM to a room",
|
||||||
"Displays action": "Displays action",
|
"Displays action": "Displays action",
|
||||||
"Reason": "Reason",
|
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepted the invitation for %(displayName)s",
|
||||||
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
|
"%(targetName)s accepted an invitation": "%(targetName)s accepted an invitation",
|
||||||
"%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
|
"%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
|
||||||
"%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.",
|
"%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s banned %(targetName)s: %(reason)s",
|
||||||
"%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.",
|
"%(senderName)s banned %(targetName)s": "%(senderName)s banned %(targetName)s",
|
||||||
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.",
|
"%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s changed their display name to %(displayName)s",
|
||||||
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
|
"%(senderName)s set their display name to %(displayName)s": "%(senderName)s set their display name to %(displayName)s",
|
||||||
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s removed their display name (%(oldDisplayName)s).",
|
"%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s removed their display name (%(oldDisplayName)s)",
|
||||||
"%(senderName)s removed their profile picture.": "%(senderName)s removed their profile picture.",
|
"%(senderName)s removed their profile picture": "%(senderName)s removed their profile picture",
|
||||||
"%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
|
"%(senderName)s changed their profile picture": "%(senderName)s changed their profile picture",
|
||||||
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
|
"%(senderName)s set a profile picture": "%(senderName)s set a profile picture",
|
||||||
"%(senderName)s made no change.": "%(senderName)s made no change.",
|
"%(senderName)s made no change": "%(senderName)s made no change",
|
||||||
"%(targetName)s joined the room.": "%(targetName)s joined the room.",
|
"%(targetName)s joined the room": "%(targetName)s joined the room",
|
||||||
"%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.",
|
"%(targetName)s rejected the invitation": "%(targetName)s rejected the invitation",
|
||||||
"%(targetName)s left the room.": "%(targetName)s left the room.",
|
"%(targetName)s left the room: %(reason)s": "%(targetName)s left the room: %(reason)s",
|
||||||
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.",
|
"%(targetName)s left the room": "%(targetName)s left the room",
|
||||||
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.",
|
"%(senderName)s unbanned %(targetName)s": "%(senderName)s unbanned %(targetName)s",
|
||||||
"%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.",
|
"%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s",
|
||||||
|
"%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s withdrew %(targetName)s's invitation",
|
||||||
|
"%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s kicked %(targetName)s: %(reason)s",
|
||||||
|
"%(senderName)s kicked %(targetName)s": "%(senderName)s kicked %(targetName)s",
|
||||||
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
|
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
|
||||||
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
|
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
|
||||||
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.",
|
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.",
|
||||||
|
@ -1411,6 +1415,7 @@
|
||||||
"Failed to unban": "Failed to unban",
|
"Failed to unban": "Failed to unban",
|
||||||
"Unban": "Unban",
|
"Unban": "Unban",
|
||||||
"Banned by %(displayName)s": "Banned by %(displayName)s",
|
"Banned by %(displayName)s": "Banned by %(displayName)s",
|
||||||
|
"Reason": "Reason",
|
||||||
"Error changing power level requirement": "Error changing power level requirement",
|
"Error changing power level requirement": "Error changing power level requirement",
|
||||||
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.",
|
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.",
|
||||||
"Error changing power level": "Error changing power level",
|
"Error changing power level": "Error changing power level",
|
||||||
|
@ -2278,7 +2283,6 @@
|
||||||
"Confirm to continue": "Confirm to continue",
|
"Confirm to continue": "Confirm to continue",
|
||||||
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
|
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
|
||||||
"Invite by email": "Invite by email",
|
"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.",
|
"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.",
|
"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.",
|
"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
|
// The following interfaces take their names and member names from seshat and the spec
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
export interface MatrixEvent {
|
export interface IMatrixEvent {
|
||||||
type: string;
|
type: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
content: {};
|
content: {};
|
||||||
|
@ -27,37 +27,37 @@ export interface MatrixEvent {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatrixProfile {
|
export interface IMatrixProfile {
|
||||||
avatar_url: string;
|
avatar_url: string;
|
||||||
displayname: string;
|
displayname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CrawlerCheckpoint {
|
export interface ICrawlerCheckpoint {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
token: string;
|
token: string;
|
||||||
fullCrawl?: boolean;
|
fullCrawl?: boolean;
|
||||||
direction: string;
|
direction: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultContext {
|
export interface IResultContext {
|
||||||
events_before: [MatrixEvent];
|
events_before: [IMatrixEvent];
|
||||||
events_after: [MatrixEvent];
|
events_after: [IMatrixEvent];
|
||||||
profile_info: Map<string, MatrixProfile>;
|
profile_info: Map<string, IMatrixProfile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultsElement {
|
export interface IResultsElement {
|
||||||
rank: number;
|
rank: number;
|
||||||
result: MatrixEvent;
|
result: IMatrixEvent;
|
||||||
context: ResultContext;
|
context: IResultContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchResult {
|
export interface ISearchResult {
|
||||||
count: number;
|
count: number;
|
||||||
results: [ResultsElement];
|
results: [IResultsElement];
|
||||||
highlights: [string];
|
highlights: [string];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchArgs {
|
export interface ISearchArgs {
|
||||||
search_term: string;
|
search_term: string;
|
||||||
before_limit: number;
|
before_limit: number;
|
||||||
after_limit: number;
|
after_limit: number;
|
||||||
|
@ -65,19 +65,19 @@ export interface SearchArgs {
|
||||||
room_id?: string;
|
room_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventAndProfile {
|
export interface IEventAndProfile {
|
||||||
event: MatrixEvent;
|
event: IMatrixEvent;
|
||||||
profile: MatrixProfile;
|
profile: IMatrixProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadArgs {
|
export interface ILoadArgs {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
fromEvent?: string;
|
fromEvent?: string;
|
||||||
direction?: string;
|
direction?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IndexStats {
|
export interface IIndexStats {
|
||||||
size: number;
|
size: number;
|
||||||
eventCount: number;
|
eventCount: number;
|
||||||
roomCount: number;
|
roomCount: number;
|
||||||
|
@ -119,13 +119,13 @@ export default abstract class BaseEventIndexManager {
|
||||||
* Queue up an event to be added to the index.
|
* Queue up an event to be added to the index.
|
||||||
*
|
*
|
||||||
* @param {MatrixEvent} ev The event that should 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.
|
* time of the event receival.
|
||||||
*
|
*
|
||||||
* @return {Promise} A promise that will resolve when the was queued up for
|
* @return {Promise} A promise that will resolve when the was queued up for
|
||||||
* addition.
|
* addition.
|
||||||
*/
|
*/
|
||||||
async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<void> {
|
async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,10 +160,10 @@ export default abstract class BaseEventIndexManager {
|
||||||
/**
|
/**
|
||||||
* Get statistical information of the index.
|
* 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.
|
* statistics.
|
||||||
*/
|
*/
|
||||||
async getStats(): Promise<IndexStats> {
|
async getStats(): Promise<IIndexStats> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,13 +203,13 @@ export default abstract class BaseEventIndexManager {
|
||||||
/**
|
/**
|
||||||
* Search the event index using the given term for matching events.
|
* 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.
|
* 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.
|
* of search results once the search is done.
|
||||||
*/
|
*/
|
||||||
async searchEventIndex(searchArgs: SearchArgs): Promise<SearchResult> {
|
async searchEventIndex(searchArgs: ISearchArgs): Promise<ISearchResult> {
|
||||||
throw new Error("Unimplemented");
|
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.
|
* 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.
|
* 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
|
* should be stored in the index which should be used to continue crawling
|
||||||
* the room.
|
* 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
|
* to fetch the current batch of events. This checkpoint will be removed
|
||||||
* from the index.
|
* from the index.
|
||||||
*
|
*
|
||||||
|
@ -231,9 +231,9 @@ export default abstract class BaseEventIndexManager {
|
||||||
* were already added to the index, false otherwise.
|
* were already added to the index, false otherwise.
|
||||||
*/
|
*/
|
||||||
async addHistoricEvents(
|
async addHistoricEvents(
|
||||||
events: [EventAndProfile],
|
events: IEventAndProfile[],
|
||||||
checkpoint: CrawlerCheckpoint | null,
|
checkpoint: ICrawlerCheckpoint | null,
|
||||||
oldCheckpoint: CrawlerCheckpoint | null,
|
oldCheckpoint: ICrawlerCheckpoint | null,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
@ -241,36 +241,36 @@ export default abstract class BaseEventIndexManager {
|
||||||
/**
|
/**
|
||||||
* Add a new crawler checkpoint to the index.
|
* 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.
|
* to the index.
|
||||||
*
|
*
|
||||||
* @return {Promise} A promise that will resolve once the checkpoint has
|
* @return {Promise} A promise that will resolve once the checkpoint has
|
||||||
* been stored.
|
* been stored.
|
||||||
*/
|
*/
|
||||||
async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> {
|
async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new crawler checkpoint to the index.
|
* 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.
|
* removed from the index.
|
||||||
*
|
*
|
||||||
* @return {Promise} A promise that will resolve once the checkpoint has
|
* @return {Promise} A promise that will resolve once the checkpoint has
|
||||||
* been removed.
|
* been removed.
|
||||||
*/
|
*/
|
||||||
async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> {
|
async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the stored checkpoints from the index.
|
* 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.
|
* array of crawler checkpoints once they have been loaded from the index.
|
||||||
*/
|
*/
|
||||||
async loadCheckpoints(): Promise<[CrawlerCheckpoint]> {
|
async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,11 +286,11 @@ export default abstract class BaseEventIndexManager {
|
||||||
* @param {string} args.direction The direction to which we should continue
|
* @param {string} args.direction The direction to which we should continue
|
||||||
* loading events from. This is used only if fromEvent is used as well.
|
* 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
|
* array of Matrix events that contain mxc URLs accompanied with the
|
||||||
* historic profile of the sender.
|
* historic profile of the sender.
|
||||||
*/
|
*/
|
||||||
async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> {
|
async loadFileEvents(args: ILoadArgs): Promise<IEventAndProfile[]> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import { sleep } from "../utils/promise";
|
import { sleep } from "../utils/promise";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import { SettingLevel } from "../settings/SettingLevel";
|
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
|
// The time in ms that the crawler will wait loop iterations if there
|
||||||
// have not been any checkpoints to consume in the last iteration.
|
// 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.
|
* Event indexing class that wraps the platform specific event indexing.
|
||||||
*/
|
*/
|
||||||
export default class EventIndex extends EventEmitter {
|
export default class EventIndex extends EventEmitter {
|
||||||
private crawlerCheckpoints: CrawlerCheckpoint[] = [];
|
private crawlerCheckpoints: ICrawlerCheckpoint[] = [];
|
||||||
private crawler: ICrawler = null;
|
private crawler: ICrawler = null;
|
||||||
private currentCheckpoint: CrawlerCheckpoint = null;
|
private currentCheckpoint: ICrawlerCheckpoint = null;
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
@ -111,14 +111,14 @@ export default class EventIndex extends EventEmitter {
|
||||||
const timeline = room.getLiveTimeline();
|
const timeline = room.getLiveTimeline();
|
||||||
const token = timeline.getPaginationToken("b");
|
const token = timeline.getPaginationToken("b");
|
||||||
|
|
||||||
const backCheckpoint: CrawlerCheckpoint = {
|
const backCheckpoint: ICrawlerCheckpoint = {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
token: token,
|
token: token,
|
||||||
direction: "b",
|
direction: "b",
|
||||||
fullCrawl: true,
|
fullCrawl: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const forwardCheckpoint: CrawlerCheckpoint = {
|
const forwardCheckpoint: ICrawlerCheckpoint = {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
token: token,
|
token: token,
|
||||||
direction: "f",
|
direction: "f",
|
||||||
|
@ -668,13 +668,13 @@ export default class EventIndex extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Search the event index using the given term for matching events.
|
* 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.
|
* sets the search term and determines the search result contents.
|
||||||
*
|
*
|
||||||
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
||||||
* of search results once the search is done.
|
* of search results once the search is done.
|
||||||
*/
|
*/
|
||||||
public async search(searchArgs: SearchArgs) {
|
public async search(searchArgs: ISearchArgs) {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
return indexManager.searchEventIndex(searchArgs);
|
return indexManager.searchEventIndex(searchArgs);
|
||||||
}
|
}
|
||||||
|
@ -709,7 +709,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
|
||||||
const loadArgs: LoadArgs = {
|
const loadArgs: ILoadArgs = {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
};
|
};
|
||||||
|
|
|
@ -86,8 +86,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
|
||||||
body.append('cross_signing_key', client.getCrossSigningId());
|
body.append('cross_signing_key', client.getCrossSigningId());
|
||||||
|
|
||||||
// add cross-signing status information
|
// add cross-signing status information
|
||||||
const crossSigning = client.crypto._crossSigningInfo;
|
const crossSigning = client.crypto.crossSigningInfo;
|
||||||
const secretStorage = client.crypto._secretStorage;
|
const secretStorage = client.crypto.secretStorage;
|
||||||
|
|
||||||
body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
|
body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
|
||||||
body.append("cross_signing_supported_by_hs",
|
body.append("cross_signing_supported_by_hs",
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
|
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 { IContent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
|
import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
|
||||||
|
@ -149,7 +149,7 @@ function stringAsTextNode(string: string): Text {
|
||||||
function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void {
|
function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void {
|
||||||
const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route);
|
const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route);
|
||||||
switch (diff.action) {
|
switch (diff.action) {
|
||||||
case Action.ReplaceElement: {
|
case "replaceElement": {
|
||||||
const container = document.createElement("span");
|
const container = document.createElement("span");
|
||||||
const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue));
|
const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue));
|
||||||
const insNode = wrapInsertion(diffTreeToDOM(diff.newValue));
|
const insNode = wrapInsertion(diffTreeToDOM(diff.newValue));
|
||||||
|
@ -158,17 +158,17 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
||||||
refNode.parentNode.replaceChild(container, refNode);
|
refNode.parentNode.replaceChild(container, refNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.RemoveTextElement: {
|
case "removeTextElement": {
|
||||||
const delNode = wrapDeletion(stringAsTextNode(diff.value));
|
const delNode = wrapDeletion(stringAsTextNode(diff.value));
|
||||||
refNode.parentNode.replaceChild(delNode, refNode);
|
refNode.parentNode.replaceChild(delNode, refNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.RemoveElement: {
|
case "removeElement": {
|
||||||
const delNode = wrapDeletion(diffTreeToDOM(diff.element));
|
const delNode = wrapDeletion(diffTreeToDOM(diff.element));
|
||||||
refNode.parentNode.replaceChild(delNode, refNode);
|
refNode.parentNode.replaceChild(delNode, refNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.ModifyTextElement: {
|
case "modifyTextElement": {
|
||||||
const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue);
|
const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue);
|
||||||
diffMathPatch.diff_cleanupSemantic(textDiffs);
|
diffMathPatch.diff_cleanupSemantic(textDiffs);
|
||||||
const container = document.createElement("span");
|
const container = document.createElement("span");
|
||||||
|
@ -184,12 +184,12 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
||||||
refNode.parentNode.replaceChild(container, refNode);
|
refNode.parentNode.replaceChild(container, refNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.AddElement: {
|
case "addElement": {
|
||||||
const insNode = wrapInsertion(diffTreeToDOM(diff.element));
|
const insNode = wrapInsertion(diffTreeToDOM(diff.element));
|
||||||
insertBefore(refParentNode, refNode, insNode);
|
insertBefore(refParentNode, refNode, insNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.AddTextElement: {
|
case "addTextElement": {
|
||||||
// XXX: sometimes diffDOM says insert a newline when there shouldn't be one
|
// 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.
|
// 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
|
// 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,
|
// 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
|
// show the link with old href as removed and with the new href as added
|
||||||
case Action.RemoveAttribute:
|
case "removeAttribute":
|
||||||
case Action.AddAttribute:
|
case "addAttribute":
|
||||||
case Action.ModifyAttribute: {
|
case "modifyAttribute": {
|
||||||
const delNode = wrapDeletion(refNode.cloneNode(true));
|
const delNode = wrapDeletion(refNode.cloneNode(true));
|
||||||
const updatedNode = refNode.cloneNode(true) as HTMLElement;
|
const updatedNode = refNode.cloneNode(true) as HTMLElement;
|
||||||
if (diff.action === "addAttribute" || diff.action === "modifyAttribute") {
|
if (diff.action === "addAttribute" || diff.action === "modifyAttribute") {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
import {getAddressType} from '../UserAddress';
|
|
||||||
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
|
import { AddressType, getAddressType } from '../UserAddress';
|
||||||
import GroupStore from '../stores/GroupStore';
|
import GroupStore from '../stores/GroupStore';
|
||||||
import {_t} from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
import * as sdk from "../index";
|
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
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
|
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
||||||
*/
|
*/
|
||||||
export default class MultiInviter {
|
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
|
* @param {string} targetId The ID of the room or group to invite to
|
||||||
*/
|
*/
|
||||||
constructor(targetId) {
|
constructor(targetId: string) {
|
||||||
if (targetId[0] === '+') {
|
if (targetId[0] === '+') {
|
||||||
this.roomId = null;
|
this.roomId = null;
|
||||||
this.groupId = targetId;
|
this.groupId = targetId;
|
||||||
|
@ -39,41 +66,38 @@ export default class MultiInviter {
|
||||||
this.roomId = targetId;
|
this.roomId = targetId;
|
||||||
this.groupId = null;
|
this.groupId = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.canceled = false;
|
public get fatal() {
|
||||||
this.addrs = [];
|
return this._fatal;
|
||||||
this.busy = false;
|
|
||||||
this.completionStates = {}; // State of each address (invited or error)
|
|
||||||
this.errors = {}; // { address: {errorText, errcode} }
|
|
||||||
this.deferred = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invite users to this room. This may only be called once per
|
* Invite users to this room. This may only be called once per
|
||||||
* instance of the class.
|
* 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)
|
* @param {string} reason Reason for inviting (optional)
|
||||||
* @returns {Promise} Resolved when all invitations in the queue are complete
|
* @returns {Promise} Resolved when all invitations in the queue are complete
|
||||||
*/
|
*/
|
||||||
invite(addrs, reason) {
|
public invite(addresses, reason?: string): Promise<CompletionStates> {
|
||||||
if (this.addrs.length > 0) {
|
if (this.addresses.length > 0) {
|
||||||
throw new Error("Already inviting/invited");
|
throw new Error("Already inviting/invited");
|
||||||
}
|
}
|
||||||
this.addrs.push(...addrs);
|
this.addresses.push(...addresses);
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
|
|
||||||
for (const addr of this.addrs) {
|
for (const addr of this.addresses) {
|
||||||
if (getAddressType(addr) === null) {
|
if (getAddressType(addr) === null) {
|
||||||
this.completionStates[addr] = 'error';
|
this.completionStates[addr] = InviteState.Error;
|
||||||
this.errors[addr] = {
|
this.errors[addr] = {
|
||||||
errcode: 'M_INVALID',
|
errcode: 'M_INVALID',
|
||||||
errorText: _t('Unrecognised address'),
|
errorText: _t('Unrecognised address'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.deferred = defer();
|
this.deferred = defer<CompletionStates>();
|
||||||
this._inviteMore(0);
|
this.inviteMore(0);
|
||||||
|
|
||||||
return this.deferred.promise;
|
return this.deferred.promise;
|
||||||
}
|
}
|
||||||
|
@ -81,33 +105,36 @@ export default class MultiInviter {
|
||||||
/**
|
/**
|
||||||
* Stops inviting. Causes promises returned by invite() to be rejected.
|
* Stops inviting. Causes promises returned by invite() to be rejected.
|
||||||
*/
|
*/
|
||||||
cancel() {
|
public cancel(): void {
|
||||||
if (!this.busy) return;
|
if (!this.busy) return;
|
||||||
|
|
||||||
this._canceled = true;
|
this.canceled = true;
|
||||||
this.deferred.reject(new Error('canceled'));
|
this.deferred.reject(new Error('canceled'));
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletionState(addr) {
|
public getCompletionState(addr: string): InviteState {
|
||||||
return this.completionStates[addr];
|
return this.completionStates[addr];
|
||||||
}
|
}
|
||||||
|
|
||||||
getErrorText(addr) {
|
public getErrorText(addr: string): string {
|
||||||
return this.errors[addr] ? this.errors[addr].errorText : null;
|
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);
|
const addrType = getAddressType(addr);
|
||||||
|
|
||||||
if (addrType === 'email') {
|
if (addrType === AddressType.Email) {
|
||||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||||
} else if (addrType === 'mx-user-id') {
|
} else if (addrType === AddressType.MatrixUserId) {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
if (!room) throw new Error("Room not found");
|
if (!room) throw new Error("Room not found");
|
||||||
|
|
||||||
const member = room.getMember(addr);
|
const member = room.getMember(addr);
|
||||||
if (member && ['join', 'invite'].includes(member.membership)) {
|
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)) {
|
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
||||||
|
@ -124,28 +151,28 @@ export default class MultiInviter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_doInvite(address, ignoreProfile) {
|
private doInvite(address: string, ignoreProfile = false): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
console.log(`Inviting ${address}`);
|
console.log(`Inviting ${address}`);
|
||||||
|
|
||||||
let doInvite;
|
let doInvite;
|
||||||
if (this.groupId !== null) {
|
if (this.groupId !== null) {
|
||||||
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
|
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
|
||||||
} else {
|
} else {
|
||||||
doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile);
|
doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
doInvite.then(() => {
|
doInvite.then(() => {
|
||||||
if (this._canceled) {
|
if (this.canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.completionStates[address] = 'invited';
|
this.completionStates[address] = InviteState.Invited;
|
||||||
delete this.errors[address];
|
delete this.errors[address];
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (this._canceled) {
|
if (this.canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +188,7 @@ export default class MultiInviter {
|
||||||
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
||||||
// we're being throttled so wait a bit & try again
|
// we're being throttled so wait a bit & try again
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._doInvite(address, ignoreProfile).then(resolve, reject);
|
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
return;
|
return;
|
||||||
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) {
|
} 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) {
|
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) {
|
||||||
// Invite without the profile check
|
// Invite without the profile check
|
||||||
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
|
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") {
|
} else if (err.errcode === "M_BAD_STATE") {
|
||||||
errorText = _t("The user must be unbanned before they can be invited.");
|
errorText = _t("The user must be unbanned before they can be invited.");
|
||||||
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
|
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
|
||||||
|
@ -180,14 +207,14 @@ export default class MultiInviter {
|
||||||
errorText = _t('Unknown server error');
|
errorText = _t('Unknown server error');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.completionStates[address] = 'error';
|
this.completionStates[address] = InviteState.Error;
|
||||||
this.errors[address] = {errorText, errcode: err.errcode};
|
this.errors[address] = { errorText, errcode: err.errcode };
|
||||||
|
|
||||||
this.busy = !fatal;
|
this.busy = !fatal;
|
||||||
this.fatal = fatal;
|
this._fatal = fatal;
|
||||||
|
|
||||||
if (fatal) {
|
if (fatal) {
|
||||||
reject();
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
@ -195,22 +222,22 @@ export default class MultiInviter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_inviteMore(nextIndex, ignoreProfile) {
|
private inviteMore(nextIndex: number, ignoreProfile = false): void {
|
||||||
if (this._canceled) {
|
if (this.canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextIndex === this.addrs.length) {
|
if (nextIndex === this.addresses.length) {
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
if (Object.keys(this.errors).length > 0 && !this.groupId) {
|
if (Object.keys(this.errors).length > 0 && !this.groupId) {
|
||||||
// There were problems inviting some people - see if we can invite them
|
// There were problems inviting some people - see if we can invite them
|
||||||
// without caring if they exist or not.
|
// 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)
|
||||||
const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode));
|
.filter(a => UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode));
|
||||||
|
|
||||||
if (unknownProfileUsers.length > 0) {
|
if (unknownProfileUsers.length > 0) {
|
||||||
const inviteUnknowns = () => {
|
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));
|
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -219,15 +246,17 @@ export default class MultiInviter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
|
|
||||||
console.log("Showing failed to invite dialog...");
|
console.log("Showing failed to invite dialog...");
|
||||||
Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, {
|
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(),
|
onInviteAnyways: () => inviteUnknowns(),
|
||||||
onGiveUp: () => {
|
onGiveUp: () => {
|
||||||
// Fake all the completion states because we already warned the user
|
// Fake all the completion states because we already warned the user
|
||||||
for (const addr of unknownProfileUsers) {
|
for (const addr of unknownProfileUsers) {
|
||||||
this.completionStates[addr] = 'invited';
|
this.completionStates[addr] = InviteState.Invited;
|
||||||
}
|
}
|
||||||
this.deferred.resolve(this.completionStates);
|
this.deferred.resolve(this.completionStates);
|
||||||
},
|
},
|
||||||
|
@ -239,25 +268,25 @@ export default class MultiInviter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addr = this.addrs[nextIndex];
|
const addr = this.addresses[nextIndex];
|
||||||
|
|
||||||
// don't try to invite it if it's an invalid address
|
// don't try to invite it if it's an invalid address
|
||||||
// (it will already be marked as an error though,
|
// (it will already be marked as an error though,
|
||||||
// so no need to do so again)
|
// so no need to do so again)
|
||||||
if (getAddressType(addr) === null) {
|
if (getAddressType(addr) === null) {
|
||||||
this._inviteMore(nextIndex + 1);
|
this.inviteMore(nextIndex + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't re-invite (there's no way in the UI to do this, but
|
// don't re-invite (there's no way in the UI to do this, but
|
||||||
// for sanity's sake)
|
// for sanity's sake)
|
||||||
if (this.completionStates[addr] === 'invited') {
|
if (this.completionStates[addr] === InviteState.Invited) {
|
||||||
this._inviteMore(nextIndex + 1);
|
this.inviteMore(nextIndex + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._doInvite(addr, ignoreProfile).then(() => {
|
this.doInvite(addr, ignoreProfile).then(() => {
|
||||||
this._inviteMore(nextIndex + 1, ignoreProfile);
|
this.inviteMore(nextIndex + 1, ignoreProfile);
|
||||||
}).catch(() => this.deferred.resolve(this.completionStates));
|
}).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 React from 'react';
|
||||||
import ReactTestUtils from 'react-dom/test-utils';
|
import ReactTestUtils from 'react-dom/test-utils';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import * as TestUtils from '../../../test-utils';
|
import * as TestUtils from '../../../test-utils';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
|
|
||||||
import sdk from '../../../skinned-sdk';
|
import sdk from '../../../skinned-sdk';
|
||||||
|
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||||
import {Room, RoomMember, User} from 'matrix-js-sdk';
|
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 { compare } from "../../../../src/utils/strings";
|
||||||
|
import MemberList from "../../../../src/components/views/rooms/MemberList";
|
||||||
|
|
||||||
function generateRoomId() {
|
function generateRoomId() {
|
||||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
describe('MemberList', () => {
|
describe('MemberList', () => {
|
||||||
function createRoom(opts) {
|
function createRoom(opts) {
|
||||||
const room = new Room(generateRoomId(), null, client.getUserId());
|
const room = new Room(generateRoomId(), null, client.getUserId());
|
||||||
|
@ -97,13 +112,19 @@ describe('MemberList', () => {
|
||||||
memberListRoom.currentState.members[member.userId] = member;
|
memberListRoom.currentState.members[member.userId] = member;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemberList = sdk.getComponent('views.rooms.MemberList');
|
|
||||||
const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList);
|
const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList);
|
||||||
const gatherWrappedRef = (r) => {
|
const gatherWrappedRef = (r) => {
|
||||||
memberList = r;
|
memberList = r;
|
||||||
};
|
};
|
||||||
root = ReactDOM.render(<WrappedMemberList roomId={memberListRoom.roomId}
|
root = ReactDOM.render(
|
||||||
wrappedRef={gatherWrappedRef} />, parentDiv);
|
(
|
||||||
|
<WrappedMemberList
|
||||||
|
roomId={memberListRoom.roomId}
|
||||||
|
wrappedRef={gatherWrappedRef}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
parentDiv,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach((done) => {
|
afterEach((done) => {
|
||||||
|
@ -213,8 +234,8 @@ describe('MemberList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
// Bypass all the event listeners and skip to the good part
|
||||||
memberList._showPresence = enablePresence;
|
memberList.showPresence = enablePresence;
|
||||||
memberList._updateListNow();
|
memberList.updateListNow();
|
||||||
|
|
||||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||||
|
@ -225,7 +246,7 @@ describe('MemberList', () => {
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
// Bypass all the event listeners and skip to the good part
|
||||||
memberList._showPresence = enablePresence;
|
memberList._showPresence = enablePresence;
|
||||||
memberList._updateListNow();
|
memberList.updateListNow();
|
||||||
|
|
||||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||||
|
@ -254,8 +275,8 @@ describe('MemberList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
// Bypass all the event listeners and skip to the good part
|
||||||
memberList._showPresence = enablePresence;
|
memberList.showPresence = enablePresence;
|
||||||
memberList._updateListNow();
|
memberList.updateListNow();
|
||||||
|
|
||||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||||
|
@ -273,8 +294,8 @@ describe('MemberList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
// Bypass all the event listeners and skip to the good part
|
||||||
memberList._showPresence = enablePresence;
|
memberList.showPresence = enablePresence;
|
||||||
memberList._updateListNow();
|
memberList.updateListNow();
|
||||||
|
|
||||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
Loading…
Reference in a new issue