Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17731

This commit is contained in:
Michael Telatynski 2021-06-25 14:50:24 +01:00
commit 1d374f6cbb
51 changed files with 865 additions and 586 deletions

View file

@ -79,7 +79,7 @@
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"matrix-js-sdk": "12.0.0", "matrix-js-sdk": "12.0.0",
"matrix-widget-api": "^0.1.0-beta.14", "matrix-widget-api": "^0.1.0-beta.15",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",
"pako": "^2.0.3", "pako": "^2.0.3",
@ -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"

View file

@ -111,6 +111,29 @@ $roomListCollapsedWidth: 68px;
} }
} }
.mx_LeftPanel_dialPadButton {
width: 32px;
height: 32px;
border-radius: 8px;
background-color: $roomlist-button-bg-color;
position: relative;
margin-left: 8px;
&::before {
content: '';
position: absolute;
top: 8px;
left: 8px;
width: 16px;
height: 16px;
mask-image: url('$(res)/img/element-icons/call/dialpad.svg');
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $secondary-fg-color;
}
}
.mx_LeftPanel_exploreButton { .mx_LeftPanel_exploreButton {
width: 32px; width: 32px;
height: 32px; height: 32px;
@ -185,6 +208,12 @@ $roomListCollapsedWidth: 68px;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
.mx_LeftPanel_dialPadButton {
margin-left: 0;
margin-top: 8px;
background-color: transparent;
}
.mx_LeftPanel_exploreButton { .mx_LeftPanel_exploreButton {
margin-left: 0; margin-left: 0;
margin-top: 8px; margin-top: 8px;

View file

@ -71,7 +71,7 @@ limitations under the License.
&::before { &::before {
background-color: #ffffff; background-color: #ffffff;
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-size: 90%; mask-size: 80%;
} }
&::after { &::after {

View file

@ -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;
}
}
}
}

View file

@ -17,4 +17,9 @@ limitations under the License.
.mx_TextualEvent { .mx_TextualEvent {
opacity: 0.5; opacity: 0.5;
overflow-y: hidden; overflow-y: hidden;
a {
color: $accent-color;
cursor: pointer;
}
} }

View file

@ -21,7 +21,7 @@ limitations under the License.
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 80%;
} }
&.mx_cryptoEvent_icon::after { &.mx_cryptoEvent_icon::after {

View file

@ -45,7 +45,7 @@ limitations under the License.
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 80%;
} }
// transparent-looking border surrounding the shield for when overlain over avatars // transparent-looking border surrounding the shield for when overlain over avatars
@ -59,7 +59,7 @@ limitations under the License.
} }
// shrink the infill of the badge // shrink the infill of the badge
&::before { &::before {
mask-size: 65%; mask-size: 60%;
} }
} }

View file

@ -345,7 +345,7 @@ $hover-select-border: 4px;
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 80%;
} }
} }

View file

