Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into export-conversations
This commit is contained in:
commit
544761329c
39 changed files with 757 additions and 472 deletions
|
@ -80,7 +80,7 @@
|
|||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.20",
|
||||
"matrix-js-sdk": "12.0.0",
|
||||
"matrix-widget-api": "^0.1.0-beta.14",
|
||||
"matrix-widget-api": "^0.1.0-beta.15",
|
||||
"minimist": "^1.2.5",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
|
@ -126,7 +126,7 @@
|
|||
"@sinonjs/fake-timers": "^7.0.2",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@types/counterpart": "^0.18.1",
|
||||
"@types/diff-match-patch": "^1.0.5",
|
||||
"@types/diff-match-patch": "^1.0.32",
|
||||
"@types/flux": "^3.1.9",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/linkifyjs": "^2.1.3",
|
||||
|
@ -175,7 +175,7 @@
|
|||
"jest": {
|
||||
"testEnvironment": "./__test-utils__/environment.js",
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.[jt]s"
|
||||
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||
],
|
||||
"setupFiles": [
|
||||
"jest-canvas-mock"
|
||||
|
|
|
@ -71,7 +71,7 @@ limitations under the License.
|
|||
&::before {
|
||||
background-color: #ffffff;
|
||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
mask-size: 90%;
|
||||
mask-size: 80%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
|
|
|
@ -295,6 +295,7 @@ limitations under the License.
|
|||
|
||||
.mx_InviteDialog_content {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,3 +317,42 @@ limitations under the License.
|
|||
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_multiInviterError {
|
||||
> h4 {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $secondary-fg-color;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
> div {
|
||||
.mx_InviteDialog_multiInviterError_entry {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.mx_InviteDialog_multiInviterError_entry_userProfile {
|
||||
.mx_InviteDialog_multiInviterError_entry_name {
|
||||
margin-left: 6px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
font-weight: $font-semi-bold;
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_multiInviterError_entry_userId {
|
||||
margin-left: 6px;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $tertiary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_multiInviterError_entry_error {
|
||||
margin-left: 32px;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
color: $notice-primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 90%;
|
||||
mask-size: 80%;
|
||||
}
|
||||
|
||||
&.mx_cryptoEvent_icon::after {
|
||||
|
|
|
@ -45,7 +45,7 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 90%;
|
||||
mask-size: 80%;
|
||||
}
|
||||
|
||||
// transparent-looking border surrounding the shield for when overlain over avatars
|
||||
|
@ -59,7 +59,7 @@ limitations under the License.
|
|||
}
|
||||
// shrink the infill of the badge
|
||||
&::before {
|
||||
mask-size: 65%;
|
||||
mask-size: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -345,7 +345,7 @@ $hover-select-border: 4px;
|
|||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 90%;
|
||||
mask-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ limitations under the License.
|
|||
.mx_DialPad_button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: $theme-button-bg-color;
|
||||
background-color: $dialpad-button-bg-color;
|
||||
border-radius: 40px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
|
|
|
@ -27,9 +27,22 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_DialPadContextMenu_dialled {
|
||||
height: 1em;
|
||||
height: 1.5em;
|
||||
font-size: 18px;
|
||||
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 {
|
||||
|
|
|
@ -118,6 +118,9 @@ $voipcall-plinth-color: #394049;
|
|||
// ********************
|
||||
|
||||
$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-filter-active-bg-color: $bg-color;
|
||||
|
|
|
@ -114,6 +114,8 @@ $voipcall-plinth-color: #394049;
|
|||
// ********************
|
||||
|
||||
$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-filter-active-bg-color: $roomlist-button-bg-color;
|
||||
|
|
|
@ -181,6 +181,8 @@ $voipcall-plinth-color: #F4F6FA;
|
|||
// ********************
|
||||
|
||||
$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-filter-active-bg-color: $roomlist-button-bg-color;
|
||||
|
|
|
@ -173,6 +173,8 @@ $voipcall-plinth-color: #F4F6FA;
|
|||
// ********************
|
||||
|
||||
$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-filter-active-bg-color: #ffffff;
|
||||
|
|
|
@ -15,20 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
declare module "diff-dom" {
|
||||
enum Action {
|
||||
AddElement = "addElement",
|
||||
AddTextElement = "addTextElement",
|
||||
RemoveTextElement = "removeTextElement",
|
||||
RemoveElement = "removeElement",
|
||||
ReplaceElement = "replaceElement",
|
||||
ModifyTextElement = "modifyTextElement",
|
||||
AddAttribute = "addAttribute",
|
||||
RemoveAttribute = "removeAttribute",
|
||||
ModifyAttribute = "modifyAttribute",
|
||||
}
|
||||
|
||||
export interface IDiff {
|
||||
action: Action;
|
||||
action: string;
|
||||
name: string;
|
||||
text?: string;
|
||||
route: number[];
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@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 SettingsStore from "./settings/SettingsStore";
|
||||
import {SettingLevel} from "./settings/SettingLevel";
|
||||
import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
export default {
|
||||
hasAnyLabeledDevices: async function() {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.some(d => !!d.label);
|
||||
},
|
||||
|
||||
getDevices: function() {
|
||||
// Only needed for Electron atm, though should work in modern browsers
|
||||
// once permission has been granted to the webapp
|
||||
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
||||
const audiooutput = [];
|
||||
const audioinput = [];
|
||||
const videoinput = [];
|
||||
|
||||
devices.forEach((device) => {
|
||||
switch (device.kind) {
|
||||
case 'audiooutput': audiooutput.push(device); break;
|
||||
case 'audioinput': audioinput.push(device); break;
|
||||
case 'videoinput': videoinput.push(device); break;
|
||||
}
|
||||
});
|
||||
|
||||
// console.log("Loaded WebRTC Devices", mediaDevices);
|
||||
return {
|
||||
audiooutput,
|
||||
audioinput,
|
||||
videoinput,
|
||||
};
|
||||
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
|
||||
},
|
||||
|
||||
loadDevices: function() {
|
||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||
|
||||
setMatrixCallAudioInput(audioDeviceId);
|
||||
setMatrixCallVideoInput(videoDeviceId);
|
||||
},
|
||||
|
||||
setAudioOutput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||
},
|
||||
|
||||
setAudioInput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||
setMatrixCallAudioInput(deviceId);
|
||||
},
|
||||
|
||||
setVideoInput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||
setMatrixCallVideoInput(deviceId);
|
||||
},
|
||||
|
||||
getAudioOutput: function() {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
||||
},
|
||||
|
||||
getAudioInput: function() {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
|
||||
},
|
||||
|
||||
getVideoInput: function() {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
|
||||
},
|
||||
};
|
120
src/MediaDeviceHandler.ts
Normal file
120
src/MediaDeviceHandler.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||
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 SettingsStore from "./settings/SettingsStore";
|
||||
import { SettingLevel } from "./settings/SettingLevel";
|
||||
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
|
||||
import EventEmitter from 'events';
|
||||
|
||||
interface IMediaDevices {
|
||||
audioOutput: Array<MediaDeviceInfo>;
|
||||
audioInput: Array<MediaDeviceInfo>;
|
||||
videoInput: Array<MediaDeviceInfo>;
|
||||
}
|
||||
|
||||
export enum MediaDeviceHandlerEvent {
|
||||
AudioOutputChanged = "audio_output_changed",
|
||||
}
|
||||
|
||||
export default class MediaDeviceHandler extends EventEmitter {
|
||||
private static internalInstance;
|
||||
|
||||
public static get instance(): MediaDeviceHandler {
|
||||
if (!MediaDeviceHandler.internalInstance) {
|
||||
MediaDeviceHandler.internalInstance = new MediaDeviceHandler();
|
||||
}
|
||||
return MediaDeviceHandler.internalInstance;
|
||||
}
|
||||
|
||||
public static async hasAnyLabeledDevices(): Promise<boolean> {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return devices.some(d => Boolean(d.label));
|
||||
}
|
||||
|
||||
public static async getDevices(): Promise<IMediaDevices> {
|
||||
// Only needed for Electron atm, though should work in modern browsers
|
||||
// once permission has been granted to the webapp
|
||||
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
|
||||
const audioOutput = [];
|
||||
const audioInput = [];
|
||||
const videoInput = [];
|
||||
|
||||
devices.forEach((device) => {
|
||||
switch (device.kind) {
|
||||
case 'audiooutput': audioOutput.push(device); break;
|
||||
case 'audioinput': audioInput.push(device); break;
|
||||
case 'videoinput': videoInput.push(device); break;
|
||||
}
|
||||
});
|
||||
|
||||
return { audioOutput, audioInput, videoInput };
|
||||
} catch (error) {
|
||||
console.warn('Unable to refresh WebRTC Devices: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves devices from the SettingsStore and tells the js-sdk to use them
|
||||
*/
|
||||
public static loadDevices(): void {
|
||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||
|
||||
setMatrixCallAudioInput(audioDeviceId);
|
||||
setMatrixCallVideoInput(videoDeviceId);
|
||||
}
|
||||
|
||||
public setAudioOutput(deviceId: string): void {
|
||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||
this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will not change the device that a potential call uses. The call will
|
||||
* need to be ended and started again for this change to take effect
|
||||
* @param {string} deviceId
|
||||
*/
|
||||
public setAudioInput(deviceId: string): void {
|
||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||
setMatrixCallAudioInput(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will not change the device that a potential call uses. The call will
|
||||
* need to be ended and started again for this change to take effect
|
||||
* @param {string} deviceId
|
||||
*/
|
||||
public setVideoInput(deviceId: string): void {
|
||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||
setMatrixCallVideoInput(deviceId);
|
||||
}
|
||||
|
||||
public static getAudioOutput(): string {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
||||
}
|
||||
|
||||
public static getAudioInput(): string {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
|
||||
}
|
||||
|
||||
public static getVideoInput(): string {
|
||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,15 +14,26 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import MultiInviter from './utils/MultiInviter';
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import MultiInviter, { CompletionStates } from './utils/MultiInviter';
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './';
|
||||
import { _t } from './languageHandler';
|
||||
import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||
import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog";
|
||||
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
||||
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
||||
import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore";
|
||||
import BaseAvatar from "./components/views/avatars/BaseAvatar";
|
||||
import { mediaFromMxc } from "./customisations/Media";
|
||||
|
||||
export interface IInviteResult {
|
||||
states: CompletionStates;
|
||||
inviter: MultiInviter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invites multiple addresses to a room
|
||||
|
@ -32,15 +41,15 @@ import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
|||
* no option to cancel.
|
||||
*
|
||||
* @param {string} roomId The ID of the room to invite to
|
||||
* @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||
* @returns {Promise} Promise
|
||||
*/
|
||||
export function inviteMultipleToRoom(roomId, addrs) {
|
||||
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
|
||||
const inviter = new MultiInviter(roomId);
|
||||
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
|
||||
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
||||
}
|
||||
|
||||
export function showStartChatInviteDialog(initialText) {
|
||||
export function showStartChatInviteDialog(initialText = ""): void {
|
||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
|
@ -49,7 +58,7 @@ export function showStartChatInviteDialog(initialText) {
|
|||
);
|
||||
}
|
||||
|
||||
export function showRoomInviteDialog(roomId, initialText = "") {
|
||||
export function showRoomInviteDialog(roomId: string, initialText = ""): void {
|
||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||
Modal.createTrackedDialog(
|
||||
"Invite Users", "", InviteDialog, {
|
||||
|
@ -61,14 +70,14 @@ export function showRoomInviteDialog(roomId, initialText = "") {
|
|||
);
|
||||
}
|
||||
|
||||
export function showCommunityRoomInviteDialog(roomId, communityName) {
|
||||
export function showCommunityRoomInviteDialog(roomId: string, communityName: string): void {
|
||||
Modal.createTrackedDialog(
|
||||
'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
}
|
||||
|
||||
export function showCommunityInviteDialog(communityId) {
|
||||
export function showCommunityInviteDialog(communityId: string): void {
|
||||
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
|
||||
if (chat) {
|
||||
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
|
||||
|
@ -83,7 +92,7 @@ export function showCommunityInviteDialog(communityId) {
|
|||
* @param {MatrixEvent} event The event to check
|
||||
* @returns {boolean} True if valid, false otherwise
|
||||
*/
|
||||
export function isValid3pidInvite(event) {
|
||||
export function isValid3pidInvite(event: MatrixEvent): boolean {
|
||||
if (!event || event.getType() !== "m.room.third_party_invite") return false;
|
||||
|
||||
// any events without these keys are not valid 3pid invites, so we ignore them
|
||||
|
@ -96,7 +105,7 @@ export function isValid3pidInvite(event) {
|
|||
return true;
|
||||
}
|
||||
|
||||
export function inviteUsersToRoom(roomId, userIds) {
|
||||
export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
|
||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
showAnyInviteErrors(result.states, room, result.inviter);
|
||||
|
@ -110,9 +119,14 @@ export function inviteUsersToRoom(roomId, userIds) {
|
|||
});
|
||||
}
|
||||
|
||||
export function showAnyInviteErrors(addrs, room, inviter) {
|
||||
export function showAnyInviteErrors(
|
||||
states: CompletionStates,
|
||||
room: Room,
|
||||
inviter: MultiInviter,
|
||||
userMap?: Map<string, Member>,
|
||||
): boolean {
|
||||
// Show user any errors
|
||||
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
|
||||
const failedUsers = Object.keys(states).filter(a => states[a] === 'error');
|
||||
if (failedUsers.length === 1 && inviter.fatal) {
|
||||
// Just get the first message because there was a fatal problem on the first
|
||||
// user. This usually means that no other users were attempted, making it
|
||||
|
@ -126,19 +140,47 @@ export function showAnyInviteErrors(addrs, room, inviter) {
|
|||
} else {
|
||||
const errorList = [];
|
||||
for (const addr of failedUsers) {
|
||||
if (addrs[addr] === "error") {
|
||||
if (states[addr] === "error") {
|
||||
const reason = inviter.getErrorText(addr);
|
||||
errorList.push(addr + ": " + reason);
|
||||
}
|
||||
}
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (errorList.length > 0) {
|
||||
// React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
|
||||
const description = <div>{errorList.map(e => <div key={e}>{e}</div>)}</div>;
|
||||
const description = <div className="mx_InviteDialog_multiInviterError">
|
||||
<h4>{ _t("We sent the others, but the below people couldn't be invited to <RoomName/>", {}, {
|
||||
RoomName: () => <b>{ room.name }</b>,
|
||||
}) }</h4>
|
||||
<div>
|
||||
{ failedUsers.map(addr => {
|
||||
const user = userMap?.get(addr) || cli.getUser(addr);
|
||||
const name = (user as Member).name || (user as User).rawDisplayName;
|
||||
const avatarUrl = (user as Member).getMxcAvatarUrl?.() || (user as User).avatarUrl;
|
||||
return <div key={addr} className="mx_InviteDialog_multiInviterError_entry">
|
||||
<div className="mx_InviteDialog_multiInviterError_entry_userProfile">
|
||||
<BaseAvatar
|
||||
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null}
|
||||
name={name}
|
||||
idName={user.userId}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span className="mx_InviteDialog_multiInviterError_entry_name">{ name }</span>
|
||||
<span className="mx_InviteDialog_multiInviterError_entry_userId">{ user.userId }</span>
|
||||
</div>
|
||||
<div className="mx_InviteDialog_multiInviterError_entry_error">
|
||||
{ inviter.getErrorText(addr) }
|
||||
</div>
|
||||
</div>;
|
||||
}) }
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
|
||||
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
||||
Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, {
|
||||
title: _t("Some invites couldn't be sent"),
|
||||
description,
|
||||
});
|
||||
return false;
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,15 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
export const addressTypes = [
|
||||
'mx-user-id', 'mx-room-id', 'email',
|
||||
];
|
||||
export const addressTypes = ['mx-user-id', 'mx-room-id', 'email'];
|
||||
|
||||
export enum AddressType {
|
||||
Email = "email",
|
||||
MatrixUserId = "mx-user-id",
|
||||
MatrixRoomId = "mx-room-id",
|
||||
}
|
||||
|
||||
// PropType definition for an object describing
|
||||
// an address that can be invited to a room (which
|
||||
|
@ -40,18 +44,13 @@ export const UserAddressType = PropTypes.shape({
|
|||
isKnown: PropTypes.bool,
|
||||
});
|
||||
|
||||
export function getAddressType(inputText) {
|
||||
const isEmailAddress = emailRegex.test(inputText);
|
||||
const isUserId = mxUserIdRegex.test(inputText);
|
||||
const isRoomId = mxRoomIdRegex.test(inputText);
|
||||
|
||||
// sanity check the input for user IDs
|
||||
if (isEmailAddress) {
|
||||
return 'email';
|
||||
} else if (isUserId) {
|
||||
return 'mx-user-id';
|
||||
} else if (isRoomId) {
|
||||
return 'mx-room-id';
|
||||
export function getAddressType(inputText: string): AddressType | null {
|
||||
if (emailRegex.test(inputText)) {
|
||||
return AddressType.Email;
|
||||
} else if (mxUserIdRegex.test(inputText)) {
|
||||
return AddressType.MatrixUserId;
|
||||
} else if (mxRoomIdRegex.test(inputText)) {
|
||||
return AddressType.MatrixRoomId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
|
@ -22,7 +22,7 @@ import { MatrixClient } from 'matrix-js-sdk/src/client';
|
|||
|
||||
import {Key} from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import MediaDeviceHandler from '../../MediaDeviceHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
|
@ -167,7 +167,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
||||
CallMediaHandler.loadDevices();
|
||||
MediaDeviceHandler.loadDevices();
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ import createRoom, {IOpts} from "../../createRoom";
|
|||
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import ThemeController from "../../settings/controllers/ThemeController";
|
||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
import { startAnyRegistrationFlow } from "../../Registration";
|
||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import Field from "../elements/Field";
|
||||
import Dialpad from '../voip/DialPad';
|
||||
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});
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
this.setState({value: ev.target.value});
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return <ContextMenu {...this.props}>
|
||||
<div className="mx_DialPadContextMenu_header">
|
||||
<div>
|
||||
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
||||
</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 className="mx_DialPadContextMenu_horizSep" />
|
||||
<div className="mx_DialPadContextMenu_dialPad">
|
||||
|
|
|
@ -179,7 +179,7 @@ export default class MessageContextMenu extends React.Component {
|
|||
pinnedIds.push(eventId);
|
||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||
event_ids: [
|
||||
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
|
||||
...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []),
|
||||
eventId,
|
||||
],
|
||||
});
|
||||
|
|
|
@ -39,6 +39,9 @@ import ProgressBar from "../elements/ProgressBar";
|
|||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
|
@ -204,6 +207,17 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
} : null;
|
||||
|
||||
const [truncateAt, setTruncateAt] = useState(20);
|
||||
function overflowTile(overflowCount, totalCount) {
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={() => setTruncateAt(totalCount)} />
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_AddExistingToSpace">
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
|
@ -216,16 +230,21 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
|||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpace_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
{ rooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>;
|
||||
}) }
|
||||
<TruncatedList
|
||||
truncateAt={truncateAt}
|
||||
createOverflowElement={overflowTile}
|
||||
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={onChange ? (checked) => {
|
||||
onChange(checked, room);
|
||||
} : null}
|
||||
/>,
|
||||
)}
|
||||
getChildCount={() => rooms.length}
|
||||
/>
|
||||
</div>
|
||||
) : undefined }
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { _t, _td } from '../../../languageHandler';
|
|||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import * as Email from '../../../email';
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
|
|
|
@ -40,6 +40,9 @@ import NotificationBadge from "../rooms/NotificationBadge";
|
|||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
|
||||
const AVATAR_SIZE = 30;
|
||||
|
||||
|
@ -196,6 +199,17 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
}).match(lcQuery);
|
||||
}
|
||||
|
||||
const [truncateAt, setTruncateAt] = useState(20);
|
||||
function overflowTile(overflowCount, totalCount) {
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={() => setTruncateAt(totalCount)} />
|
||||
);
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Forward message")}
|
||||
className="mx_ForwardDialog"
|
||||
|
@ -228,15 +242,20 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_ForwardList_results">
|
||||
{ rooms.map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
event={event}
|
||||
matrixClient={cli}
|
||||
onFinished={onFinished}
|
||||
/>,
|
||||
) }
|
||||
<TruncatedList
|
||||
truncateAt={truncateAt}
|
||||
createOverflowElement={overflowTile}
|
||||
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||
<Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
event={event}
|
||||
matrixClient={cli}
|
||||
onFinished={onFinished}
|
||||
/>,
|
||||
)}
|
||||
getChildCount={() => rooms.length}
|
||||
/>
|
||||
</div>
|
||||
) : <span className="mx_ForwardList_noResults">
|
||||
{ _t("No results") }
|
||||
|
|
|
@ -17,37 +17,45 @@ limitations under the License.
|
|||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import * as Email from "../../../email";
|
||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
|
||||
import { abbreviateUrl } from "../../../utils/UrlUtils";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import { humanizeTime } from "../../../utils/humanize";
|
||||
import createRoom, {
|
||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
||||
canEncryptToAllUsers,
|
||||
ensureDMExists,
|
||||
findDMForUser,
|
||||
privateShouldBeEncrypted,
|
||||
} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
import {
|
||||
IInviteResult,
|
||||
inviteMultipleToRoom,
|
||||
showAnyInviteErrors,
|
||||
showCommunityInviteDialog,
|
||||
} from "../../../RoomInvite";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import {getAddressType} from "../../../UserAddress";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { getAddressType } from "../../../UserAddress";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { compare } from '../../../utils/strings';
|
||||
|
@ -74,10 +82,10 @@ export const KIND_CALL_TRANSFER = "call_transfer";
|
|||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||
|
||||
// This is the interface that is expected by various components in this file. It is a bit
|
||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
|
||||
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||
// for 3PIDs/email addresses.
|
||||
abstract class Member {
|
||||
export abstract class Member {
|
||||
/**
|
||||
* The display name of this Member. For users this should be their profile's display
|
||||
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||
|
@ -102,7 +110,8 @@ class DirectoryMember extends Member {
|
|||
private readonly displayName: string;
|
||||
private readonly avatarUrl: string;
|
||||
|
||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||
// eslint-disable-next-line camelcase
|
||||
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
|
||||
super();
|
||||
this._userId = userDirResult.user_id;
|
||||
this.displayName = userDirResult.display_name;
|
||||
|
@ -601,19 +610,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||
}
|
||||
|
||||
private shouldAbortAfterInviteError(result): boolean {
|
||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
||||
if (failedUsers.length > 0) {
|
||||
console.log("Failed to invite users: ", result);
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
|
||||
csvUsers: failedUsers.join(", "),
|
||||
}),
|
||||
});
|
||||
return true; // abort
|
||||
}
|
||||
return false;
|
||||
private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean {
|
||||
this.setState({ busy: false });
|
||||
const userMap = new Map<string, Member>(this.state.targets.map(member => [member.userId, member]));
|
||||
return !showAnyInviteErrors(result.states, room, result.inviter, userMap);
|
||||
}
|
||||
|
||||
private convertFilter(): Member[] {
|
||||
|
@ -731,7 +731,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
try {
|
||||
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
||||
if (!this.shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||
if (!this.shouldAbortAfterInviteError(result, room)) { // handles setting error message too
|
||||
this.props.onFinished();
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
|||
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||
|
||||
state: IState = {
|
||||
disabledButtonIds: [],
|
||||
disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
|
||||
.map(b => b.id),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
|
|
@ -20,9 +20,9 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import * as sdk from "../../../index";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { UserAddressType } from '../../../UserAddress.js';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import {mediaFromMxc} from "../../../customisations/Media";
|
||||
import { UserAddressType } from '../../../UserAddress';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.elements.AddressTile")
|
||||
export default class AddressTile extends React.Component {
|
||||
|
|
|
@ -16,31 +16,29 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.elements.TruncatedList")
|
||||
export default class TruncatedList extends React.Component {
|
||||
static propTypes = {
|
||||
// The number of elements to show before truncating. If negative, no truncation is done.
|
||||
truncateAt: PropTypes.number,
|
||||
// The className to apply to the wrapping div
|
||||
className: PropTypes.string,
|
||||
// A function that returns the children to be rendered into the element.
|
||||
// function getChildren(start: number, end: number): Array<React.Node>
|
||||
// The start element is included, the end is not (as in `slice`).
|
||||
// If omitted, the React child elements will be used. This parameter can be used
|
||||
// to avoid creating unnecessary React elements.
|
||||
getChildren: PropTypes.func,
|
||||
// A function that should return the total number of child element available.
|
||||
// Required if getChildren is supplied.
|
||||
getChildCount: PropTypes.func,
|
||||
// A function which will be invoked when an overflow element is required.
|
||||
// This will be inserted after the children.
|
||||
createOverflowElement: PropTypes.func,
|
||||
};
|
||||
interface IProps {
|
||||
// The number of elements to show before truncating. If negative, no truncation is done.
|
||||
truncateAt?: number;
|
||||
// The className to apply to the wrapping div
|
||||
className?: string;
|
||||
// A function that returns the children to be rendered into the element.
|
||||
// The start element is included, the end is not (as in `slice`).
|
||||
// If omitted, the React child elements will be used. This parameter can be used
|
||||
// to avoid creating unnecessary React elements.
|
||||
getChildren?: (start: number, end: number) => Array<React.ReactNode>;
|
||||
// A function that should return the total number of child element available.
|
||||
// Required if getChildren is supplied.
|
||||
getChildCount?: () => number;
|
||||
// A function which will be invoked when an overflow element is required.
|
||||
// This will be inserted after the children.
|
||||
createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.TruncatedList")
|
||||
export default class TruncatedList extends React.Component<IProps> {
|
||||
static defaultProps ={
|
||||
truncateAt: 2,
|
||||
createOverflowElement(overflowCount, totalCount) {
|
||||
|
@ -50,7 +48,7 @@ export default class TruncatedList extends React.Component {
|
|||
},
|
||||
};
|
||||
|
||||
_getChildren(start, end) {
|
||||
private getChildren(start: number, end: number): Array<React.ReactNode> {
|
||||
if (this.props.getChildren && this.props.getChildCount) {
|
||||
return this.props.getChildren(start, end);
|
||||
} else {
|
||||
|
@ -63,7 +61,7 @@ export default class TruncatedList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getChildCount() {
|
||||
private getChildCount(): number {
|
||||
if (this.props.getChildren && this.props.getChildCount) {
|
||||
return this.props.getChildCount();
|
||||
} else {
|
||||
|
@ -73,10 +71,10 @@ export default class TruncatedList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
let overflowNode = null;
|
||||
|
||||
const totalChildren = this._getChildCount();
|
||||
const totalChildren = this.getChildCount();
|
||||
let upperBound = totalChildren;
|
||||
if (this.props.truncateAt >= 0) {
|
||||
const overflowCount = totalChildren - this.props.truncateAt;
|
||||
|
@ -87,7 +85,7 @@ export default class TruncatedList extends React.Component {
|
|||
upperBound = this.props.truncateAt;
|
||||
}
|
||||
}
|
||||
const childNodes = this._getChildren(0, upperBound);
|
||||
const childNodes = this.getChildren(0, upperBound);
|
||||
|
||||
return (
|
||||
<div className={this.props.className}>
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Flair from '../elements/Flair.js';
|
||||
import Flair from '../elements/Flair';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,17 +21,28 @@ import React from 'react';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
import rate_limited_func from "../../../ratelimitedfunc";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import rateLimitedFunction from "../../../ratelimitedfunc";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import TruncatedList from '../elements/TruncatedList';
|
||||
import Spinner from "../elements/Spinner";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import EntityTile from "./EntityTile";
|
||||
import MemberTile from "./MemberTile";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -40,41 +52,59 @@ const SHOW_MORE_INCREMENT = 100;
|
|||
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
||||
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
loading: boolean;
|
||||
members: Array<RoomMember>;
|
||||
filteredJoinedMembers: Array<RoomMember>;
|
||||
filteredInvitedMembers: Array<RoomMember | MatrixEvent>;
|
||||
canInvite: boolean;
|
||||
truncateAtJoined: number;
|
||||
truncateAtInvited: number;
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.MemberList")
|
||||
export default class MemberList extends React.Component {
|
||||
export default class MemberList extends React.Component<IProps, IState> {
|
||||
private showPresence = true;
|
||||
private mounted = false;
|
||||
private collator: Intl.Collator;
|
||||
private sortNames = new Map<RoomMember, string>(); // RoomMember -> sortName
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
// show an empty list
|
||||
this.state = this._getMembersState([]);
|
||||
this.state = this.getMembersState([]);
|
||||
} else {
|
||||
this.state = this._getMembersState(this.roomMembers());
|
||||
this.state = this.getMembersState(this.roomMembers());
|
||||
}
|
||||
|
||||
cli.on("Room", this.onRoom); // invites & joining after peek
|
||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||
this._showPresence = true;
|
||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
||||
this._showPresence = enablePresenceByHsUrl[hsUrl];
|
||||
}
|
||||
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
this._mounted = true;
|
||||
this.mounted = true;
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
this.showMembersAccordingToMembershipWithLL();
|
||||
cli.on("Room.myMembership", this.onMyMembership);
|
||||
} else {
|
||||
this._listenForMembersChanges();
|
||||
this.listenForMembersChanges();
|
||||
}
|
||||
}
|
||||
|
||||
_listenForMembersChanges() {
|
||||
private listenForMembersChanges(): void {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on("RoomState.members", this.onRoomStateMember);
|
||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||
|
@ -89,7 +119,7 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
this.mounted = false;
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli) {
|
||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||
|
@ -103,7 +133,7 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateList.cancelPendingCall();
|
||||
this.updateList.cancelPendingCall();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,7 +141,7 @@ export default class MemberList extends React.Component {
|
|||
* show a spinner and load the members if the user is joined,
|
||||
* or show the members available so far if the user is invited
|
||||
*/
|
||||
async _showMembersAccordingToMembershipWithLL() {
|
||||
private async showMembersAccordingToMembershipWithLL(): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (cli.hasLazyLoadMembersEnabled()) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
@ -122,31 +152,31 @@ export default class MemberList extends React.Component {
|
|||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
} catch (ex) {/* already logged in RoomView */}
|
||||
if (this._mounted) {
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
this._listenForMembersChanges();
|
||||
if (this.mounted) {
|
||||
this.setState(this.getMembersState(this.roomMembers()));
|
||||
this.listenForMembersChanges();
|
||||
}
|
||||
} else {
|
||||
// show the members we already have loaded
|
||||
this.setState(this._getMembersState(this.roomMembers()));
|
||||
this.setState(this.getMembersState(this.roomMembers()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get canInvite() {
|
||||
private get canInvite(): boolean {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
return room && room.canInvite(cli.getUserId());
|
||||
}
|
||||
|
||||
_getMembersState(members) {
|
||||
// set the state after determining _showPresence to make sure it's
|
||||
// taken into account while rerendering
|
||||
private getMembersState(members: Array<RoomMember>): IState {
|
||||
// set the state after determining showPresence to make sure it's
|
||||
// taken into account while rendering
|
||||
return {
|
||||
loading: false,
|
||||
members: members,
|
||||
filteredJoinedMembers: this._filterMembers(members, 'join'),
|
||||
filteredInvitedMembers: this._filterMembers(members, 'invite'),
|
||||
filteredJoinedMembers: this.filterMembers(members, 'join'),
|
||||
filteredInvitedMembers: this.filterMembers(members, 'invite'),
|
||||
canInvite: this.canInvite,
|
||||
|
||||
// ideally we'd size this to the page height, but
|
||||
|
@ -157,72 +187,72 @@ export default class MemberList extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
onUserPresenceChange = (event, user) => {
|
||||
private onUserPresenceChange = (event: MatrixEvent, user: User): void => {
|
||||
// Attach a SINGLE listener for global presence changes then locate the
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
// ever attaching their own listener.
|
||||
const tile = this.refs[user.userId];
|
||||
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
|
||||
if (tile) {
|
||||
this._updateList(); // reorder the membership list
|
||||
this.updateList(); // reorder the membership list
|
||||
}
|
||||
};
|
||||
|
||||
onRoom = room => {
|
||||
private onRoom = (room: Room): void => {
|
||||
if (room.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
// We listen for room events because when we accept an invite
|
||||
// we need to wait till the room is fully populated with state
|
||||
// before refreshing the member list else we get a stale list.
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
this.showMembersAccordingToMembershipWithLL();
|
||||
};
|
||||
|
||||
onMyMembership = (room, membership, oldMembership) => {
|
||||
private onMyMembership = (room: Room, membership: string, oldMembership: string): void => {
|
||||
if (room.roomId === this.props.roomId && membership === "join") {
|
||||
this._showMembersAccordingToMembershipWithLL();
|
||||
this.showMembersAccordingToMembershipWithLL();
|
||||
}
|
||||
};
|
||||
|
||||
onRoomStateMember = (ev, state, member) => {
|
||||
private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
this.updateList();
|
||||
};
|
||||
|
||||
onRoomMemberName = (ev, member) => {
|
||||
private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => {
|
||||
if (member.roomId !== this.props.roomId) {
|
||||
return;
|
||||
}
|
||||
this._updateList();
|
||||
this.updateList();
|
||||
};
|
||||
|
||||
onRoomStateEvent = (event, state) => {
|
||||
private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => {
|
||||
if (event.getRoomId() === this.props.roomId &&
|
||||
event.getType() === "m.room.third_party_invite") {
|
||||
this._updateList();
|
||||
this.updateList();
|
||||
}
|
||||
|
||||
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
||||
};
|
||||
|
||||
_updateList = rate_limited_func(() => {
|
||||
this._updateListNow();
|
||||
private updateList = rateLimitedFunction(() => {
|
||||
this.updateListNow();
|
||||
}, 500);
|
||||
|
||||
_updateListNow() {
|
||||
// console.log("Updating memberlist");
|
||||
const newState = {
|
||||
private updateListNow(): void {
|
||||
const members = this.roomMembers()
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
members: this.roomMembers(),
|
||||
};
|
||||
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
|
||||
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery);
|
||||
this.setState(newState);
|
||||
members: members,
|
||||
filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery),
|
||||
filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery),
|
||||
});
|
||||
}
|
||||
|
||||
getMembersWithUser() {
|
||||
private getMembersWithUser(): Array<RoomMember> {
|
||||
if (!this.props.roomId) return [];
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
|
@ -230,15 +260,18 @@ export default class MemberList extends React.Component {
|
|||
|
||||
const allMembers = Object.values(room.currentState.members);
|
||||
|
||||
allMembers.forEach(function(member) {
|
||||
allMembers.forEach((member) => {
|
||||
// work around a race where you might have a room member object
|
||||
// before the user object exists. This may or may not cause
|
||||
// before the user object exists. This may or may not cause
|
||||
// https://github.com/vector-im/vector-web/issues/186
|
||||
if (member.user === null) {
|
||||
if (!member.user) {
|
||||
member.user = cli.getUser(member.userId);
|
||||
}
|
||||
|
||||
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
|
||||
this.sortNames.set(
|
||||
member,
|
||||
(member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""),
|
||||
);
|
||||
|
||||
// XXX: this user may have no lastPresenceTs value!
|
||||
// the right solution here is to fix the race rather than leave it as 0
|
||||
|
@ -247,7 +280,7 @@ export default class MemberList extends React.Component {
|
|||
return allMembers;
|
||||
}
|
||||
|
||||
roomMembers() {
|
||||
private roomMembers(): Array<RoomMember> {
|
||||
const allMembers = this.getMembersWithUser();
|
||||
const filteredAndSortedMembers = allMembers.filter((m) => {
|
||||
return (
|
||||
|
@ -255,23 +288,21 @@ export default class MemberList extends React.Component {
|
|||
);
|
||||
});
|
||||
const language = SettingsStore.getValue("language");
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
|
||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', ignorePunctuation: false });
|
||||
filteredAndSortedMembers.sort(this.memberSort);
|
||||
return filteredAndSortedMembers;
|
||||
}
|
||||
|
||||
_createOverflowTileJoined = (overflowCount, totalCount) => {
|
||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
|
||||
private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||
return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList);
|
||||
};
|
||||
|
||||
_createOverflowTileInvited = (overflowCount, totalCount) => {
|
||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
|
||||
private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||
return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList);
|
||||
};
|
||||
|
||||
_createOverflowTile = (overflowCount, totalCount, onClick) => {
|
||||
private createOverflowTile = (overflowCount: number, totalCount: number, onClick: () => void): JSX.Element=> {
|
||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
|
@ -281,31 +312,48 @@ export default class MemberList extends React.Component {
|
|||
);
|
||||
};
|
||||
|
||||
_showMoreJoinedMemberList = () => {
|
||||
private showMoreJoinedMemberList = (): void => {
|
||||
this.setState({
|
||||
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
||||
});
|
||||
};
|
||||
|
||||
_showMoreInvitedMemberList = () => {
|
||||
private showMoreInvitedMemberList = (): void => {
|
||||
this.setState({
|
||||
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
||||
});
|
||||
};
|
||||
|
||||
memberString(member) {
|
||||
/**
|
||||
* SHOULD ONLY BE USED BY TESTS
|
||||
*/
|
||||
public memberString(member: RoomMember): string {
|
||||
if (!member) {
|
||||
return "(null)";
|
||||
} else {
|
||||
const u = member.user;
|
||||
return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "<null>") + ", " + (u ? u.getLastActiveTs() : "<null>") + ", " + (u ? u.currentlyActive : "<null>") + ", " + (u ? u.presence : "<null>") + ")";
|
||||
return (
|
||||
"(" +
|
||||
member.name +
|
||||
", " +
|
||||
member.powerLevel +
|
||||
", " +
|
||||
(u ? u.lastActiveAgo : "<null>") +
|
||||
", " +
|
||||
(u ? u.getLastActiveTs() : "<null>") +
|
||||
", " +
|
||||
(u ? u.currentlyActive : "<null>") +
|
||||
", " +
|
||||
(u ? u.presence : "<null>") +
|
||||
")"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// returns negative if a comes before b,
|
||||
// returns 0 if a and b are equivalent in ordering
|
||||
// returns positive if a comes after b.
|
||||
memberSort = (memberA, memberB) => {
|
||||
private memberSort = (memberA: RoomMember, memberB: RoomMember): number => {
|
||||
// order by presence, with "active now" first.
|
||||
// ...and then by power level
|
||||
// ...and then by last active
|
||||
|
@ -325,7 +373,7 @@ export default class MemberList extends React.Component {
|
|||
if (!userA && userB) return 1;
|
||||
|
||||
// First by presence
|
||||
if (this._showPresence) {
|
||||
if (this.showPresence) {
|
||||
const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
|
||||
const presenceIndex = p => {
|
||||
const order = ['active', 'online', 'offline'];
|
||||
|
@ -349,31 +397,31 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
// Third by last active
|
||||
if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
||||
if (this.showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
||||
// console.log("Comparing on last active timestamp - returning");
|
||||
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
||||
}
|
||||
|
||||
// Fourth by name (alphabetical)
|
||||
return this.collator.compare(memberA.sortName, memberB.sortName);
|
||||
return this.collator.compare(this.sortNames.get(memberA), this.sortNames.get(memberB));
|
||||
};
|
||||
|
||||
onSearchQueryChanged = searchQuery => {
|
||||
private onSearchQueryChanged = (searchQuery: string): void => {
|
||||
this.setState({
|
||||
searchQuery,
|
||||
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery),
|
||||
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery),
|
||||
filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery),
|
||||
filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery),
|
||||
});
|
||||
};
|
||||
|
||||
_onPending3pidInviteClick = inviteEvent => {
|
||||
private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
|
||||
dis.dispatch({
|
||||
action: 'view_3pid_invite',
|
||||
event: inviteEvent,
|
||||
});
|
||||
};
|
||||
|
||||
_filterMembers(members, membership, query) {
|
||||
private filterMembers(members: Array<RoomMember>, membership: string, query?: string): Array<RoomMember> {
|
||||
return members.filter((m) => {
|
||||
if (query) {
|
||||
query = query.toLowerCase();
|
||||
|
@ -389,7 +437,7 @@ export default class MemberList extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_getPending3PidInvites() {
|
||||
private getPending3PidInvites(): Array<MatrixEvent> {
|
||||
// include 3pid invites (m.room.third_party_invite) state events.
|
||||
// The HS may have already converted these into m.room.member invites so
|
||||
// we shouldn't add them if the 3pid invite state key (token) is in the
|
||||
|
@ -409,42 +457,40 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_makeMemberTiles(members) {
|
||||
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
|
||||
private makeMemberTiles(members: Array<RoomMember | MatrixEvent>) {
|
||||
return members.map((m) => {
|
||||
if (m.userId) {
|
||||
if (m instanceof RoomMember) {
|
||||
// Is a Matrix invite
|
||||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this._showPresence} />;
|
||||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
|
||||
} else {
|
||||
// Is a 3pid invite
|
||||
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
|
||||
onClick={() => this._onPending3pidInviteClick(m)} />;
|
||||
onClick={() => this.onPending3pidInviteClick(m)} />;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getChildrenJoined = (start, end) => this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
|
||||
|
||||
_getChildCountJoined = () => this.state.filteredJoinedMembers.length;
|
||||
|
||||
_getChildrenInvited = (start, end) => {
|
||||
let targets = this.state.filteredInvitedMembers;
|
||||
if (end > this.state.filteredInvitedMembers.length) {
|
||||
targets = targets.concat(this._getPending3PidInvites());
|
||||
}
|
||||
|
||||
return this._makeMemberTiles(targets.slice(start, end));
|
||||
private getChildrenJoined = (start: number, end: number): Array<JSX.Element> => {
|
||||
return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end))
|
||||
};
|
||||
|
||||
_getChildCountInvited = () => {
|
||||
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length;
|
||||
private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length;
|
||||
|
||||
private getChildrenInvited = (start: number, end: number): Array<JSX.Element> => {
|
||||
let targets = this.state.filteredInvitedMembers;
|
||||
if (end > this.state.filteredInvitedMembers.length) {
|
||||
targets = targets.concat(this.getPending3PidInvites());
|
||||
}
|
||||
|
||||
return this.makeMemberTiles(targets.slice(start, end));
|
||||
};
|
||||
|
||||
private getChildCountInvited = (): number => {
|
||||
return this.state.filteredInvitedMembers.length + (this.getPending3PidInvites() || []).length;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <BaseCard
|
||||
className="mx_MemberList"
|
||||
onClose={this.props.onClose}
|
||||
|
@ -454,9 +500,6 @@ export default class MemberList extends React.Component {
|
|||
</BaseCard>;
|
||||
}
|
||||
|
||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
let inviteButton;
|
||||
|
@ -470,22 +513,30 @@ export default class MemberList extends React.Component {
|
|||
inviteButtonText = _t("Invite to this space");
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
inviteButton =
|
||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!this.state.canInvite}>
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
className="mx_MemberList_invite"
|
||||
onClick={this.onInviteButtonClick}
|
||||
disabled={!this.state.canInvite}
|
||||
>
|
||||
<span>{ inviteButtonText }</span>
|
||||
</AccessibleButton>;
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let invitedHeader;
|
||||
let invitedSection;
|
||||
if (this._getChildCountInvited() > 0) {
|
||||
if (this.getChildCountInvited() > 0) {
|
||||
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
||||
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited}
|
||||
createOverflowElement={this._createOverflowTileInvited}
|
||||
getChildren={this._getChildrenInvited}
|
||||
getChildCount={this._getChildCountInvited}
|
||||
/>;
|
||||
invitedSection = (
|
||||
<TruncatedList
|
||||
className="mx_MemberList_section mx_MemberList_invited"
|
||||
truncateAt={this.state.truncateAtInvited}
|
||||
createOverflowElement={this.createOverflowTileInvited}
|
||||
getChildren={this.getChildrenInvited}
|
||||
getChildCount={this.getChildCountInvited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const footer = (
|
||||
|
@ -517,17 +568,19 @@ export default class MemberList extends React.Component {
|
|||
previousPhase={previousPhase}
|
||||
>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this._createOverflowTileJoined}
|
||||
getChildren={this._getChildrenJoined}
|
||||
getChildCount={this._getChildCountJoined} />
|
||||
<TruncatedList
|
||||
className="mx_MemberList_section mx_MemberList_joined"
|
||||
truncateAt={this.state.truncateAtJoined}
|
||||
createOverflowElement={this.createOverflowTileJoined}
|
||||
getChildren={this.getChildrenJoined}
|
||||
getChildCount={this.getChildCountJoined} />
|
||||
{ invitedHeader }
|
||||
{ invitedSection }
|
||||
</div>
|
||||
</BaseCard>;
|
||||
}
|
||||
|
||||
onInviteButtonClick = () => {
|
||||
onInviteButtonClick = (): void => {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
return;
|
|
@ -30,7 +30,7 @@ import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
|||
import {MsgType} from "matrix-js-sdk/src/@types/event";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import CallMediaHandler from "../../../CallMediaHandler";
|
||||
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
@ -129,8 +129,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
// Do a sanity test to ensure we're about to grab a valid microphone reference. Things might
|
||||
// change between this and recording, but at least we will have tried.
|
||||
try {
|
||||
const devices = await CallMediaHandler.getDevices();
|
||||
if (!devices?.['audioinput']?.length) {
|
||||
const devices = await MediaDeviceHandler.getDevices();
|
||||
if (!devices?.['audioInput']?.length) {
|
||||
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
|
||||
title: _t("No microphone found"),
|
||||
description: <>
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import {_t} from "../../../../../languageHandler";
|
||||
import SdkConfig from "../../../../../SdkConfig";
|
||||
import CallMediaHandler from "../../../../../CallMediaHandler";
|
||||
import MediaDeviceHandler from "../../../../../MediaDeviceHandler";
|
||||
import Field from "../../../elements/Field";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||
|
@ -41,7 +41,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
|
||||
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
|
||||
if (canSeeDeviceLabels) {
|
||||
this._refreshMediaDevices();
|
||||
}
|
||||
|
@ -49,10 +49,10 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
|
||||
_refreshMediaDevices = async (stream) => {
|
||||
this.setState({
|
||||
mediaDevices: await CallMediaHandler.getDevices(),
|
||||
activeAudioOutput: CallMediaHandler.getAudioOutput(),
|
||||
activeAudioInput: CallMediaHandler.getAudioInput(),
|
||||
activeVideoInput: CallMediaHandler.getVideoInput(),
|
||||
mediaDevices: await MediaDeviceHandler.getDevices(),
|
||||
activeAudioOutput: MediaDeviceHandler.getAudioOutput(),
|
||||
activeAudioInput: MediaDeviceHandler.getAudioInput(),
|
||||
activeVideoInput: MediaDeviceHandler.getVideoInput(),
|
||||
});
|
||||
if (stream) {
|
||||
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
||||
|
@ -100,21 +100,21 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
};
|
||||
|
||||
_setAudioOutput = (e) => {
|
||||
CallMediaHandler.setAudioOutput(e.target.value);
|
||||
MediaDeviceHandler.instance.setAudioOutput(e.target.value);
|
||||
this.setState({
|
||||
activeAudioOutput: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_setAudioInput = (e) => {
|
||||
CallMediaHandler.setAudioInput(e.target.value);
|
||||
MediaDeviceHandler.instance.setAudioInput(e.target.value);
|
||||
this.setState({
|
||||
activeAudioInput: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
_setVideoInput = (e) => {
|
||||
CallMediaHandler.setVideoInput(e.target.value);
|
||||
MediaDeviceHandler.instance.setVideoInput(e.target.value);
|
||||
this.setState({
|
||||
activeVideoInput: e.target.value,
|
||||
});
|
||||
|
@ -171,7 +171,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
|
||||
const audioOutputs = this.state.mediaDevices.audioOutput.slice(0);
|
||||
if (audioOutputs.length > 0) {
|
||||
const defaultDevice = getDefaultDevice(audioOutputs);
|
||||
speakerDropdown = (
|
||||
|
@ -183,7 +183,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
|
||||
const audioInputs = this.state.mediaDevices.audioInput.slice(0);
|
||||
if (audioInputs.length > 0) {
|
||||
const defaultDevice = getDefaultDevice(audioInputs);
|
||||
microphoneDropdown = (
|
||||
|
@ -195,7 +195,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const videoInputs = this.state.mediaDevices.videoinput.slice(0);
|
||||
const videoInputs = this.state.mediaDevices.videoInput.slice(0);
|
||||
if (videoInputs.length > 0) {
|
||||
const defaultDevice = getDefaultDevice(videoInputs);
|
||||
webcamDropdown = (
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||
import { logger } from 'matrix-js-sdk/src/logger';
|
||||
import CallMediaHandler from "../../../CallMediaHandler";
|
||||
import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
|
||||
|
||||
interface IProps {
|
||||
feed: CallFeed,
|
||||
|
@ -27,19 +27,25 @@ export default class AudioFeed extends React.Component<IProps> {
|
|||
private element = createRef<HTMLAudioElement>();
|
||||
|
||||
componentDidMount() {
|
||||
MediaDeviceHandler.instance.addListener(
|
||||
MediaDeviceHandlerEvent.AudioOutputChanged,
|
||||
this.onAudioOutputChanged,
|
||||
);
|
||||
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.playMedia();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
MediaDeviceHandler.instance.removeListener(
|
||||
MediaDeviceHandlerEvent.AudioOutputChanged,
|
||||
this.onAudioOutputChanged,
|
||||
);
|
||||
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||
this.stopMedia();
|
||||
}
|
||||
|
||||
private playMedia() {
|
||||
private onAudioOutputChanged = (audioOutput: string) => {
|
||||
const element = this.element.current;
|
||||
const audioOutput = CallMediaHandler.getAudioOutput();
|
||||
|
||||
if (audioOutput) {
|
||||
try {
|
||||
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
|
||||
|
@ -52,7 +58,11 @@ export default class AudioFeed extends React.Component<IProps> {
|
|||
logger.warn("Couldn't set requested audio output device: using default", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private playMedia() {
|
||||
const element = this.element.current;
|
||||
this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
|
||||
element.muted = false;
|
||||
element.srcObject = this.props.feed.stream;
|
||||
element.autoplay = true;
|
||||
|
|
|
@ -397,7 +397,8 @@
|
|||
"Failed to invite": "Failed to invite",
|
||||
"Operation failed": "Operation failed",
|
||||
"Failed to invite users to the room:": "Failed to invite users to the room:",
|
||||
"Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
|
||||
"We sent the others, but the below people couldn't be invited to <RoomName/>": "We sent the others, but the below people couldn't be invited to <RoomName/>",
|
||||
"Some invites couldn't be sent": "Some invites couldn't be sent",
|
||||
"You need to be logged in.": "You need to be logged in.",
|
||||
"You need to be able to invite users to do that.": "You need to be able to invite users to do that.",
|
||||
"Unable to create widget.": "Unable to create widget.",
|
||||
|
@ -2287,7 +2288,6 @@
|
|||
"Confirm to continue": "Confirm to continue",
|
||||
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
|
||||
"Invite by email": "Invite by email",
|
||||
"Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s",
|
||||
"We couldn't create your DM.": "We couldn't create your DM.",
|
||||
"Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.",
|
||||
"We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.",
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React, { ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
|
||||
import { Action, DiffDOM, IDiff } from "diff-dom";
|
||||
import { DiffDOM, IDiff } from "diff-dom";
|
||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
|
||||
|
@ -149,7 +149,7 @@ function stringAsTextNode(string: string): Text {
|
|||
function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void {
|
||||
const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route);
|
||||
switch (diff.action) {
|
||||
case Action.ReplaceElement: {
|
||||
case "replaceElement": {
|
||||
const container = document.createElement("span");
|
||||
const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue));
|
||||
const insNode = wrapInsertion(diffTreeToDOM(diff.newValue));
|
||||
|
@ -158,17 +158,17 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
|||
refNode.parentNode.replaceChild(container, refNode);
|
||||
break;
|
||||
}
|
||||
case Action.RemoveTextElement: {
|
||||
case "removeTextElement": {
|
||||
const delNode = wrapDeletion(stringAsTextNode(diff.value));
|
||||
refNode.parentNode.replaceChild(delNode, refNode);
|
||||
break;
|
||||
}
|
||||
case Action.RemoveElement: {
|
||||
case "removeElement": {
|
||||
const delNode = wrapDeletion(diffTreeToDOM(diff.element));
|
||||
refNode.parentNode.replaceChild(delNode, refNode);
|
||||
break;
|
||||
}
|
||||
case Action.ModifyTextElement: {
|
||||
case "modifyTextElement": {
|
||||
const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue);
|
||||
diffMathPatch.diff_cleanupSemantic(textDiffs);
|
||||
const container = document.createElement("span");
|
||||
|
@ -184,12 +184,12 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
|||
refNode.parentNode.replaceChild(container, refNode);
|
||||
break;
|
||||
}
|
||||
case Action.AddElement: {
|
||||
case "addElement": {
|
||||
const insNode = wrapInsertion(diffTreeToDOM(diff.element));
|
||||
insertBefore(refParentNode, refNode, insNode);
|
||||
break;
|
||||
}
|
||||
case Action.AddTextElement: {
|
||||
case "addTextElement": {
|
||||
// XXX: sometimes diffDOM says insert a newline when there shouldn't be one
|
||||
// but we must insert the node anyway so that we don't break the route child IDs.
|
||||
// See https://github.com/fiduswriter/diffDOM/issues/100
|
||||
|
@ -199,9 +199,9 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
|||
}
|
||||
// e.g. when changing a the href of a link,
|
||||
// show the link with old href as removed and with the new href as added
|
||||
case Action.RemoveAttribute:
|
||||
case Action.AddAttribute:
|
||||
case Action.ModifyAttribute: {
|
||||
case "removeAttribute":
|
||||
case "addAttribute":
|
||||
case "modifyAttribute": {
|
||||
const delNode = wrapDeletion(refNode.cloneNode(true));
|
||||
const updatedNode = refNode.cloneNode(true) as HTMLElement;
|
||||
if (diff.action === "addAttribute" || diff.action === "modifyAttribute") {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,23 +14,51 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import {getAddressType} from '../UserAddress';
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||
import { AddressType, getAddressType } from '../UserAddress';
|
||||
import GroupStore from '../stores/GroupStore';
|
||||
import {_t} from "../languageHandler";
|
||||
import * as sdk from "../index";
|
||||
import { _t } from "../languageHandler";
|
||||
import Modal from "../Modal";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import {defer} from "./promise";
|
||||
import { defer, IDeferred } from "./promise";
|
||||
import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog";
|
||||
|
||||
export enum InviteState {
|
||||
Invited = "invited",
|
||||
Error = "error",
|
||||
}
|
||||
|
||||
interface IError {
|
||||
errorText: string;
|
||||
errcode: string;
|
||||
}
|
||||
|
||||
const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
|
||||
|
||||
export type CompletionStates = Record<string, InviteState>;
|
||||
|
||||
/**
|
||||
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
||||
*/
|
||||
export default class MultiInviter {
|
||||
private readonly roomId?: string;
|
||||
private readonly groupId?: string;
|
||||
|
||||
private canceled = false;
|
||||
private addresses: string[] = [];
|
||||
private busy = false;
|
||||
private _fatal = false;
|
||||
private completionStates: CompletionStates = {}; // State of each address (invited or error)
|
||||
private errors: Record<string, IError> = {}; // { address: {errorText, errcode} }
|
||||
private deferred: IDeferred<CompletionStates> = null;
|
||||
private reason: string = null;
|
||||
|
||||
/**
|
||||
* @param {string} targetId The ID of the room or group to invite to
|
||||
*/
|
||||
constructor(targetId) {
|
||||
constructor(targetId: string) {
|
||||
if (targetId[0] === '+') {
|
||||
this.roomId = null;
|
||||
this.groupId = targetId;
|
||||
|
@ -39,41 +66,38 @@ export default class MultiInviter {
|
|||
this.roomId = targetId;
|
||||
this.groupId = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.canceled = false;
|
||||
this.addrs = [];
|
||||
this.busy = false;
|
||||
this.completionStates = {}; // State of each address (invited or error)
|
||||
this.errors = {}; // { address: {errorText, errcode} }
|
||||
this.deferred = null;
|
||||
public get fatal() {
|
||||
return this._fatal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite users to this room. This may only be called once per
|
||||
* instance of the class.
|
||||
*
|
||||
* @param {array} addrs Array of addresses to invite
|
||||
* @param {array} addresses Array of addresses to invite
|
||||
* @param {string} reason Reason for inviting (optional)
|
||||
* @returns {Promise} Resolved when all invitations in the queue are complete
|
||||
*/
|
||||
invite(addrs, reason) {
|
||||
if (this.addrs.length > 0) {
|
||||
public invite(addresses, reason?: string): Promise<CompletionStates> {
|
||||
if (this.addresses.length > 0) {
|
||||
throw new Error("Already inviting/invited");
|
||||
}
|
||||
this.addrs.push(...addrs);
|
||||
this.addresses.push(...addresses);
|
||||
this.reason = reason;
|
||||
|
||||
for (const addr of this.addrs) {
|
||||
for (const addr of this.addresses) {
|
||||
if (getAddressType(addr) === null) {
|
||||
this.completionStates[addr] = 'error';
|
||||
this.completionStates[addr] = InviteState.Error;
|
||||
this.errors[addr] = {
|
||||
errcode: 'M_INVALID',
|
||||
errorText: _t('Unrecognised address'),
|
||||
};
|
||||
}
|
||||
}
|
||||
this.deferred = defer();
|
||||
this._inviteMore(0);
|
||||
this.deferred = defer<CompletionStates>();
|
||||
this.inviteMore(0);
|
||||
|
||||
return this.deferred.promise;
|
||||
}
|
||||
|
@ -81,33 +105,36 @@ export default class MultiInviter {
|
|||
/**
|
||||
* Stops inviting. Causes promises returned by invite() to be rejected.
|
||||
*/
|
||||
cancel() {
|
||||
public cancel(): void {
|
||||
if (!this.busy) return;
|
||||
|
||||
this._canceled = true;
|
||||
this.canceled = true;
|
||||
this.deferred.reject(new Error('canceled'));
|
||||
}
|
||||
|
||||
getCompletionState(addr) {
|
||||
public getCompletionState(addr: string): InviteState {
|
||||
return this.completionStates[addr];
|
||||
}
|
||||
|
||||
getErrorText(addr) {
|
||||
public getErrorText(addr: string): string {
|
||||
return this.errors[addr] ? this.errors[addr].errorText : null;
|
||||
}
|
||||
|
||||
async _inviteToRoom(roomId, addr, ignoreProfile) {
|
||||
private async inviteToRoom(roomId: string, addr: string, ignoreProfile = false): Promise<{}> {
|
||||
const addrType = getAddressType(addr);
|
||||
|
||||
if (addrType === 'email') {
|
||||
if (addrType === AddressType.Email) {
|
||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
} else if (addrType === AddressType.MatrixUserId) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) throw new Error("Room not found");
|
||||
|
||||
const member = room.getMember(addr);
|
||||
if (member && ['join', 'invite'].includes(member.membership)) {
|
||||
throw {errcode: "RIOT.ALREADY_IN_ROOM", error: "Member already invited"};
|
||||
throw new new MatrixError({
|
||||
errcode: "RIOT.ALREADY_IN_ROOM",
|
||||
error: "Member already invited",
|
||||
});
|
||||
}
|
||||
|
||||
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
||||
|
@ -124,28 +151,28 @@ export default class MultiInviter {
|
|||
}
|
||||
}
|
||||
|
||||
_doInvite(address, ignoreProfile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
private doInvite(address: string, ignoreProfile = false): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
console.log(`Inviting ${address}`);
|
||||
|
||||
let doInvite;
|
||||
if (this.groupId !== null) {
|
||||
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
|
||||
} else {
|
||||
doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile);
|
||||
doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile);
|
||||
}
|
||||
|
||||
doInvite.then(() => {
|
||||
if (this._canceled) {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.completionStates[address] = 'invited';
|
||||
this.completionStates[address] = InviteState.Invited;
|
||||
delete this.errors[address];
|
||||
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
if (this._canceled) {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -161,7 +188,7 @@ export default class MultiInviter {
|
|||
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
||||
// we're being throttled so wait a bit & try again
|
||||
setTimeout(() => {
|
||||
this._doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||
}, 5000);
|
||||
return;
|
||||
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) {
|
||||
|
@ -171,7 +198,7 @@ export default class MultiInviter {
|
|||
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) {
|
||||
// Invite without the profile check
|
||||
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
|
||||
this._doInvite(address, true).then(resolve, reject);
|
||||
this.doInvite(address, true).then(resolve, reject);
|
||||
} else if (err.errcode === "M_BAD_STATE") {
|
||||
errorText = _t("The user must be unbanned before they can be invited.");
|
||||
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
|
||||
|
@ -180,14 +207,14 @@ export default class MultiInviter {
|
|||
errorText = _t('Unknown server error');
|
||||
}
|
||||
|
||||
this.completionStates[address] = 'error';
|
||||
this.errors[address] = {errorText, errcode: err.errcode};
|
||||
this.completionStates[address] = InviteState.Error;
|
||||
this.errors[address] = { errorText, errcode: err.errcode };
|
||||
|
||||
this.busy = !fatal;
|
||||
this.fatal = fatal;
|
||||
this._fatal = fatal;
|
||||
|
||||
if (fatal) {
|
||||
reject();
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
@ -195,22 +222,22 @@ export default class MultiInviter {
|
|||
});
|
||||
}
|
||||
|
||||
_inviteMore(nextIndex, ignoreProfile) {
|
||||
if (this._canceled) {
|
||||
private inviteMore(nextIndex: number, ignoreProfile = false): void {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextIndex === this.addrs.length) {
|
||||
if (nextIndex === this.addresses.length) {
|
||||
this.busy = false;
|
||||
if (Object.keys(this.errors).length > 0 && !this.groupId) {
|
||||
// There were problems inviting some people - see if we can invite them
|
||||
// without caring if they exist or not.
|
||||
const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
|
||||
const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode));
|
||||
const unknownProfileUsers = Object.keys(this.errors)
|
||||
.filter(a => UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode));
|
||||
|
||||
if (unknownProfileUsers.length > 0) {
|
||||
const inviteUnknowns = () => {
|
||||
const promises = unknownProfileUsers.map(u => this._doInvite(u, true));
|
||||
const promises = unknownProfileUsers.map(u => this.doInvite(u, true));
|
||||
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
||||
};
|
||||
|
||||
|
@ -219,15 +246,17 @@ export default class MultiInviter {
|
|||
return;
|
||||
}
|
||||
|
||||
const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
|
||||
console.log("Showing failed to invite dialog...");
|
||||
Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, {
|
||||
unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}),
|
||||
unknownProfileUsers: unknownProfileUsers.map(u => ({
|
||||
userId: u,
|
||||
errorText: this.errors[u].errorText,
|
||||
})),
|
||||
onInviteAnyways: () => inviteUnknowns(),
|
||||
onGiveUp: () => {
|
||||
// Fake all the completion states because we already warned the user
|
||||
for (const addr of unknownProfileUsers) {
|
||||
this.completionStates[addr] = 'invited';
|
||||
this.completionStates[addr] = InviteState.Invited;
|
||||
}
|
||||
this.deferred.resolve(this.completionStates);
|
||||
},
|
||||
|
@ -239,25 +268,25 @@ export default class MultiInviter {
|
|||
return;
|
||||
}
|
||||
|
||||
const addr = this.addrs[nextIndex];
|
||||
const addr = this.addresses[nextIndex];
|
||||
|
||||
// don't try to invite it if it's an invalid address
|
||||
// (it will already be marked as an error though,
|
||||
// so no need to do so again)
|
||||
if (getAddressType(addr) === null) {
|
||||
this._inviteMore(nextIndex + 1);
|
||||
this.inviteMore(nextIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// don't re-invite (there's no way in the UI to do this, but
|
||||
// for sanity's sake)
|
||||
if (this.completionStates[addr] === 'invited') {
|
||||
this._inviteMore(nextIndex + 1);
|
||||
if (this.completionStates[addr] === InviteState.Invited) {
|
||||
this.inviteMore(nextIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
this._doInvite(addr, ignoreProfile).then(() => {
|
||||
this._inviteMore(nextIndex + 1, ignoreProfile);
|
||||
this.doInvite(addr, ignoreProfile).then(() => {
|
||||
this.inviteMore(nextIndex + 1, ignoreProfile);
|
||||
}).catch(() => this.deferred.resolve(this.completionStates));
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import * as Recorder from 'opus-recorder';
|
||||
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import CallMediaHandler from "../CallMediaHandler";
|
||||
import MediaDeviceHandler from "../MediaDeviceHandler";
|
||||
import {SimpleObservable} from "matrix-widget-api";
|
||||
import {clamp, percentageOf, percentageWithin} from "../utils/numbers";
|
||||
import EventEmitter from "events";
|
||||
|
@ -97,7 +97,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
audio: {
|
||||
channelCount: CHANNELS,
|
||||
noiseSuppression: true, // browsers ignore constraints they can't honour
|
||||
deviceId: CallMediaHandler.getAudioInput(),
|
||||
deviceId: MediaDeviceHandler.getAudioInput(),
|
||||
},
|
||||
});
|
||||
this.recorderContext = createAudioContext({
|
||||
|
|
|
@ -1,21 +1,36 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactTestUtils from 'react-dom/test-utils';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import * as TestUtils from '../../../test-utils';
|
||||
|
||||
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
|
||||
import sdk from '../../../skinned-sdk';
|
||||
|
||||
import {Room, RoomMember, User} from 'matrix-js-sdk';
|
||||
|
||||
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { compare } from "../../../../src/utils/strings";
|
||||
import MemberList from "../../../../src/components/views/rooms/MemberList";
|
||||
|
||||
function generateRoomId() {
|
||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||
}
|
||||
|
||||
|
||||
describe('MemberList', () => {
|
||||
function createRoom(opts) {
|
||||
const room = new Room(generateRoomId(), null, client.getUserId());
|
||||
|
@ -97,13 +112,19 @@ describe('MemberList', () => {
|
|||
memberListRoom.currentState.members[member.userId] = member;
|
||||
}
|
||||
|
||||
const MemberList = sdk.getComponent('views.rooms.MemberList');
|
||||
const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList);
|
||||
const gatherWrappedRef = (r) => {
|
||||
memberList = r;
|
||||
};
|
||||
root = ReactDOM.render(<WrappedMemberList roomId={memberListRoom.roomId}
|
||||
wrappedRef={gatherWrappedRef} />, parentDiv);
|
||||
root = ReactDOM.render(
|
||||
(
|
||||
<WrappedMemberList
|
||||
roomId={memberListRoom.roomId}
|
||||
wrappedRef={gatherWrappedRef}
|
||||
/>
|
||||
),
|
||||
parentDiv,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
|
@ -213,8 +234,8 @@ describe('MemberList', () => {
|
|||
});
|
||||
|
||||
// Bypass all the event listeners and skip to the good part
|
||||
memberList._showPresence = enablePresence;
|
||||
memberList._updateListNow();
|
||||
memberList.showPresence = enablePresence;
|
||||
memberList.updateListNow();
|
||||
|
||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||
|
@ -225,7 +246,7 @@ describe('MemberList', () => {
|
|||
|
||||
// Bypass all the event listeners and skip to the good part
|
||||
memberList._showPresence = enablePresence;
|
||||
memberList._updateListNow();
|
||||
memberList.updateListNow();
|
||||
|
||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||
|
@ -254,8 +275,8 @@ describe('MemberList', () => {
|
|||
});
|
||||
|
||||
// Bypass all the event listeners and skip to the good part
|
||||
memberList._showPresence = enablePresence;
|
||||
memberList._updateListNow();
|
||||
memberList.showPresence = enablePresence;
|
||||
memberList.updateListNow();
|
||||
|
||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||
|
@ -273,8 +294,8 @@ describe('MemberList', () => {
|
|||
});
|
||||
|
||||
// Bypass all the event listeners and skip to the good part
|
||||
memberList._showPresence = enablePresence;
|
||||
memberList._updateListNow();
|
||||
memberList.showPresence = enablePresence;
|
||||
memberList.updateListNow();
|
||||
|
||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
10
yarn.lock
10
yarn.lock
|
@ -1486,7 +1486,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8"
|
||||
integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ==
|
||||
|
||||
"@types/diff-match-patch@^1.0.5":
|
||||
"@types/diff-match-patch@^1.0.32":
|
||||
version "1.0.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz#d9c3b8c914aa8229485351db4865328337a3d09f"
|
||||
integrity sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A==
|
||||
|
@ -5777,10 +5777,10 @@ matrix-react-test-utils@^0.2.3:
|
|||
"@babel/traverse" "^7.13.17"
|
||||
walk "^2.3.14"
|
||||
|
||||
matrix-widget-api@^0.1.0-beta.14:
|
||||
version "0.1.0-beta.14"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70"
|
||||
integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ==
|
||||
matrix-widget-api@^0.1.0-beta.15:
|
||||
version "0.1.0-beta.15"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745"
|
||||
integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg==
|
||||
dependencies:
|
||||
"@types/events" "^3.0.0"
|
||||
events "^3.2.0"
|
||||
|
|
Loading…
Reference in a new issue