@ -73,7 +73,7 @@ limitations under the License.
} }
} }
.mx_AccessibleButton { .mx_AccessibleButton_hasKind {
padding: 8px 22px; padding: 8px 22px;
margin-left: auto; margin-left: auto;
display: block; display: block;

View file

@ -23,7 +23,7 @@ limitations under the License.
.mx_DialPad_button { .mx_DialPad_button {
width: 40px; width: 40px;
height: 40px; height: 40px;
background-color: $theme-button-bg-color; background-color: $dialpad-button-bg-color;
border-radius: 40px; border-radius: 40px;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;

View file

@ -27,9 +27,22 @@ limitations under the License.
} }
.mx_DialPadContextMenu_dialled { .mx_DialPadContextMenu_dialled {
height: 1em; height: 1.5em;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
max-width: 150px;
border: none;
margin: 0px;
}
.mx_DialPadContextMenu_dialled input {
font-size: 18px;
font-weight: 600;
overflow: hidden;
max-width: 150px;
text-align: left;
direction: rtl;
padding: 8px 0px;
background-color: rgb(0, 0, 0, 0);
} }
.mx_DialPadContextMenu_dialPad { .mx_DialPadContextMenu_dialPad {

View file

@ -0,0 +1,3 @@
<svg width="12" height="18" viewBox="0 0 12 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 14.25C5.175 14.25 4.5 14.925 4.5 15.75C4.5 16.575 5.175 17.25 6 17.25C6.825 17.25 7.5 16.575 7.5 15.75C7.5 14.925 6.825 14.25 6 14.25ZM1.5 0.75C0.675 0.75 0 1.425 0 2.25C0 3.075 0.675 3.75 1.5 3.75C2.325 3.75 3 3.075 3 2.25C3 1.425 2.325 0.75 1.5 0.75ZM1.5 5.25C0.675 5.25 0 5.925 0 6.75C0 7.575 0.675 8.25 1.5 8.25C2.325 8.25 3 7.575 3 6.75C3 5.925 2.325 5.25 1.5 5.25ZM1.5 9.75C0.675 9.75 0 10.425 0 11.25C0 12.075 0.675 12.75 1.5 12.75C2.325 12.75 3 12.075 3 11.25C3 10.425 2.325 9.75 1.5 9.75ZM10.5 3.75C11.325 3.75 12 3.075 12 2.25C12 1.425 11.325 0.75 10.5 0.75C9.675 0.75 9 1.425 9 2.25C9 3.075 9.675 3.75 10.5 3.75ZM6 9.75C5.175 9.75 4.5 10.425 4.5 11.25C4.5 12.075 5.175 12.75 6 12.75C6.825 12.75 7.5 12.075 7.5 11.25C7.5 10.425 6.825 9.75 6 9.75ZM10.5 9.75C9.675 9.75 9 10.425 9 11.25C9 12.075 9.675 12.75 10.5 12.75C11.325 12.75 12 12.075 12 11.25C12 10.425 11.325 9.75 10.5 9.75ZM10.5 5.25C9.675 5.25 9 5.925 9 6.75C9 7.575 9.675 8.25 10.5 8.25C11.325 8.25 12 7.575 12 6.75C12 5.925 11.325 5.25 10.5 5.25ZM6 5.25C5.175 5.25 4.5 5.925 4.5 6.75C4.5 7.575 5.175 8.25 6 8.25C6.825 8.25 7.5 7.575 7.5 6.75C7.5 5.925 6.825 5.25 6 5.25ZM6 0.75C5.175 0.75 4.5 1.425 4.5 2.25C4.5 3.075 5.175 3.75 6 3.75C6.825 3.75 7.5 3.075 7.5 2.25C7.5 1.425 6.825 0.75 6 0.75Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -118,6 +118,9 @@ $voipcall-plinth-color: #394049;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #6F7882;
;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $bg-color; $roomlist-filter-active-bg-color: $bg-color;

View file

@ -114,6 +114,8 @@ $voipcall-plinth-color: #394049;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #6F7882;
;
$roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $roomlist-button-bg-color; $roomlist-filter-active-bg-color: $roomlist-button-bg-color;

View file

@ -181,6 +181,8 @@ $voipcall-plinth-color: #F4F6FA;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #e3e8f0;
$roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $roomlist-button-bg-color; $roomlist-filter-active-bg-color: $roomlist-button-bg-color;

View file

@ -173,6 +173,8 @@ $voipcall-plinth-color: #F4F6FA;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #e3e8f0;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: #ffffff; $roomlist-filter-active-bg-color: #ffffff;

View file

@ -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[];

View file

@ -68,7 +68,7 @@ export const Notifier = {
// or not // or not
pendingEncryptedEventIds: [], pendingEncryptedEventIds: [],
notificationMessageForEvent: function(ev: MatrixEvent) { notificationMessageForEvent: function(ev: MatrixEvent): string {
if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) { if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) {
return typehandlers[ev.getContent().msgtype](ev); return typehandlers[ev.getContent().msgtype](ev);
} }

View file

@ -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;

View file

@ -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();

View file

@ -13,6 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import * as Roles from './Roles'; import * as Roles from './Roles';
@ -20,6 +22,11 @@ import {isValid3pidInvite} from "./RoomInvite";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList"; import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore"; import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
import { RightPanelPhases } from './stores/RightPanelStorePhases';
import { Action } from './dispatcher/actions';
import defaultDispatcher from './dispatcher/dispatcher';
import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
// These functions are frequently used just to check whether an event has // These functions are frequently used just to check whether an event has
// any text to display at all. For this reason they return deferred values // any text to display at all. For this reason they return deferred values
@ -31,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
senderName, ? _t('%(senderName)s withdrew %(targetName)s\'s invitation: %(reason)s', {
targetName, senderName,
}) + ' ' + getReason(); targetName,
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;
} }
@ -466,9 +486,33 @@ function textForPowerEvent(event): () => string | null {
}); });
} }
function textForPinnedEvent(event): () => string | null { const onPinnedMessagesClick = (): void => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.PinnedMessages,
allowClose: false,
});
}
function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null {
if (!SettingsStore.getValue("feature_pinning")) return null;
const senderName = event.sender ? event.sender.name : event.getSender(); const senderName = event.sender ? event.sender.name : event.getSender();
return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
if (allowJSX) {
return () => (
<span>
{
_t(
"%(senderName)s changed the <a>pinned messages</a> for the room.",
{ senderName },
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> },
)
}
</span>
);
}
return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName });
} }
function textForWidgetEvent(event): () => string | null { function textForWidgetEvent(event): () => string | null {
@ -594,7 +638,7 @@ function textForMjolnirEvent(event): () => string | null {
} }
interface IHandlers { interface IHandlers {
[type: string]: (ev: any) => (() => string | null); [type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null);
} }
const handlers: IHandlers = { const handlers: IHandlers = {
@ -635,7 +679,9 @@ export function hasText(ev): boolean {
return Boolean(handler?.(ev)); return Boolean(handler?.(ev));
} }
export function textForEvent(ev): string { export function textForEvent(ev: MatrixEvent): string;
export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element;
export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
return handler?.(ev)?.() || ''; return handler?.(ev, allowJSX)?.() || '';
} }

View file

@ -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;
} }

View file

@ -24,6 +24,7 @@ import CustomRoomTagPanel from "./CustomRoomTagPanel";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import RoomList from "../views/rooms/RoomList"; import RoomList from "../views/rooms/RoomList";
import CallHandler from "../../CallHandler";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist"; import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import UserMenu from "./UserMenu"; import UserMenu from "./UserMenu";
@ -124,6 +125,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.setState({ activeSpace }); this.setState({ activeSpace });
}; };
private onDialPad = () => {
dis.fire(Action.OpenDialPad);
}
private onExplore = () => { private onExplore = () => {
dis.fire(Action.ViewRoomDirectory); dis.fire(Action.ViewRoomDirectory);
}; };
@ -397,7 +402,20 @@ export default class LeftPanel extends React.Component<IProps, IState> {
} }
} }
private renderSearchExplore(): React.ReactNode { private renderSearchDialExplore(): React.ReactNode {
let dialPadButton = null;
// If we have dialer support, show a button to bring up the dial pad
// to start a new call
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
dialPadButton =
<AccessibleTooltipButton
className={classNames("mx_LeftPanel_dialPadButton", {})}
onClick={this.onDialPad}
title={_t("Open dial pad")}
/>;
}
return ( return (
<div <div
className="mx_LeftPanel_filterContainer" className="mx_LeftPanel_filterContainer"
@ -410,6 +428,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onSelectRoom={this.selectRoom} onSelectRoom={this.selectRoom}
/> />
{dialPadButton}
<AccessibleTooltipButton <AccessibleTooltipButton
className={classNames("mx_LeftPanel_exploreButton", { className={classNames("mx_LeftPanel_exploreButton", {
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace, mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
@ -458,7 +479,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
{leftLeftPanel} {leftLeftPanel}
<aside className="mx_LeftPanel_roomListContainer"> <aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()} {this.renderHeader()}
{this.renderSearchExplore()} {this.renderSearchDialExplore()}
{this.renderBreadcrumbs()} {this.renderBreadcrumbs()}
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} /> <RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
<div className="mx_LeftPanel_roomListWrapper"> <div className="mx_LeftPanel_roomListWrapper">

View file

@ -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";

View file

@ -18,6 +18,7 @@ import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import Field from "../elements/Field";
import Dialpad from '../voip/DialPad'; import Dialpad from '../voip/DialPad';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@ -44,13 +45,21 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
this.setState({value: this.state.value + digit}); this.setState({value: this.state.value + digit});
} }
onChange = (ev) => {
this.setState({value: ev.target.value});
}
render() { render() {
return <ContextMenu {...this.props}> return <ContextMenu {...this.props}>
<div className="mx_DialPadContextMenu_header"> <div className="mx_DialPadContextMenu_header">
<div> <div>
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span> <span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
</div> </div>
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</div> <Field className="mx_DialPadContextMenu_dialled"
value={this.state.value} autoFocus={true}
onChange={this.onChange}
/>
</div> </div>
<div className="mx_DialPadContextMenu_horizSep" /> <div className="mx_DialPadContextMenu_horizSep" />
<div className="mx_DialPadContextMenu_dialPad"> <div className="mx_DialPadContextMenu_dialPad">

View file

@ -179,7 +179,7 @@ export default class MessageContextMenu extends React.Component {
pinnedIds.push(eventId); pinnedIds.push(eventId);
cli.setRoomAccountData(room.roomId, ReadPinsEventId, { cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
event_ids: [ event_ids: [
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids, ...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []),
eventId, eventId,
], ],
}); });

View file

@ -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';

View file

@ -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>

View file

@ -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();
} }

View file

@ -63,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef(); private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
state: IState = { state: IState = {
disabledButtonIds: [], disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
.map(b => b.id),
}; };
constructor(props) { constructor(props) {

View file

@ -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 {

View file

@ -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";

View file

@ -28,7 +28,7 @@ export default class TextualEvent extends React.Component {
}; };
render() { render() {
const text = TextForEvent.textForEvent(this.props.mxEvent); const text = TextForEvent.textForEvent(this.props.mxEvent, true);
if (text == null || text.length === 0) return null; if (text == null || text.length === 0) return null;
return ( return (
<div className="mx_TextualEvent">{ text }</div> <div className="mx_TextualEvent">{ text }</div>

View file

@ -503,7 +503,7 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
return member.powerLevel < levelToSend; return member.powerLevel < levelToSend;
}; };
const getPowerLevels = room => room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {}; const getPowerLevels = room => room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => { export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room)); const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));

View file

@ -17,13 +17,23 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler'; import { _td } from '../../../languageHandler';
import classNames from "classnames"; import classNames from "classnames";
import E2EIcon from './E2EIcon'; import E2EIcon from './E2EIcon';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseAvatar from '../avatars/BaseAvatar';
import PresenceLabel from "./PresenceLabel";
export enum PowerStatus {
Admin = "admin",
Moderator = "moderator",
}
const PowerLabel: Record<PowerStatus, string> = {
[PowerStatus.Admin]: _td("Admin"),
[PowerStatus.Moderator]: _td("Mod"),
}
const PRESENCE_CLASS = { const PRESENCE_CLASS = {
"offline": "mx_EntityTile_offline", "offline": "mx_EntityTile_offline",
@ -31,14 +41,14 @@ const PRESENCE_CLASS = {
"unavailable": "mx_EntityTile_unavailable", "unavailable": "mx_EntityTile_unavailable",
}; };
function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { function presenceClassForMember(presenceState: string, lastActiveAgo: number, showPresence: boolean): string {
if (showPresence === false) { if (showPresence === false) {
return 'mx_EntityTile_online_beenactive'; return 'mx_EntityTile_online_beenactive';
} }
// offline is split into two categories depending on whether we have // offline is split into two categories depending on whether we have
// a last_active_ago for them. // a last_active_ago for them.
if (presenceState == 'offline') { if (presenceState === 'offline') {
if (lastActiveAgo) { if (lastActiveAgo) {
return PRESENCE_CLASS['offline'] + '_beenactive'; return PRESENCE_CLASS['offline'] + '_beenactive';
} else { } else {
@ -51,29 +61,32 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
} }
} }
@replaceableComponent("views.rooms.EntityTile") interface IProps {
class EntityTile extends React.Component { name?: string;
static propTypes = { title?: string;
name: PropTypes.string, avatarJsx?: JSX.Element; // <BaseAvatar />
title: PropTypes.string, className?: string;
avatarJsx: PropTypes.any, // <BaseAvatar /> presenceState?: string;
className: PropTypes.string, presenceLastActiveAgo?: number;
presenceState: PropTypes.string, presenceLastTs?: number;
presenceLastActiveAgo: PropTypes.number, presenceCurrentlyActive?: boolean;
presenceLastTs: PropTypes.number, showInviteButton?: boolean;
presenceCurrentlyActive: PropTypes.bool, onClick?(): void;
showInviteButton: PropTypes.bool, suppressOnHover?: boolean;
shouldComponentUpdate: PropTypes.func, showPresence?: boolean;
onClick: PropTypes.func, subtextLabel?: string;
suppressOnHover: PropTypes.bool, e2eStatus?: string;
showPresence: PropTypes.bool, powerStatus?: PowerStatus;
subtextLabel: PropTypes.string, }
e2eStatus: PropTypes.string,
};
interface IState {
hover: boolean;
}
@replaceableComponent("views.rooms.EntityTile")
export default class EntityTile extends React.PureComponent<IProps, IState> {
static defaultProps = { static defaultProps = {
shouldComponentUpdate: function(nextProps, nextState) { return true; }, onClick: () => {},
onClick: function() {},
presenceState: "offline", presenceState: "offline",
presenceLastActiveAgo: 0, presenceLastActiveAgo: 0,
presenceLastTs: 0, presenceLastTs: 0,
@ -82,13 +95,12 @@ class EntityTile extends React.Component {
showPresence: true, showPresence: true,
}; };
state = { constructor(props: IProps) {
hover: false, super(props);
};
shouldComponentUpdate(nextProps, nextState) { this.state = {
if (this.state.hover !== nextState.hover) return true; hover: false,
return this.props.shouldComponentUpdate(nextProps, nextState); };
} }
render() { render() {
@ -110,7 +122,6 @@ class EntityTile extends React.Component {
const activeAgo = this.props.presenceLastActiveAgo ? const activeAgo = this.props.presenceLastActiveAgo ?
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1; (Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
const PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
let presenceLabel = null; let presenceLabel = null;
if (this.props.showPresence) { if (this.props.showPresence) {
presenceLabel = <PresenceLabel activeAgo={activeAgo} presenceLabel = <PresenceLabel activeAgo={activeAgo}
@ -155,10 +166,7 @@ class EntityTile extends React.Component {
let powerLabel; let powerLabel;
const powerStatus = this.props.powerStatus; const powerStatus = this.props.powerStatus;
if (powerStatus) { if (powerStatus) {
const powerText = { const powerText = PowerLabel[powerStatus];
[EntityTile.POWER_STATUS_MODERATOR]: _t("Mod"),
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
}[powerStatus];
powerLabel = <div className="mx_EntityTile_power">{powerText}</div>; powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
} }
@ -168,14 +176,12 @@ class EntityTile extends React.Component {
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />; e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />;
} }
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const av = this.props.avatarJsx || const av = this.props.avatarJsx ||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />; <BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />;
// The wrapping div is required to make the magic mouse listener work, for some reason. // The wrapping div is required to make the magic mouse listener work, for some reason.
return ( return (
<div ref={(c) => this.container = c} > <div>
<AccessibleButton <AccessibleButton
className={classNames(mainClassNames)} className={classNames(mainClassNames)}
title={this.props.title} title={this.props.title}
@ -193,8 +199,3 @@ class EntityTile extends React.Component {
); );
} }
} }
EntityTile.POWER_STATUS_MODERATOR = "moderator";
EntityTile.POWER_STATUS_ADMIN = "admin";
export default EntityTile;

View file

@ -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;

View file

@ -17,20 +17,33 @@ limitations under the License.
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from "../../../index";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
import EntityTile, { PowerStatus } from "./EntityTile";
import MemberAvatar from "./../avatars/MemberAvatar";
interface IProps {
member: RoomMember;
showPresence?: boolean;
}
interface IState {
statusMessage: string;
isRoomEncrypted: boolean;
e2eStatus: string;
}
@replaceableComponent("views.rooms.MemberTile") @replaceableComponent("views.rooms.MemberTile")
export default class MemberTile extends React.Component { export default class MemberTile extends React.Component<IProps, IState> {
static propTypes = { private userLastModifiedTime: number;
member: PropTypes.any.isRequired, // RoomMember private memberLastModifiedTime: number;
showPresence: PropTypes.bool,
};
static defaultProps = { static defaultProps = {
showPresence: true, showPresence: true,
@ -52,7 +65,7 @@ export default class MemberTile extends React.Component {
if (SettingsStore.getValue("feature_custom_status")) { if (SettingsStore.getValue("feature_custom_status")) {
const { user } = this.props.member; const { user } = this.props.member;
if (user) { if (user) {
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted); user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
} }
} }
@ -80,7 +93,7 @@ export default class MemberTile extends React.Component {
if (user) { if (user) {
user.removeListener( user.removeListener(
"User._unstable_statusMessage", "User._unstable_statusMessage",
this._onStatusMessageCommitted, this.onStatusMessageCommitted,
); );
} }
@ -91,8 +104,8 @@ export default class MemberTile extends React.Component {
} }
} }
onRoomStateEvents = ev => { private onRoomStateEvents = (ev: MatrixEvent): void => {
if (ev.getType() !== "m.room.encryption") return; if (ev.getType() !== EventType.RoomEncryption) return;
const { roomId } = this.props.member; const { roomId } = this.props.member;
if (ev.getRoomId() !== roomId) return; if (ev.getRoomId() !== roomId) return;
@ -105,17 +118,17 @@ export default class MemberTile extends React.Component {
this.updateE2EStatus(); this.updateE2EStatus();
}; };
onUserTrustStatusChanged = (userId, trustStatus) => { private onUserTrustStatusChanged = (userId: string, trustStatus: string): void => {
if (userId !== this.props.member.userId) return; if (userId !== this.props.member.userId) return;
this.updateE2EStatus(); this.updateE2EStatus();
}; };
onDeviceVerificationChanged = (userId, deviceId, deviceInfo) => { private onDeviceVerificationChanged = (userId: string, deviceId: string, deviceInfo: DeviceInfo): void => {
if (userId !== this.props.member.userId) return; if (userId !== this.props.member.userId) return;
this.updateE2EStatus(); this.updateE2EStatus();
}; };
async updateE2EStatus() { private async updateE2EStatus(): Promise<void> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const { userId } = this.props.member; const { userId } = this.props.member;
const isMe = userId === cli.getUserId(); const isMe = userId === cli.getUserId();
@ -143,32 +156,32 @@ export default class MemberTile extends React.Component {
}); });
} }
getStatusMessage() { private getStatusMessage(): string {
const { user } = this.props.member; const { user } = this.props.member;
if (!user) { if (!user) {
return ""; return "";
} }
return user._unstable_statusMessage; return user.unstable_statusMessage;
} }
_onStatusMessageCommitted = () => { private onStatusMessageCommitted = (): void => {
// The `User` object has observed a status message change. // The `User` object has observed a status message change.
this.setState({ this.setState({
statusMessage: this.getStatusMessage(), statusMessage: this.getStatusMessage(),
}); });
}; };
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean {
if ( if (
this.member_last_modified_time === undefined || this.memberLastModifiedTime === undefined ||
this.member_last_modified_time < nextProps.member.getLastModifiedTime() this.memberLastModifiedTime < nextProps.member.getLastModifiedTime()
) { ) {
return true; return true;
} }
if ( if (
nextProps.member.user && nextProps.member.user &&
(this.user_last_modified_time === undefined || (this.userLastModifiedTime === undefined ||
this.user_last_modified_time < nextProps.member.user.getLastModifiedTime()) this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime())
) { ) {
return true; return true;
} }
@ -181,18 +194,18 @@ export default class MemberTile extends React.Component {
return false; return false;
} }
onClick = e => { private onClick = (): void => {
dis.dispatch({ dis.dispatch({
action: Action.ViewUser, action: Action.ViewUser,
member: this.props.member, member: this.props.member,
}); });
}; };
_getDisplayName() { private getDisplayName(): string {
return this.props.member.name; return this.props.member.name;
} }
getPowerLabel() { private getPowerLabel(): string {
return _t("%(userName)s (power %(powerLevelNumber)s)", { return _t("%(userName)s (power %(powerLevelNumber)s)", {
userName: this.props.member.userId, userName: this.props.member.userId,
powerLevelNumber: this.props.member.powerLevel, powerLevelNumber: this.props.member.powerLevel,
@ -200,11 +213,8 @@ export default class MemberTile extends React.Component {
} }
render() { render() {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const EntityTile = sdk.getComponent('rooms.EntityTile');
const member = this.props.member; const member = this.props.member;
const name = this._getDisplayName(); const name = this.getDisplayName();
const presenceState = member.user ? member.user.presence : null; const presenceState = member.user ? member.user.presence : null;
let statusMessage = null; let statusMessage = null;
@ -217,13 +227,13 @@ export default class MemberTile extends React.Component {
); );
if (member.user) { if (member.user) {
this.user_last_modified_time = member.user.getLastModifiedTime(); this.userLastModifiedTime = member.user.getLastModifiedTime();
} }
this.member_last_modified_time = member.getLastModifiedTime(); this.memberLastModifiedTime = member.getLastModifiedTime();
const powerStatusMap = new Map([ const powerStatusMap = new Map([
[100, EntityTile.POWER_STATUS_ADMIN], [100, PowerStatus.Admin],
[50, EntityTile.POWER_STATUS_MODERATOR], [50, PowerStatus.Moderator],
]); ]);
// Find the nearest power level with a badge // Find the nearest power level with a badge

View file

@ -15,26 +15,23 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// number of milliseconds ago this user was last active.
// zero = unknown
activeAgo?: number;
// if true, activeAgo is an approximation and "Now" should
// be shown instead
currentlyActive?: boolean;
// offline, online, etc
presenceState?: string;
}
@replaceableComponent("views.rooms.PresenceLabel") @replaceableComponent("views.rooms.PresenceLabel")
export default class PresenceLabel extends React.Component { export default class PresenceLabel extends React.Component<IProps> {
static propTypes = {
// number of milliseconds ago this user was last active.
// zero = unknown
activeAgo: PropTypes.number,
// if true, activeAgo is an approximation and "Now" should
// be shown instead
currentlyActive: PropTypes.bool,
// offline, online, etc
presenceState: PropTypes.string,
};
static defaultProps = { static defaultProps = {
activeAgo: -1, activeAgo: -1,
presenceState: null, presenceState: null,
@ -42,29 +39,29 @@ export default class PresenceLabel extends React.Component {
// Return duration as a string using appropriate time units // Return duration as a string using appropriate time units
// XXX: This would be better handled using a culture-aware library, but we don't use one yet. // XXX: This would be better handled using a culture-aware library, but we don't use one yet.
getDuration(time) { private getDuration(time: number): string {
if (!time) return; if (!time) return;
const t = parseInt(time / 1000); const t = time / 1000;
const s = t % 60; const s = t % 60;
const m = parseInt(t / 60) % 60; const m = t / 60 % 60;
const h = parseInt(t / (60 * 60)) % 24; const h = t / (60 * 60) % 24;
const d = parseInt(t / (60 * 60 * 24)); const d = t / (60 * 60 * 24);
if (t < 60) { if (t < 60) {
if (t < 0) { if (t < 0) {
return _t("%(duration)ss", {duration: 0}); return _t("%(duration)ss", { duration: 0 });
} }
return _t("%(duration)ss", {duration: s}); return _t("%(duration)ss", { duration: s });
} }
if (t < 60 * 60) { if (t < 60 * 60) {
return _t("%(duration)sm", {duration: m}); return _t("%(duration)sm", { duration: m });
} }
if (t < 24 * 60 * 60) { if (t < 24 * 60 * 60) {
return _t("%(duration)sh", {duration: h}); return _t("%(duration)sh", { duration: h });
} }
return _t("%(duration)sd", {duration: d}); return _t("%(duration)sd", { duration: d });
} }
getPrettyPresence(presence, activeAgo, currentlyActive) { private getPrettyPresence(presence: string, activeAgo: number, currentlyActive: boolean): string {
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) { if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
const duration = this.getDuration(activeAgo); const duration = this.getDuration(activeAgo);
if (presence === "online") return _t("Online for %(duration)s", { duration: duration }); if (presence === "online") return _t("Online for %(duration)s", { duration: duration });

View file

@ -45,7 +45,6 @@ import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import CallHandler from "../../../CallHandler";
import SpaceStore, {ISuggestedRoom, SUGGESTED_ROOMS} from "../../../stores/SpaceStore"; import SpaceStore, {ISuggestedRoom, SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space"; import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@ -103,38 +102,6 @@ interface ITagAestheticsMap {
[tagId: TagID]: ITagAesthetics; [tagId: TagID]: ITagAesthetics;
} }
// If we have no dialer support, we just show the create chat dialog
const dmOnAddRoom = (dispatcher?: Dispatcher<ActionPayload>) => {
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
};
// If we have dialer support, show a context menu so the user can pick between
// the dialer and the create chat dialog
const dmAddRoomContextMenu = (onFinished: () => void) => {
return <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Start a Conversation")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.dispatch({action: "view_create_chat"});
}}
/>
<IconizedContextMenuOption
label={_t("Open dial pad")}
iconClassName="mx_RoomList_iconDialpad"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.fire(Action.OpenDialPad);
}}
/>
</IconizedContextMenuOptionList>;
};
const TAG_AESTHETICS: ITagAestheticsMap = { const TAG_AESTHETICS: ITagAestheticsMap = {
[DefaultTagID.Invite]: { [DefaultTagID.Invite]: {
sectionLabel: _td("Invites"), sectionLabel: _td("Invites"),
@ -151,8 +118,9 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
isInvite: false, isInvite: false,
defaultHidden: false, defaultHidden: false,
addRoomLabel: _td("Start chat"), addRoomLabel: _td("Start chat"),
// Either onAddRoom or addRoomContextMenu are set depending on whether we onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
// have dialer support. (dispatcher || defaultDispatcher).dispatch({action: 'view_create_chat'});
},
}, },
[DefaultTagID.Untagged]: { [DefaultTagID.Untagged]: {
sectionLabel: _td("Rooms"), sectionLabel: _td("Rooms"),
@ -271,7 +239,6 @@ function customTagAesthetics(tagId: TagID): ITagAesthetics {
export default class RoomList extends React.PureComponent<IProps, IState> { export default class RoomList extends React.PureComponent<IProps, IState> {
private dispatcherRef; private dispatcherRef;
private customTagStoreRef; private customTagStoreRef;
private tagAesthetics: ITagAestheticsMap;
private roomStoreToken: fbEmitter.EventSubscription; private roomStoreToken: fbEmitter.EventSubscription;
constructor(props: IProps) { constructor(props: IProps) {
@ -282,10 +249,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
suggestedRooms: SpaceStore.instance.suggestedRooms, suggestedRooms: SpaceStore.instance.suggestedRooms,
}; };
// shallow-copy from the template as we need to make modifications to it
this.tagAesthetics = objectShallowClone(TAG_AESTHETICS);
this.updateDmAddRoomAction();
} }
public componentDidMount(): void { public componentDidMount(): void {
@ -311,17 +274,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
}); });
}; };
private updateDmAddRoomAction() {
const dmTagAesthetics = objectShallowClone(TAG_AESTHETICS[DefaultTagID.DM]);
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
dmTagAesthetics.addRoomContextMenu = dmAddRoomContextMenu;
} else {
dmTagAesthetics.onAddRoom = dmOnAddRoom;
}
this.tagAesthetics[DefaultTagID.DM] = dmTagAesthetics;
}
private onAction = (payload: ActionPayload) => { private onAction = (payload: ActionPayload) => {
if (payload.action === Action.ViewRoomDelta) { if (payload.action === Action.ViewRoomDelta) {
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload; const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
@ -335,7 +287,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
}); });
} }
} else if (payload.action === Action.PstnSupportUpdated) { } else if (payload.action === Action.PstnSupportUpdated) {
this.updateDmAddRoomAction();
this.updateLists(); this.updateLists();
} }
}; };
@ -524,7 +475,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId) const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
? customTagAesthetics(orderedTagId) ? customTagAesthetics(orderedTagId)
: this.tagAesthetics[orderedTagId]; : TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
// The cost of mounting/unmounting this component offsets the cost // The cost of mounting/unmounting this component offsets the cost

View file

@ -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"));

View file

@ -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();

View file

@ -27,6 +27,11 @@ export interface SetRightPanelPhasePayload extends ActionPayload {
phase: RightPanelPhases; phase: RightPanelPhases;
refireParams?: SetRightPanelPhaseRefireParams; refireParams?: SetRightPanelPhaseRefireParams;
/**
* By default SetRightPanelPhase can close the panel, this allows overriding that behaviour
*/
allowClose?: boolean;
} }
export interface SetRightPanelPhaseRefireParams { export interface SetRightPanelPhaseRefireParams {

View file

@ -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.",
@ -558,6 +562,7 @@
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).", "%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
"%(senderName)s changed the <a>pinned messages</a> for the room.": "%(senderName)s changed the <a>pinned messages</a> for the room.",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
@ -1410,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",
@ -1576,8 +1582,6 @@
"Search": "Search", "Search": "Search",
"Voice call": "Voice call", "Voice call": "Voice call",
"Video call": "Video call", "Video call": "Video call",
"Start a Conversation": "Start a Conversation",
"Open dial pad": "Open dial pad",
"Invites": "Invites", "Invites": "Invites",
"Favourites": "Favourites", "Favourites": "Favourites",
"People": "People", "People": "People",
@ -2277,7 +2281,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.",
@ -2657,6 +2660,7 @@
"Explore Public Rooms": "Explore Public Rooms", "Explore Public Rooms": "Explore Public Rooms",
"Create a Group Chat": "Create a Group Chat", "Create a Group Chat": "Create a Group Chat",
"Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s", "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s",
"Open dial pad": "Open dial pad",
"Failed to reject invitation": "Failed to reject invitation", "Failed to reject invitation": "Failed to reject invitation",
"Cannot create rooms in this community": "Cannot create rooms in this community", "Cannot create rooms in this community": "Cannot create rooms in this community",
"You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.", "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",

View file

@ -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");
} }

View file

@ -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,
}; };

View file

@ -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",

View file

@ -161,6 +161,7 @@ export default class RightPanelStore extends Store<ActionPayload> {
case Action.SetRightPanelPhase: { case Action.SetRightPanelPhase: {
let targetPhase = payload.phase; let targetPhase = payload.phase;
let refireParams = payload.refireParams; let refireParams = payload.refireParams;
const allowClose = payload.allowClose ?? true;
// redirect to EncryptionPanel if there is an ongoing verification request // redirect to EncryptionPanel if there is an ongoing verification request
if (targetPhase === RightPanelPhases.RoomMemberInfo && payload.refireParams) { if (targetPhase === RightPanelPhases.RoomMemberInfo && payload.refireParams) {
const {member} = payload.refireParams; const {member} = payload.refireParams;
@ -192,7 +193,7 @@ export default class RightPanelStore extends Store<ActionPayload> {
}); });
} }
} else { } else {
if (targetPhase === this.state.lastRoomPhase && !refireParams) { if (targetPhase === this.state.lastRoomPhase && !refireParams && allowClose) {
this.setState({ this.setState({
showRoomPanel: !this.state.showRoomPanel, showRoomPanel: !this.state.showRoomPanel,
previousPhase: null, previousPhase: null,

View file

@ -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") {

View file

@ -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));
} }
} }

View file

@ -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);

View file

@ -1334,6 +1334,7 @@
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
version "3.2.3" version "3.2.3"
uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
@ -5772,10 +5773,10 @@ matrix-react-test-utils@^0.2.3:
"@babel/traverse" "^7.13.17" "@babel/traverse" "^7.13.17"
walk "^2.3.14" walk "^2.3.14"
matrix-widget-api@^0.1.0-beta.14: matrix-widget-api@^0.1.0-beta.15:
version "0.1.0-beta.14" version "0.1.0-beta.15"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745"
integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ== integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg==
dependencies: dependencies:
"@types/events" "^3.0.0" "@types/events" "^3.0.0"
events "^3.2.0" events "^3.2.0"