diff --git a/package.json b/package.json
index 756604269d..bae7b1e4cf 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,7 @@
"linkifyjs": "^2.1.9",
"lodash": "^4.17.20",
"matrix-js-sdk": "12.0.0",
- "matrix-widget-api": "^0.1.0-beta.14",
+ "matrix-widget-api": "^0.1.0-beta.15",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
@@ -89,7 +89,7 @@
"qrcode": "^1.4.4",
"re-resizable": "^6.9.0",
"react": "^17.0.2",
- "react-beautiful-dnd": "^4.0.1",
+ "react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.2",
"react-focus-lock": "^2.5.0",
"react-transition-group": "^4.4.1",
@@ -123,7 +123,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",
@@ -133,8 +133,9 @@
"@types/pako": "^1.0.1",
"@types/parse5": "^6.0.0",
"@types/qrcode": "^1.3.5",
- "@types/react": "^16.9",
- "@types/react-dom": "^16.9.10",
+ "@types/react": "^17.0.2",
+ "@types/react-beautiful-dnd": "^13.0.0",
+ "@types/react-dom": "^17.0.2",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^2.3.1",
"@types/zxcvbn": "^4.4.0",
@@ -168,13 +169,10 @@
"typescript": "^4.1.3",
"walk": "^2.3.14"
},
- "resolutions": {
- "**/@types/react": "^16.14"
- },
"jest": {
"testEnvironment": "./__test-utils__/environment.js",
"testMatch": [
- "/test/**/*-test.[jt]s"
+ "/test/**/*-test.[jt]s?(x)"
],
"setupFiles": [
"jest-canvas-mock"
diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss
index c433ccf275..e64057d16c 100644
--- a/res/css/structures/_SpacePanel.scss
+++ b/res/css/structures/_SpacePanel.scss
@@ -31,7 +31,6 @@ $activeBorderColor: $secondary-fg-color;
// Create another flexbox so the Panel fills the container
display: flex;
flex-direction: column;
- overflow-y: auto;
.mx_SpacePanel_spaceTreeWrapper {
flex: 1;
@@ -69,6 +68,12 @@ $activeBorderColor: $secondary-fg-color;
cursor: pointer;
}
+ .mx_SpaceItem_dragging {
+ .mx_SpaceButton_toggleCollapse {
+ visibility: hidden;
+ }
+ }
+
.mx_SpaceTreeLevel {
display: flex;
flex-direction: column;
diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss
index 14e4c01389..35d6087a1b 100644
--- a/res/css/structures/_ToastContainer.scss
+++ b/res/css/structures/_ToastContainer.scss
@@ -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 {
diff --git a/res/css/views/context_menus/_TagTileContextMenu.scss b/res/css/views/context_menus/_TagTileContextMenu.scss
index 8929c8906e..d707f4ce7c 100644
--- a/res/css/views/context_menus/_TagTileContextMenu.scss
+++ b/res/css/views/context_menus/_TagTileContextMenu.scss
@@ -38,6 +38,15 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/view-community.svg');
}
+.mx_TagTileContextMenu_moveUp::before {
+ transform: rotate(180deg);
+ mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
+}
+
+.mx_TagTileContextMenu_moveDown::before {
+ mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
+}
+
.mx_TagTileContextMenu_hideCommunity::before {
mask-image: url('$(res)/img/element-icons/hide.svg');
}
diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss
index 2e48b5d8e9..c01b43c1c4 100644
--- a/res/css/views/dialogs/_InviteDialog.scss
+++ b/res/css/views/dialogs/_InviteDialog.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/res/css/views/dialogs/_SettingsDialog.scss b/res/css/views/dialogs/_SettingsDialog.scss
index 6c4ed35c5a..b3b6802c3d 100644
--- a/res/css/views/dialogs/_SettingsDialog.scss
+++ b/res/css/views/dialogs/_SettingsDialog.scss
@@ -15,7 +15,7 @@ limitations under the License.
*/
// Not actually a component but things shared by settings components
-.mx_UserSettingsDialog, .mx_RoomSettingsDialog {
+.mx_UserSettingsDialog, .mx_RoomSettingsDialog, .mx_SpaceSettingsDialog {
width: 90vw;
max-width: 1000px;
// set the height too since tabbed view scrolls itself.
diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss
index 6e5fd9c8c8..fa074fdbe8 100644
--- a/res/css/views/dialogs/_SpaceSettingsDialog.scss
+++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss
@@ -15,7 +15,6 @@ limitations under the License.
*/
.mx_SpaceSettingsDialog {
- width: 480px;
color: $primary-fg-color;
.mx_SpaceSettings_errorText {
@@ -32,8 +31,44 @@ limitations under the License.
margin-left: 16px;
}
- .mx_AccessibleButton_kind_danger {
- margin-top: 28px;
+ .mx_SettingsTab_section {
+ .mx_SettingsTab_section_caption {
+ margin-top: 12px;
+ margin-bottom: 20px;
+ }
+
+ & + .mx_SettingsTab_subheading {
+ border-top: 1px solid $message-body-panel-bg-color;
+ margin-top: 0;
+ padding-top: 24px;
+ }
+
+ .mx_RadioButton {
+ margin-top: 8px;
+ margin-bottom: 4px;
+
+ .mx_RadioButton_content {
+ font-weight: $font-semi-bold;
+ line-height: $font-18px;
+ color: $primary-fg-color;
+ }
+
+ & + span {
+ font-size: $font-15px;
+ line-height: $font-18px;
+ color: $secondary-fg-color;
+ margin-left: 26px;
+ }
+ }
+
+ .mx_SettingsTab_showAdvanced {
+ margin: 16px 0;
+ padding: 0;
+ }
+
+ .mx_SettingsFlag {
+ margin-top: 24px;
+ }
}
.mx_SpaceSettingsDialog_buttons {
@@ -52,4 +87,14 @@ limitations under the License.
.mx_AccessibleButton_hasKind {
padding: 8px 22px;
}
+
+ .mx_TabbedView_tabLabel {
+ .mx_SpaceSettingsDialog_generalIcon::before {
+ mask-image: url('$(res)/img/element-icons/settings.svg');
+ }
+
+ .mx_SpaceSettingsDialog_visibilityIcon::before {
+ mask-image: url('$(res)/img/element-icons/eye.svg');
+ }
+ }
}
diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss
index 4faa4b594f..bcc40f1181 100644
--- a/res/css/views/messages/_common_CryptoEvent.scss
+++ b/res/css/views/messages/_common_CryptoEvent.scss
@@ -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 {
diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss
index a3473dedec..68ad44cf6a 100644
--- a/res/css/views/rooms/_E2EIcon.scss
+++ b/res/css/views/rooms/_E2EIcon.scss
@@ -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%;
}
}
diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 3af266caee..27a83e58f8 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -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%;
}
}
diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss
index 32454b9530..68e8723f11 100644
--- a/res/css/views/spaces/_SpaceBasicSettings.scss
+++ b/res/css/views/spaces/_SpaceBasicSettings.scss
@@ -16,7 +16,7 @@ limitations under the License.
.mx_SpaceBasicSettings {
.mx_Field {
- margin: 32px 0;
+ margin: 24px 0;
}
.mx_SpaceBasicSettings_avatarContainer {
diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss
index 0c7bff0ce8..483b131bfe 100644
--- a/res/css/views/voip/_DialPad.scss
+++ b/res/css/views/voip/_DialPad.scss
@@ -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;
diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss
index 520f51cf93..31327113cf 100644
--- a/res/css/views/voip/_DialPadContextMenu.scss
+++ b/res/css/views/voip/_DialPadContextMenu.scss
@@ -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 {
diff --git a/res/img/element-icons/eye.svg b/res/img/element-icons/eye.svg
new file mode 100644
index 0000000000..0460a6201d
--- /dev/null
+++ b/res/img/element-icons/eye.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 2d0e3d2a8b..8b5fde3bd1 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -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;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index a852ad94e9..eb6dc40599 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -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;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 84666bc662..a6b180bab4 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -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;
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index c889f43d0b..d8dab9c9c4 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -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;
diff --git a/src/@types/diff-dom.ts b/src/@types/diff-dom.ts
index 884ee6126d..38ff6432cf 100644
--- a/src/@types/diff-dom.ts
+++ b/src/@types/diff-dom.ts
@@ -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[];
diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js
deleted file mode 100644
index 634f0bb336..0000000000
--- a/src/CallMediaHandler.js
+++ /dev/null
@@ -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");
- },
-};
diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts
index 960d844e9e..07c0c546fe 100644
--- a/src/DecryptionFailureTracker.ts
+++ b/src/DecryptionFailureTracker.ts
@@ -25,7 +25,7 @@ export class DecryptionFailure {
}
}
-type Fn = (count: number, trackedErrCode: string) => void;
+type TrackingFn = (count: number, trackedErrCode: string) => void;
type ErrCodeMapFn = (errcode: string) => string;
export class DecryptionFailureTracker {
@@ -73,7 +73,7 @@ export class DecryptionFailureTracker {
* @param {function?} errorCodeMapFn The function used to map error codes to the
* trackedErrorCode. If not provided, the `.code` of errors will be used.
*/
- constructor(private readonly fn: Fn, private readonly errorCodeMapFn?: ErrCodeMapFn) {
+ constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) {
if (!fn || typeof fn !== 'function') {
throw new Error('DecryptionFailureTracker requires tracking function');
}
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 5803029030..983538d65b 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -505,7 +505,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
* @returns {string} Linkified string
*/
-export function linkifyString(str: string, options = linkifyMatrix.options) {
+export function linkifyString(str: string, options = linkifyMatrix.options): string {
return _linkifyString(str, options);
}
@@ -516,7 +516,7 @@ export function linkifyString(str: string, options = linkifyMatrix.options) {
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options
* @returns {object}
*/
-export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options) {
+export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options): HTMLElement {
return _linkifyElement(element, options);
}
diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts
new file mode 100644
index 0000000000..49ef123def
--- /dev/null
+++ b/src/MediaDeviceHandler.ts
@@ -0,0 +1,120 @@
+/*
+Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
+Copyright 2021 Šimon Brandner
+
+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;
+ audioInput: Array;
+ videoInput: Array;
+}
+
+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 {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ return devices.some(d => Boolean(d.label));
+ }
+
+ public static async getDevices(): Promise {
+ // 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");
+ }
+}
diff --git a/src/RoomInvite.js b/src/RoomInvite.tsx
similarity index 54%
rename from src/RoomInvite.js
rename to src/RoomInvite.tsx
index aa758ecbdc..c86f832b90 100644
--- a/src/RoomInvite.js
+++ b/src/RoomInvite.tsx
@@ -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 {
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 {
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,
+): 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( )` anymore, so this is our solution
- const description = {errorList.map(e =>
{e}
)}
;
+ const description =
+
{ _t("We sent the others, but the below people couldn't be invited to ", {}, {
+ RoomName: () => { room.name } ,
+ }) }
+
+ { 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
+
+
+ { name }
+ { user.userId }
+
+
+ { inviter.getErrorText(addr) }
+
+
;
+ }) }
+
+
;
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;
diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts
index 09c8d30614..1ba0d6439b 100644
--- a/src/SecurityManager.ts
+++ b/src/SecurityManager.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
+import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import Modal from './Modal';
import * as sdk from './index';
@@ -28,6 +28,7 @@ import AccessSecretStorageDialog from './components/views/dialogs/security/Acces
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
import SettingsStore from "./settings/SettingsStore";
import SecurityCustomisations from "./customisations/Security";
+import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
// This stores the secret storage private keys in memory for the JS SDK. This is
// only meant to act as a cache to avoid prompting the user multiple times
@@ -244,7 +245,7 @@ async function onSecretRequested(
deviceId: string,
requestId: string,
name: string,
- deviceTrust: IDeviceTrustLevel,
+ deviceTrust: DeviceTrustLevel,
): Promise {
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
const client = MatrixClientPeg.get();
diff --git a/src/TextForEvent.ts b/src/TextForEvent.ts
index 649c53664e..ebf1645303 100644
--- a/src/TextForEvent.ts
+++ b/src/TextForEvent.ts
@@ -31,76 +31,89 @@ function textForMemberEvent(ev): () => string | null {
const targetName = ev.target ? ev.target.name : ev.getStateKey();
const prevContent = ev.getPrevContent();
const content = ev.getContent();
+ const reason = content.reason;
- const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
switch (content.membership) {
case 'invite': {
const threePidContent = content.third_party_invite;
if (threePidContent) {
if (threePidContent.display_name) {
- return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
+ return () => _t('%(targetName)s accepted the invitation for %(displayName)s', {
targetName,
displayName: threePidContent.display_name,
});
} else {
- return () => _t('%(targetName)s accepted an invitation.', {targetName});
+ return () => _t('%(targetName)s accepted an invitation', { targetName });
}
} else {
- return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
+ return () => _t('%(senderName)s invited %(targetName)s', { senderName, targetName });
}
}
case 'ban':
- return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
+ return () => reason
+ ? _t('%(senderName)s banned %(targetName)s: %(reason)s', { senderName, targetName, reason })
+ : _t('%(senderName)s banned %(targetName)s', { senderName, targetName });
case 'join':
if (prevContent && prevContent.membership === 'join') {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
- return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
+ return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s', {
oldDisplayName: prevContent.displayname,
displayName: content.displayname,
});
} else if (!prevContent.displayname && content.displayname) {
- return () => _t('%(senderName)s set their display name to %(displayName)s.', {
+ return () => _t('%(senderName)s set their display name to %(displayName)s', {
senderName: ev.getSender(),
displayName: content.displayname,
});
} else if (prevContent.displayname && !content.displayname) {
- return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
+ return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s)', {
senderName,
oldDisplayName: prevContent.displayname,
});
} else if (prevContent.avatar_url && !content.avatar_url) {
- return () => _t('%(senderName)s removed their profile picture.', {senderName});
+ return () => _t('%(senderName)s removed their profile picture', { senderName });
} else if (prevContent.avatar_url && content.avatar_url &&
prevContent.avatar_url !== content.avatar_url) {
- return () => _t('%(senderName)s changed their profile picture.', {senderName});
+ return () => _t('%(senderName)s changed their profile picture', { senderName });
} else if (!prevContent.avatar_url && content.avatar_url) {
- return () => _t('%(senderName)s set a profile picture.', {senderName});
+ return () => _t('%(senderName)s set a profile picture', { senderName });
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
- // This is a null rejoin, it will only be visible if the Labs option is enabled
- return () => _t("%(senderName)s made no change.", {senderName});
+ // This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
+ return () => _t("%(senderName)s made no change", { senderName });
} else {
return null;
}
} else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
- return () => _t('%(targetName)s joined the room.', {targetName});
+ return () => _t('%(targetName)s joined the room', { targetName });
}
case 'leave':
if (ev.getSender() === ev.getStateKey()) {
if (prevContent.membership === "invite") {
- return () => _t('%(targetName)s rejected the invitation.', {targetName});
+ return () => _t('%(targetName)s rejected the invitation', { targetName });
} else {
- return () => _t('%(targetName)s left the room.', {targetName});
+ return () => reason
+ ? _t('%(targetName)s left the room: %(reason)s', { targetName, reason })
+ : _t('%(targetName)s left the room', { targetName });
}
} else if (prevContent.membership === "ban") {
- return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
+ return () => _t('%(senderName)s unbanned %(targetName)s', { senderName, targetName });
} else if (prevContent.membership === "invite") {
- return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
- senderName,
- targetName,
- }) + ' ' + getReason();
+ return () => reason
+ ? _t('%(senderName)s withdrew %(targetName)s\'s invitation: %(reason)s', {
+ senderName,
+ targetName,
+ reason,
+ })
+ : _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName })
} else if (prevContent.membership === "join") {
- return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
+ return () => reason
+ ? _t('%(senderName)s kicked %(targetName)s: %(reason)s', {
+ senderName,
+ targetName,
+ reason,
+ })
+ : _t('%(senderName)s kicked %(targetName)s', { senderName, targetName });
} else {
return null;
}
diff --git a/src/UserAddress.js b/src/UserAddress.ts
similarity index 69%
rename from src/UserAddress.js
rename to src/UserAddress.ts
index e7501a0d91..a2c546deb7 100644
--- a/src/UserAddress.js
+++ b/src/UserAddress.ts
@@ -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;
}
diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts
index ea8eddbb8d..7f3f5d2c01 100644
--- a/src/autocomplete/Autocompleter.ts
+++ b/src/autocomplete/Autocompleter.ts
@@ -55,13 +55,14 @@ const PROVIDERS = [
EmojiProvider,
NotifProvider,
CommandProvider,
- CommunityProvider,
DuckDuckGoProvider,
];
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
if (SettingsStore.getValue("feature_spaces")) {
PROVIDERS.push(SpaceProvider);
+} else {
+ PROVIDERS.push(CommunityProvider);
}
// Providers will get rejected if they take longer than this.
diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx
index 3b7fee3a08..e8a9872b48 100644
--- a/src/components/structures/AutoHideScrollbar.tsx
+++ b/src/components/structures/AutoHideScrollbar.tsx
@@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { WheelEvent } from "react";
+import React, { HTMLAttributes, WheelEvent } from "react";
-interface IProps {
+interface IProps extends Omit, "onScroll"> {
className?: string;
onScroll?: (event: Event) => void;
onWheel?: (event: WheelEvent) => void;
@@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component {
}
public render() {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
+
return (
- { this.props.children }
+ { children }
);
}
}
diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js
index 2ff91e4976..f1c28d588a 100644
--- a/src/components/structures/GroupFilterPanel.js
+++ b/src/components/structures/GroupFilterPanel.js
@@ -24,7 +24,6 @@ import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import { _t } from '../../languageHandler';
-import { Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
@@ -83,7 +82,7 @@ class GroupFilterPanel extends React.Component {
}
};
- onMouseDown = e => {
+ onClick = e => {
// only dispatch if its not a no-op
if (this.state.selectedTags.length > 0) {
dis.dispatch({action: 'deselect_tags'});
@@ -151,28 +150,15 @@ class GroupFilterPanel extends React.Component {
return
-
- { (provided, snapshot) => (
-
- { this.renderGlobalIcon() }
- { tags }
-
- {createButton}
-
- { provided.placeholder }
-
- ) }
-
+
+ { this.renderGlobalIcon() }
+ { tags }
+
+ { createButton }
+
+
;
}
diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js
index 51a3b287f0..25dcaeed39 100644
--- a/src/components/structures/IndicatorScrollbar.js
+++ b/src/components/structures/IndicatorScrollbar.js
@@ -185,21 +185,24 @@ export default class IndicatorScrollbar extends React.Component {
};
render() {
+ // eslint-disable-next-line no-unused-vars
+ const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
+
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
- const leftOverflowIndicator = this.props.trackHorizontalOverflow
+ const leftOverflowIndicator = trackHorizontalOverflow
?
: null;
- const rightOverflowIndicator = this.props.trackHorizontalOverflow
+ const rightOverflowIndicator = trackHorizontalOverflow
?
: null;
return (
{ leftOverflowIndicator }
- { this.props.children }
+ { children }
{ rightOverflowIndicator }
);
}
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 3091278e3a..5ad67232a4 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -19,19 +19,16 @@ limitations under the License.
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client';
-import { DragDropContext } from 'react-beautiful-dnd';
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';
import { IMatrixClientCreds } from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
-import TagOrderActions from '../../actions/TagOrderActions';
-import RoomListActions from '../../actions/RoomListActions';
import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, CollapseDistributor} from '../../resizer';
import MatrixClientContext from "../../contexts/MatrixClientContext";
@@ -170,7 +167,7 @@ class LoggedInView extends React.Component {
// stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient;
- CallMediaHandler.loadDevices();
+ MediaDeviceHandler.loadDevices();
fixupColorFonts();
@@ -569,50 +566,6 @@ class LoggedInView extends React.Component {
}
};
- _onDragEnd = (result) => {
- // Dragged to an invalid destination, not onto a droppable
- if (!result.destination) {
- return;
- }
-
- const dest = result.destination.droppableId;
-
- if (dest === 'tag-panel-droppable') {
- // Could be "GroupTile +groupId:domain"
- const draggableId = result.draggableId.split(' ').pop();
-
- // Dispatch synchronously so that the GroupFilterPanel receives an
- // optimistic update from GroupFilterOrderStore before the previous
- // state is shown.
- dis.dispatch(TagOrderActions.moveTag(
- this._matrixClient,
- draggableId,
- result.destination.index,
- ), true);
- } else if (dest.startsWith('room-sub-list-droppable_')) {
- this._onRoomTileEndDrag(result);
- }
- };
-
- _onRoomTileEndDrag = (result) => {
- let newTag = result.destination.droppableId.split('_')[1];
- let prevTag = result.source.droppableId.split('_')[1];
- if (newTag === 'undefined') newTag = undefined;
- if (prevTag === 'undefined') prevTag = undefined;
-
- const roomId = result.draggableId.split('_')[1];
-
- const oldIndex = result.source.index;
- const newIndex = result.destination.index;
-
- dis.dispatch(RoomListActions.tagRoom(
- this._matrixClient,
- this._matrixClient.getRoom(roomId),
- prevTag, newTag,
- oldIndex, newIndex,
- ), true);
- };
-
render() {
const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView');
@@ -679,17 +632,15 @@ class LoggedInView extends React.Component {
aria-hidden={this.props.hideToSRUsers}
>
-
-
- { SettingsStore.getValue("feature_spaces") ? : null }
-
-
- { pageElement }
-
-
+
+ { SettingsStore.getValue("feature_spaces") ? : null }
+
+
+ { pageElement }
+
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 27d628fecf..7986da203d 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -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";
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index 1fab6c4348..d0a2fbff41 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {
{ _t(
- "To set up a filter, drag a community avatar over to the filter panel on " +
- "the far left hand side of the screen. You can click on an avatar in the " +
+ "You can click on an avatar in the " +
"filter panel at any time to see only the rooms and people associated " +
"with that community.",
) }
diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx
index 7770b32f04..c17bf958fd 100644
--- a/src/components/structures/RoomDirectory.tsx
+++ b/src/components/structures/RoomDirectory.tsx
@@ -336,11 +336,10 @@ export default class RoomDirectory extends React.Component {
}
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
+ // If room was shift-clicked, remove it from the room directory
if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault();
this.removeFromDirectory(room);
- } else {
- this.showRoom(room);
}
};
@@ -567,11 +566,11 @@ export default class RoomDirectory extends React.Component {
let avatarUrl = null;
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
+ // We use onMouseDown instead of onClick, so that we can avoid text getting selected
return [
- this.onRoomClicked(room, ev)}
- // cancel onMouseDown otherwise shift-clicking highlights text
- onMouseDown={(ev) => {ev.preventDefault();}}
+
this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomAvatar"
>
{
url={avatarUrl}
/>
,
-
this.onRoomClicked(room, ev)}
- // cancel onMouseDown otherwise shift-clicking highlights text
- onMouseDown={(ev) => {ev.preventDefault();}}
+
this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomDescription"
>
-
{ name }
-
{ ev.stopPropagation(); } }
+
this.onRoomClicked(room, ev)}
+ >
+ { name }
+
+
this.onRoomClicked(room, ev)}
dangerouslySetInnerHTML={{ __html: topic }}
/>
-
{ getDisplayAliasForRoom(room) }
+
this.onRoomClicked(room, ev)}
+ >
+ { getDisplayAliasForRoom(room) }
+
,
-
this.onRoomClicked(room, ev)}
- // cancel onMouseDown otherwise shift-clicking highlights text
- onMouseDown={(ev) => {ev.preventDefault();}}
+
this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_roomMemberCount"
>
{ room.num_joined_members }
,
-
this.onRoomClicked(room, ev)}
+
this.onRoomClicked(room, ev)}
// cancel onMouseDown otherwise shift-clicking highlights text
- onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_preview"
>
- {previewButton}
+ { previewButton }
,
-
this.onRoomClicked(room, ev)}
- // cancel onMouseDown otherwise shift-clicking highlights text
- onMouseDown={(ev) => {ev.preventDefault();}}
+
this.onRoomClicked(room, ev)}
className="mx_RoomDirectory_join"
>
- {joinOrViewButton}
+ { joinOrViewButton }
,
];
}
diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index acbde0b097..4292b60f41 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {ReactNode, useMemo, useState} from "react";
-import {Room} from "matrix-js-sdk/src/models/room";
-import {MatrixClient} from "matrix-js-sdk/src/client";
-import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
+import React, { ReactNode, useMemo, useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import classNames from "classnames";
-import {sortBy} from "lodash";
+import { sortBy } from "lodash";
-import {MatrixClientPeg} from "../../MatrixClientPeg";
+import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
-import {_t} from "../../languageHandler";
-import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
+import { _t } from "../../languageHandler";
+import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import BaseDialog from "../views/dialogs/BaseDialog";
import Spinner from "../views/elements/Spinner";
import SearchBox from "./SearchBox";
import RoomAvatar from "../views/avatars/RoomAvatar";
import RoomName from "../views/elements/RoomName";
-import {useAsyncMemo} from "../../hooks/useAsyncMemo";
-import {EnhancedMap} from "../../utils/maps";
+import { useAsyncMemo } from "../../hooks/useAsyncMemo";
+import { EnhancedMap } from "../../utils/maps";
import StyledCheckbox from "../views/elements/StyledCheckbox";
import AutoHideScrollbar from "./AutoHideScrollbar";
import BaseAvatar from "../views/avatars/BaseAvatar";
-import {mediaFromMxc} from "../../customisations/Media";
+import { mediaFromMxc } from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip";
-import {useStateToggle} from "../../hooks/useStateToggle";
-import {getOrder} from "../../stores/SpaceStore";
+import { useStateToggle } from "../../hooks/useStateToggle";
+import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
-import {linkifyElement} from "../../HtmlUtils";
+import { linkifyElement } from "../../HtmlUtils";
interface IHierarchyProps {
space: Room;
@@ -286,7 +286,7 @@ export const HierarchyLevel = ({
const children = Array.from(relations.get(spaceId)?.values() || []);
const sortedChildren = sortBy(children, ev => {
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
- return getOrder(ev.content.order, null, ev.state_key);
+ return getChildOrder(ev.content.order, null, ev.state_key);
});
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
const roomId = ev.state_key;
diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx
index 17abce0c61..8879629055 100644
--- a/src/components/views/context_menus/DialpadContextMenu.tsx
+++ b/src/components/views/context_menus/DialpadContextMenu.tsx
@@ -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
this.setState({value: this.state.value + digit});
}
+ onChange = (ev) => {
+ this.setState({value: ev.target.value});
+ }
+
+
render() {
return
{_t("Dial pad")}
-
{this.state.value}
+
diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js
index 5a1da1376d..eef10c995a 100644
--- a/src/components/views/context_menus/MessageContextMenu.js
+++ b/src/components/views/context_menus/MessageContextMenu.js
@@ -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,
],
});
diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js
index 8dea62690c..4e381643ba 100644
--- a/src/components/views/context_menus/TagTileContextMenu.js
+++ b/src/components/views/context_menus/TagTileContextMenu.js
@@ -23,45 +23,70 @@ import TagOrderActions from '../../../actions/TagOrderActions';
import {MenuItem} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
+import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
@replaceableComponent("views.context_menus.TagTileContextMenu")
export default class TagTileContextMenu extends React.Component {
static propTypes = {
tag: PropTypes.string.isRequired,
+ index: PropTypes.number.isRequired,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func.isRequired,
};
static contextType = MatrixClientContext;
- constructor() {
- super();
-
- this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
- this._onRemoveClick = this._onRemoveClick.bind(this);
- }
-
- _onViewCommunityClick() {
+ _onViewCommunityClick = () => {
dis.dispatch({
action: 'view_group',
group_id: this.props.tag,
});
this.props.onFinished();
- }
+ };
- _onRemoveClick() {
+ _onRemoveClick = () => {
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
this.props.onFinished();
- }
+ };
+
+ _onMoveUp = () => {
+ dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
+ this.props.onFinished();
+ };
+
+ _onMoveDown = () => {
+ dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1));
+ this.props.onFinished();
+ };
render() {
+ let moveUp;
+ let moveDown;
+ if (this.props.index > 0) {
+ moveUp = (
+
+ { _t("Move up") }
+
+ );
+ }
+ if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) {
+ moveDown = (
+
+ { _t("Move down") }
+
+ );
+ }
+
return
{ _t('View Community') }
+ { (moveUp || moveDown) ?
: null }
+ { moveUp }
+ { moveDown }
- { _t('Hide') }
+ { _t("Unpin") }
;
}
diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
index 822ffc2827..8997e4a5f8 100644
--- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
+++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
@@ -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
= ({
setSelectedToAdd(new Set(selectedToAdd));
} : null;
+ const [truncateAt, setTruncateAt] = useState(20);
+ function overflowTile(overflowCount, totalCount) {
+ const text = _t("and %(count)s others...", { count: overflowCount });
+ return (
+
+ } name={text} presenceState="online" suppressOnHover={true}
+ onClick={() => setTruncateAt(totalCount)} />
+ );
+ }
+
return
= ({
{ rooms.length > 0 ? (
{ _t("Rooms") }
- { rooms.map(room => {
- return {
- onChange(checked, room);
- } : null}
- />;
- }) }
+ rooms.slice(start, end).map(room =>
+ {
+ onChange(checked, room);
+ } : null}
+ />,
+ )}
+ getChildCount={() => rooms.length}
+ />
) : undefined }
diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js
index 929d688e47..77c69abc4e 100644
--- a/src/components/views/dialogs/AddressPickerDialog.js
+++ b/src/components/views/dialogs/AddressPickerDialog.js
@@ -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';
diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx
index 2690eb67d7..b1749b370a 100644
--- a/src/components/views/dialogs/DevtoolsDialog.tsx
+++ b/src/components/views/dialogs/DevtoolsDialog.tsx
@@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent {
render() {
const cli = this.context;
const room = this.props.room;
- const inRoomChannel = cli.crypto._inRoomVerificationRequests;
+ const inRoomChannel = cli.crypto.inRoomVerificationRequests;
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
return (
diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx
index c78b45e5e6..b277021157 100644
--- a/src/components/views/dialogs/ForwardDialog.tsx
+++ b/src/components/views/dialogs/ForwardDialog.tsx
@@ -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
= ({ 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 (
+
+ } name={text} presenceState="online" suppressOnHover={true}
+ onClick={() => setTruncateAt(totalCount)} />
+ );
+ }
+
return = ({ matrixClient: cli, event, permalinkCr
{ rooms.length > 0 ? (
- { rooms.map(room =>
- ,
- ) }
+ rooms.slice(start, end).map(room =>
+ ,
+ )}
+ getChildCount={() => rooms.length}
+ />
) :
{ _t("No results") }
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index ffca9a88a7..553c1c544e 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -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 ({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(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 = React.createRef();
state: IState = {
- disabledButtonIds: [],
+ disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
+ .map(b => b.id),
};
constructor(props) {
diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx
index 1a664951c5..303f17c342 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.tsx
+++ b/src/components/views/dialogs/RoomSettingsDialog.tsx
@@ -108,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component {
ROOM_ADVANCED_TAB,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
- ,
+ this.props.onFinished(true)}
+ />,
));
}
diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx
index a135b6bc16..5e0cd96740 100644
--- a/src/components/views/dialogs/SpaceSettingsDialog.tsx
+++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx
@@ -14,24 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {useState} from 'react';
-import {Room} from "matrix-js-sdk/src/models/room";
-import {MatrixClient} from "matrix-js-sdk/src/client";
-import {EventType} from "matrix-js-sdk/src/@types/event";
+import React, { useMemo } from 'react';
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
-import {_t} from '../../../languageHandler';
-import {IDialogProps} from "./IDialogProps";
+import { _t, _td } from '../../../languageHandler';
+import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
-import DevtoolsDialog from "./DevtoolsDialog";
-import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
-import {getTopic} from "../elements/RoomTopic";
-import {avatarUrlForRoom} from "../../../Avatar";
-import ToggleSwitch from "../elements/ToggleSwitch";
-import AccessibleButton from "../elements/AccessibleButton";
-import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
-import {useDispatcher} from "../../../hooks/useDispatcher";
-import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
+import { useDispatcher } from "../../../hooks/useDispatcher";
+import TabbedView, { Tab } from "../../structures/TabbedView";
+import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab';
+import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab";
+import SettingsStore from "../../../settings/SettingsStore";
+import { UIFeature } from "../../../settings/UIFeature";
+import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
+
+export enum SpaceSettingsTab {
+ General = "SPACE_GENERAL_TAB",
+ Visibility = "SPACE_VISIBILITY_TAB",
+ Advanced = "SPACE_ADVANCED_TAB",
+}
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@@ -45,63 +48,30 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin
}
});
- const [busy, setBusy] = useState(false);
- const [error, setError] = useState("");
-
- const userId = cli.getUserId();
-
- const [newAvatar, setNewAvatar] = useState(null); // undefined means to remove avatar
- const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
- const avatarChanged = newAvatar !== null;
-
- const [name, setName] = useState(space.name);
- const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
- const nameChanged = name !== space.name;
-
- const currentTopic = getTopic(space);
- const [topic, setTopic] = useState(currentTopic);
- const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
- const topicChanged = topic !== currentTopic;
-
- const currentJoinRule = space.getJoinRule();
- const [joinRule, setJoinRule] = useState(currentJoinRule);
- const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
- const joinRuleChanged = joinRule !== currentJoinRule;
-
- const onSave = async () => {
- setBusy(true);
- const promises = [];
-
- if (avatarChanged) {
- if (newAvatar) {
- promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
- url: await cli.uploadContent(newAvatar),
- }, ""));
- } else {
- promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
- }
- }
-
- if (nameChanged) {
- promises.push(cli.setRoomName(space.roomId, name));
- }
-
- if (topicChanged) {
- promises.push(cli.setRoomTopic(space.roomId, topic));
- }
-
- if (joinRuleChanged) {
- promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
- }
-
- const results = await Promise.allSettled(promises);
- setBusy(false);
- const failures = results.filter(r => r.status === "rejected");
- if (failures.length > 0) {
- console.error("Failed to save space settings: ", failures);
- setError(_t("Failed to save space settings."));
- }
- };
+ const tabs = useMemo(() => {
+ return [
+ new Tab(
+ SpaceSettingsTab.General,
+ _td("General"),
+ "mx_SpaceSettingsDialog_generalIcon",
+ ,
+ ),
+ new Tab(
+ SpaceSettingsTab.Visibility,
+ _td("Visibility"),
+ "mx_SpaceSettingsDialog_visibilityIcon",
+ ,
+ ),
+ SettingsStore.getValue(UIFeature.AdvancedSettings)
+ ? new Tab(
+ SpaceSettingsTab.Advanced,
+ _td("Advanced"),
+ "mx_RoomSettingsDialog_warningIcon",
+ ,
+ )
+ : null,
+ ].filter(Boolean);
+ }, [cli, space, onFinished]);
return = ({ matrixClient: cli, space, onFin
onFinished={onFinished}
fixedWidth={false}
>
-
-
{ _t("Edit settings relating to your space.") }
-
- { error &&
{ error }
}
-
-
onFinished(false)} />
-
-
-
-
- { _t("Make this space private") }
- setJoinRule(checked ? "invite" : "public")}
- disabled={!canSetJoinRule}
- aria-label={_t("Make this space private")}
- />
-
-
- {
- defaultDispatcher.dispatch({
- action: "leave_room",
- room_id: space.roomId,
- });
- }}
- >
- { _t("Leave Space") }
-
-
-
-
Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
- { _t("View dev tools") }
-
-
- { _t("Cancel") }
-
-
- { busy ? _t("Saving...") : _t("Save Changes") }
-
-
+
+
;
};
export default SpaceSettingsDialog;
-
diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx
index e634057a21..05bcca24b2 100644
--- a/src/components/views/elements/AccessibleButton.tsx
+++ b/src/components/views/elements/AccessibleButton.tsx
@@ -62,6 +62,8 @@ export default function AccessibleButton({
disabled,
inputRef,
className,
+ onKeyDown,
+ onKeyUp,
...restProps
}: IProps) {
const newProps: IAccessibleButtonProps = restProps;
@@ -83,6 +85,8 @@ export default function AccessibleButton({
if (e.key === Key.SPACE) {
e.stopPropagation();
e.preventDefault();
+ } else {
+ onKeyDown?.(e);
}
};
newProps.onKeyUp = (e) => {
@@ -94,6 +98,8 @@ export default function AccessibleButton({
if (e.key === Key.ENTER) {
e.stopPropagation();
e.preventDefault();
+ } else {
+ onKeyUp?.(e);
}
};
}
diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js
index df66d10a71..f8fa294b71 100644
--- a/src/components/views/elements/AddressTile.js
+++ b/src/components/views/elements/AddressTile.js
@@ -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 {
diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js
index 67572d4508..2e88d37882 100644
--- a/src/components/views/elements/DNDTagTile.js
+++ b/src/components/views/elements/DNDTagTile.js
@@ -18,7 +18,6 @@ limitations under the License.
import TagTile from './TagTile';
import React from 'react';
-import { Draggable } from 'react-beautiful-dnd';
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
import * as sdk from '../../../index';
@@ -31,32 +30,17 @@ export default function DNDTagTile(props) {
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
contextMenu = (
-
+
);
}
- return
-
- {(provided, snapshot) => (
-
-
-
- )}
-
+ return <>
+
{contextMenu}
-
;
+ >;
}
diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.tsx
similarity index 54%
rename from src/components/views/elements/EditableItemList.js
rename to src/components/views/elements/EditableItemList.tsx
index d8ec5af278..89e2e1b8a0 100644
--- a/src/components/views/elements/EditableItemList.js
+++ b/src/components/views/elements/EditableItemList.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2017, 2019 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,48 +14,48 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
-import {_t} from '../../../languageHandler';
+import React from "react";
+
+import { _t } from '../../../languageHandler';
import Field from "./Field";
import AccessibleButton from "./AccessibleButton";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
-export class EditableItem extends React.Component {
- static propTypes = {
- index: PropTypes.number,
- value: PropTypes.string,
- onRemove: PropTypes.func,
+interface IItemProps {
+ index?: number;
+ value?: string;
+ onRemove?(index: number): void;
+}
+
+interface IItemState {
+ verifyRemove: boolean;
+}
+
+export class EditableItem extends React.Component {
+ public state = {
+ verifyRemove: false,
};
- constructor() {
- super();
-
- this.state = {
- verifyRemove: false,
- };
- }
-
- _onRemove = (e) => {
+ private onRemove = (e) => {
e.stopPropagation();
e.preventDefault();
- this.setState({verifyRemove: true});
+ this.setState({ verifyRemove: true });
};
- _onDontRemove = (e) => {
+ private onDontRemove = (e) => {
e.stopPropagation();
e.preventDefault();
- this.setState({verifyRemove: false});
+ this.setState({ verifyRemove: false });
};
- _onActuallyRemove = (e) => {
+ private onActuallyRemove = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.onRemove) this.props.onRemove(this.props.index);
- this.setState({verifyRemove: false});
+ this.setState({ verifyRemove: false });
};
render() {
@@ -66,14 +66,14 @@ export class EditableItem extends React.Component {
{_t("Are you sure?")}
{_t("Yes")}
@@ -85,59 +85,68 @@ export class EditableItem extends React.Component {
return (
);
}
}
+interface IProps {
+ id: string;
+ items: string[];
+ itemsLabel?: string;
+ noItemsLabel?: string;
+ placeholder?: string;
+ newItem?: string;
+ canEdit?: boolean;
+ canRemove?: boolean;
+ suggestionsListId?: string;
+ onItemAdded?(item: string): void;
+ onItemRemoved?(index: number): void;
+ onNewItemChanged?(item: string): void;
+}
+
@replaceableComponent("views.elements.EditableItemList")
-export default class EditableItemList extends React.Component {
- static propTypes = {
- id: PropTypes.string.isRequired,
- items: PropTypes.arrayOf(PropTypes.string).isRequired,
- itemsLabel: PropTypes.string,
- noItemsLabel: PropTypes.string,
- placeholder: PropTypes.string,
- newItem: PropTypes.string,
-
- onItemAdded: PropTypes.func,
- onItemRemoved: PropTypes.func,
- onNewItemChanged: PropTypes.func,
-
- canEdit: PropTypes.bool,
- canRemove: PropTypes.bool,
- };
-
- _onItemAdded = (e) => {
+export default class EditableItemList extends React.PureComponent {
+ protected onItemAdded = (e) => {
e.stopPropagation();
e.preventDefault();
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
};
- _onItemRemoved = (index) => {
+ protected onItemRemoved = (index) => {
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
};
- _onNewItemChanged = (e) => {
+ protected onNewItemChanged = (e) => {
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
};
- _renderNewItemField() {
+ protected renderNewItemField() {
return (
);
@@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component {
key={item}
index={index}
value={item}
- onRemove={this._onItemRemoved}
+ onRemove={this.onItemRemoved}
/>;
});
const editableItemsSection = this.props.canRemove ? editableItems : ;
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
- return (
-
- { label }
+ return (
+
+
+ { label }
+
+ { editableItemsSection }
+ { this.props.canEdit ? this.renderNewItemField() :
}
- { editableItemsSection }
- { this.props.canEdit ? this._renderNewItemField() :
}
-
);
+ );
}
}
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index 8e73b3d9ca..0696ee566e 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -102,7 +102,8 @@ export default class EventTilePreview extends React.Component
{
// Fake it more
event.sender = {
- name: this.props.displayName,
+ name: this.props.displayName || this.props.userId,
+ rawDisplayName: this.props.displayName,
userId: this.props.userId,
getAvatarUrl: (..._) => {
return Avatar.avatarUrlForUser(
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 59d9a11596..1373c2df0e 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -29,6 +29,11 @@ function getId() {
return `${BASE_ID}_${count++}`;
}
+export interface IValidateOpts {
+ focused?: boolean;
+ allowEmpty?: boolean;
+}
+
interface IProps {
// The field's ID, which binds the input and label together. Immutable.
id?: string;
@@ -180,7 +185,7 @@ export default class Field extends React.PureComponent {
}
};
- public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
+ public async validate({ focused, allowEmpty = true }: IValidateOpts) {
if (!this.props.onValidate) {
return;
}
diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.tsx
similarity index 63%
rename from src/components/views/elements/LabelledToggleSwitch.js
rename to src/components/views/elements/LabelledToggleSwitch.tsx
index ef60eeed7b..d97b698fd8 100644
--- a/src/components/views/elements/LabelledToggleSwitch.js
+++ b/src/components/views/elements/LabelledToggleSwitch.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 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,38 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import PropTypes from "prop-types";
+import React from "react";
+
import ToggleSwitch from "./ToggleSwitch";
import {replaceableComponent} from "../../../utils/replaceableComponent";
+interface IProps {
+ // The value for the toggle switch
+ value: boolean;
+ // The translated label for the switch
+ label: string;
+ // Whether or not to disable the toggle switch
+ disabled?: boolean;
+ // True to put the toggle in front of the label
+ // Default false.
+ toggleInFront?: boolean;
+ // Additional class names to append to the switch. Optional.
+ className?: string;
+ // The function to call when the value changes
+ onChange(checked: boolean): void;
+}
+
@replaceableComponent("views.elements.LabelledToggleSwitch")
-export default class LabelledToggleSwitch extends React.Component {
- static propTypes = {
- // The value for the toggle switch
- value: PropTypes.bool.isRequired,
-
- // The function to call when the value changes
- onChange: PropTypes.func.isRequired,
-
- // The translated label for the switch
- label: PropTypes.string.isRequired,
-
- // Whether or not to disable the toggle switch
- disabled: PropTypes.bool,
-
- // True to put the toggle in front of the label
- // Default false.
- toggleInFront: PropTypes.bool,
-
- // Additional class names to append to the switch. Optional.
- className: PropTypes.string,
- };
-
+export default class LabelledToggleSwitch extends React.PureComponent {
render() {
// This is a minimal version of a SettingsFlag
- let firstPart = {this.props.label} ;
+ let firstPart = { this.props.label } ;
let secondPart = {
+ private fieldRef = createRef();
- constructor(props) {
- super(props);
- this.state = {isValid: true};
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isValid: true,
+ };
}
- _asFullAlias(localpart) {
+ private asFullAlias(localpart: string): string {
return `#${localpart}:${this.props.domain}`;
}
render() {
- const Field = sdk.getComponent('views.elements.Field');
const poundSign = (# );
const aliasPostfix = ":" + this.props.domain;
const domain = ({aliasPostfix} );
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
return (
this._fieldRef = ref}
- onValidate={this._onValidate}
- placeholder={_t("e.g. my-room")}
- onChange={this._onChange}
+ ref={this.fieldRef}
+ onValidate={this.onValidate}
+ placeholder={this.props.placeholder || _t("e.g. my-room")}
+ onChange={this.onChange}
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
maxLength={maxlength}
/>
);
}
- _onChange = (ev) => {
+ private onChange = (ev) => {
if (this.props.onChange) {
- this.props.onChange(this._asFullAlias(ev.target.value));
+ this.props.onChange(this.asFullAlias(ev.target.value));
}
};
- _onValidate = async (fieldState) => {
- const result = await this._validationRules(fieldState);
+ private onValidate = async (fieldState) => {
+ const result = await this.validationRules(fieldState);
this.setState({isValid: result.valid});
return result;
};
- _validationRules = withValidation({
+ private validationRules = withValidation({
rules: [
{
key: "safeLocalpart",
@@ -81,7 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
if (!value) {
return true;
}
- const fullAlias = this._asFullAlias(value);
+ const fullAlias = this.asFullAlias(value);
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
encodeURI(fullAlias) === fullAlias;
@@ -90,7 +101,7 @@ export default class RoomAliasField extends React.PureComponent {
}, {
key: "required",
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
- invalid: () => _t("Please provide a room address"),
+ invalid: () => _t("Please provide an address"),
}, {
key: "taken",
final: true,
@@ -100,7 +111,7 @@ export default class RoomAliasField extends React.PureComponent {
}
const client = MatrixClientPeg.get();
try {
- await client.getRoomIdForAlias(this._asFullAlias(value));
+ await client.getRoomIdForAlias(this.asFullAlias(value));
// we got a room id, so the alias is taken
return false;
} catch (err) {
@@ -116,15 +127,15 @@ export default class RoomAliasField extends React.PureComponent {
],
});
- get isValid() {
+ public get isValid() {
return this.state.isValid;
}
- validate(options) {
- return this._fieldRef.validate(options);
+ public validate(options: IValidateOpts) {
+ return this.fieldRef.current?.validate(options);
}
- focus() {
- this._fieldRef.focus();
+ public focus() {
+ this.fieldRef.current?.focus();
}
}
diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx
index 6b9e992f92..744b6f2059 100644
--- a/src/components/views/elements/StyledRadioGroup.tsx
+++ b/src/components/views/elements/StyledRadioGroup.tsx
@@ -34,10 +34,19 @@ interface IProps {
definitions: IDefinition[];
value?: T; // if not provided no options will be selected
outlined?: boolean;
+ disabled?: boolean;
onChange(newValue: T): void;
}
-function StyledRadioGroup({name, definitions, value, className, outlined, onChange}: IProps) {
+function StyledRadioGroup({
+ name,
+ definitions,
+ value,
+ className,
+ outlined,
+ disabled,
+ onChange,
+}: IProps) {
const _onChange = e => {
onChange(e.target.value);
};
@@ -50,12 +59,12 @@ function StyledRadioGroup({name, definitions, value, className
checked={d.checked !== undefined ? d.checked : d.value === value}
name={name}
value={d.value}
- disabled={d.disabled}
+ disabled={disabled || d.disabled}
outlined={outlined}
>
- {d.label}
+ { d.label }
- {d.description}
+ { d.description ? { d.description } : null }
)}
;
}
diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.tsx
similarity index 65%
rename from src/components/views/elements/TruncatedList.js
rename to src/components/views/elements/TruncatedList.tsx
index 0509775545..395caa9222 100644
--- a/src/components/views/elements/TruncatedList.js
+++ b/src/components/views/elements/TruncatedList.tsx
@@ -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
- // 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;
+ // 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 {
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 {
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 (
diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js
index c06d827550..6bef141cb8 100644
--- a/src/components/views/groups/GroupPublicityToggle.js
+++ b/src/components/views/groups/GroupPublicityToggle.js
@@ -66,9 +66,7 @@ export default class GroupPublicityToggle extends React.Component {
render() {
const GroupTile = sdk.getComponent('groups.GroupTile');
return
-
+
diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js
index 42a977fb79..dd8366bbe0 100644
--- a/src/components/views/groups/GroupTile.js
+++ b/src/components/views/groups/GroupTile.js
@@ -16,15 +16,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import { Draggable, Droppable } from 'react-beautiful-dnd';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import FlairStore from '../../../stores/FlairStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media";
-
-function nop() {}
+import { _t } from "../../../languageHandler";
+import TagOrderActions from "../../../actions/TagOrderActions";
+import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
@replaceableComponent("views.groups.GroupTile")
class GroupTile extends React.Component {
@@ -34,7 +34,6 @@ class GroupTile extends React.Component {
showDescription: PropTypes.bool,
// Height of the group avatar in pixels
avatarHeight: PropTypes.number,
- draggable: PropTypes.bool,
};
static contextType = MatrixClientContext;
@@ -42,7 +41,6 @@ class GroupTile extends React.Component {
static defaultProps = {
showDescription: true,
avatarHeight: 50,
- draggable: true,
};
state = {
@@ -57,7 +55,7 @@ class GroupTile extends React.Component {
});
}
- onMouseDown = e => {
+ onClick = e => {
e.preventDefault();
dis.dispatch({
action: 'view_group',
@@ -65,6 +63,18 @@ class GroupTile extends React.Component {
});
};
+ onPinClick = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0));
+ };
+
+ onUnpinClick = e => {
+ e.preventDefault();
+ e.stopPropagation();
+ dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId));
+ };
+
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@@ -78,7 +88,7 @@ class GroupTile extends React.Component {
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
: null;
- let avatarElement = (
+ const avatarElement = (
);
- if (this.props.draggable) {
- const avatarClone = avatarElement;
- avatarElement = (
-
- { (droppableProvided, droppableSnapshot) => (
-
-
- { (provided, snapshot) => (
-
-
- {avatarClone}
-
- { /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
- { provided.placeholder ? avatarClone :
}
-
- ) }
-
-
- ) }
-
- );
- }
- // XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
- // instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6156
- return
+ return
{ avatarElement }
{ name }
{ descElement }
{ this.props.groupId }
+ { !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId)
+ ?
+ { _t("Pin") }
+
+ :
+ { _t("Unpin") }
+
+ }
;
}
diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx
index 883b2bd8a7..e604b04ab0 100644
--- a/src/components/views/messages/SenderProfile.tsx
+++ b/src/components/views/messages/SenderProfile.tsx
@@ -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";
diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx
index a72731522f..1131c02dbf 100644
--- a/src/components/views/right_panel/PinnedMessagesCard.tsx
+++ b/src/components/views/right_panel/PinnedMessagesCard.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React, {useCallback, useContext, useEffect, useState} from "react";
import { Room } from "matrix-js-sdk/src/models/room";
-import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from 'matrix-js-sdk/src/@types/event';
@@ -28,6 +27,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
import PinningUtils from "../../../utils/PinningUtils";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import PinnedEventTile from "../rooms/PinnedEventTile";
+import { useRoomState } from "../../../hooks/useRoomState";
interface IProps {
room: Room;
@@ -75,24 +75,6 @@ export const useReadPinnedEvents = (room: Room): Set => {
return readPinnedEvents;
};
-const useRoomState = (room: Room, mapper: (state: RoomState) => T): T => {
- const [value, setValue] = useState(room ? mapper(room.currentState) : undefined);
-
- const update = useCallback(() => {
- if (!room) return;
- setValue(mapper(room.currentState));
- }, [room, mapper]);
-
- useEventEmitter(room?.currentState, "RoomState.events", update);
- useEffect(() => {
- update();
- return () => {
- setValue(undefined);
- };
- }, [update]);
- return value;
-};
-
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
const cli = useContext(MatrixClientContext);
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx
index 79302e2aa0..111e9dbf38 100644
--- a/src/components/views/right_panel/UserInfo.tsx
+++ b/src/components/views/right_panel/UserInfo.tsx
@@ -503,19 +503,15 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
return member.powerLevel < levelToSend;
};
+const getPowerLevels = room => room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
+
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
- const [powerLevels, setPowerLevels] = useState({});
+ const [powerLevels, setPowerLevels] = useState(getPowerLevels(room));
const update = useCallback((ev?: MatrixEvent) => {
if (!room) return;
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
-
- const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
- if (event) {
- setPowerLevels(event.getContent());
- } else {
- setPowerLevels({});
- }
+ setPowerLevels(getPowerLevels(room));
}, [room]);
useEventEmitter(cli, "RoomState.events", update);
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.tsx
similarity index 71%
rename from src/components/views/room_settings/AliasSettings.js
rename to src/components/views/room_settings/AliasSettings.tsx
index 80e0099ab3..59c4bf2c0c 100644
--- a/src/components/views/room_settings/AliasSettings.js
+++ b/src/components/views/room_settings/AliasSettings.tsx
@@ -1,6 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2018, 2019 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,59 +14,60 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import React, { ChangeEvent, createRef } from "react";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
import EditableItemList from "../elements/EditableItemList";
-import React, {createRef} from 'react';
-import PropTypes from 'prop-types';
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import * as sdk from "../../../index";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
+import Spinner from "../elements/Spinner";
import ErrorDialog from "../dialogs/ErrorDialog";
import AccessibleButton from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import RoomPublishSetting from "./RoomPublishSetting";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import RoomAliasField from "../elements/RoomAliasField";
-class EditableAliasesList extends EditableItemList {
- constructor(props) {
- super(props);
+interface IEditableAliasesListProps {
+ domain?: string;
+}
- this._aliasField = createRef();
- }
+class EditableAliasesList extends EditableItemList {
+ private aliasField = createRef();
- _onAliasAdded = async () => {
- await this._aliasField.current.validate({ allowEmpty: false });
+ private onAliasAdded = async () => {
+ await this.aliasField.current.validate({ allowEmpty: false });
- if (this._aliasField.current.isValid) {
+ if (this.aliasField.current.isValid) {
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
return;
}
- this._aliasField.current.focus();
- this._aliasField.current.validate({ allowEmpty: false, focused: true });
+ this.aliasField.current.focus();
+ this.aliasField.current.validate({ allowEmpty: false, focused: true });
};
- _renderNewItemField() {
+ protected renderNewItemField() {
// if we don't need the RoomAliasField,
- // we don't need to overriden version of _renderNewItemField
+ // we don't need to overriden version of renderNewItemField
if (!this.props.domain) {
- return super._renderNewItemField();
+ return super.renderNewItemField();
}
- const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
- const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
+ const onChange = (alias) => this.onNewItemChanged({target: {value: alias}});
return (
@@ -75,19 +75,30 @@ class EditableAliasesList extends EditableItemList {
}
}
-@replaceableComponent("views.room_settings.AliasSettings")
-export default class AliasSettings extends React.Component {
- static propTypes = {
- roomId: PropTypes.string.isRequired,
- canSetCanonicalAlias: PropTypes.bool.isRequired,
- canSetAliases: PropTypes.bool.isRequired,
- canonicalAliasEvent: PropTypes.object, // MatrixEvent
- };
+interface IProps {
+ roomId: string;
+ canSetCanonicalAlias: boolean;
+ canSetAliases: boolean;
+ canonicalAliasEvent?: MatrixEvent;
+ hidePublishSetting?: boolean;
+}
+interface IState {
+ altAliases: string[];
+ localAliases: string[];
+ canonicalAlias?: string;
+ updatingCanonicalAlias: boolean;
+ localAliasesLoading: boolean;
+ detailsOpen: boolean;
+ newAlias?: string;
+ newAltAlias?: string;
+}
+
+@replaceableComponent("views.room_settings.AliasSettings")
+export default class AliasSettings extends React.Component {
static defaultProps = {
canSetAliases: false,
canSetCanonicalAlias: false,
- aliasEvents: [],
};
constructor(props) {
@@ -122,7 +133,7 @@ export default class AliasSettings extends React.Component {
}
}
- async loadLocalAliases() {
+ private async loadLocalAliases() {
this.setState({ localAliasesLoading: true });
try {
const cli = MatrixClientPeg.get();
@@ -134,12 +145,16 @@ export default class AliasSettings extends React.Component {
}
}
this.setState({ localAliases });
+
+ if (localAliases.length === 0) {
+ this.setState({ detailsOpen: true });
+ }
} finally {
this.setState({ localAliasesLoading: false });
}
}
- changeCanonicalAlias(alias) {
+ private changeCanonicalAlias(alias: string) {
if (!this.props.canSetCanonicalAlias) return;
const oldAlias = this.state.canonicalAlias;
@@ -170,7 +185,7 @@ export default class AliasSettings extends React.Component {
});
}
- changeAltAliases(altAliases) {
+ private changeAltAliases(altAliases: string[]) {
if (!this.props.canSetCanonicalAlias) return;
this.setState({
@@ -181,7 +196,7 @@ export default class AliasSettings extends React.Component {
const eventContent = {};
if (this.state.canonicalAlias) {
- eventContent.alias = this.state.canonicalAlias;
+ eventContent["alias"] = this.state.canonicalAlias;
}
if (altAliases) {
eventContent["alt_aliases"] = altAliases;
@@ -202,11 +217,11 @@ export default class AliasSettings extends React.Component {
});
}
- onNewAliasChanged = (value) => {
- this.setState({newAlias: value});
+ private onNewAliasChanged = (value: string) => {
+ this.setState({ newAlias: value });
};
- onLocalAliasAdded = (alias) => {
+ private onLocalAliasAdded = (alias: string) => {
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
const localDomain = MatrixClientPeg.get().getDomain();
@@ -232,7 +247,7 @@ export default class AliasSettings extends React.Component {
});
};
- onLocalAliasDeleted = (index) => {
+ private onLocalAliasDeleted = (index: number) => {
const alias = this.state.localAliases[index];
// TODO: In future, we should probably be making sure that the alias actually belongs
// to this room. See https://github.com/vector-im/element-web/issues/7353
@@ -261,7 +276,7 @@ export default class AliasSettings extends React.Component {
});
};
- onLocalAliasesToggled = (event) => {
+ private onLocalAliasesToggled = (event: ChangeEvent) => {
// expanded
if (event.target.open) {
// if local aliases haven't been preloaded yet at component mount
@@ -269,43 +284,45 @@ export default class AliasSettings extends React.Component {
this.loadLocalAliases();
}
}
- this.setState({detailsOpen: event.target.open});
+ this.setState({ detailsOpen: event.currentTarget.open });
};
- onCanonicalAliasChange = (event) => {
+ private onCanonicalAliasChange = (event: ChangeEvent) => {
this.changeCanonicalAlias(event.target.value);
};
- onNewAltAliasChanged = (value) => {
- this.setState({newAltAlias: value});
+ private onNewAltAliasChanged = (value: string) => {
+ this.setState({ newAltAlias: value });
}
- onAltAliasAdded = (alias) => {
+ private onAltAliasAdded = (alias: string) => {
const altAliases = this.state.altAliases.slice();
if (!altAliases.some(a => a.trim() === alias.trim())) {
altAliases.push(alias.trim());
this.changeAltAliases(altAliases);
- this.setState({newAltAlias: ""});
+ this.setState({ newAltAlias: "" });
}
}
- onAltAliasDeleted = (index) => {
+ private onAltAliasDeleted = (index: number) => {
const altAliases = this.state.altAliases.slice();
altAliases.splice(index, 1);
this.changeAltAliases(altAliases);
}
- _getAliases() {
- return this.state.altAliases.concat(this._getLocalNonAltAliases());
+ private getAliases() {
+ return this.state.altAliases.concat(this.getLocalNonAltAliases());
}
- _getLocalNonAltAliases() {
+ private getLocalNonAltAliases() {
const {altAliases} = this.state;
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
}
render() {
- const localDomain = MatrixClientPeg.get().getDomain();
+ const cli = MatrixClientPeg.get();
+ const localDomain = cli.getDomain();
+ const isSpaceRoom = cli.getRoom(this.props.roomId)?.isSpaceRoom();
let found = false;
const canonicalValue = this.state.canonicalAlias || "";
@@ -320,7 +337,7 @@ export default class AliasSettings extends React.Component {
>
{ _t('not specified') }
{
- this._getAliases().map((alias, i) => {
+ this.getAliases().map((alias, i) => {
if (alias === this.state.canonicalAlias) found = true;
return (
@@ -340,12 +357,10 @@ export default class AliasSettings extends React.Component {
let localAliasesList;
if (this.state.localAliasesLoading) {
- const Spinner = sdk.getComponent("elements.Spinner");
localAliasesList = ;
} else {
localAliasesList = ( );
@@ -362,18 +379,27 @@ export default class AliasSettings extends React.Component {
return (
{_t("Published Addresses")}
-
{_t("Published addresses can be used by anyone on any server to join your room. " +
- "To publish an address, it needs to be set as a local address first.")}
- {canonicalAliasSection}
-
+
+ { isSpaceRoom
+ ? _t("Published addresses can be used by anyone on any server to join your space.")
+ : _t("Published addresses can be used by anyone on any server to join your room.")}
+
+ { _t("To publish an address, it needs to be set as a local address first.") }
+
+ { canonicalAliasSection }
+ { this.props.hidePublishSetting
+ ? null
+ :
}
- {this._getLocalNonAltAliases().map(alias => {
+ {this.getLocalNonAltAliases().map(alias => {
return ;
})};
-
{_t("Local Addresses")}
-
{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}
-
+
+ { _t("Local Addresses") }
+
+
+ { isSpaceRoom
+ ? _t("Set addresses for this space so users can find this space " +
+ "through your homeserver (%(localDomain)s)", { localDomain })
+ : _t("Set addresses for this room so users can find this room " +
+ "through your homeserver (%(localDomain)s)", { localDomain }) }
+
+
{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}
- {localAliasesList}
+ { localAliasesList }
);
diff --git a/src/components/views/room_settings/RoomPublishSetting.js b/src/components/views/room_settings/RoomPublishSetting.tsx
similarity index 60%
rename from src/components/views/room_settings/RoomPublishSetting.js
rename to src/components/views/room_settings/RoomPublishSetting.tsx
index 6cc3ce26ba..95b0ac100d 100644
--- a/src/components/views/room_settings/RoomPublishSetting.js
+++ b/src/components/views/room_settings/RoomPublishSetting.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020 - 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,20 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React from "react";
+
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
-import {_t} from "../../../languageHandler";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { _t } from "../../../languageHandler";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+ roomId: string;
+ label?: string;
+ canSetCanonicalAlias?: boolean;
+}
+
+interface IState {
+ isRoomPublished: boolean;
+}
@replaceableComponent("views.room_settings.RoomPublishSetting")
-export default class RoomPublishSetting extends React.PureComponent {
- constructor(props) {
- super(props);
- this.state = {isRoomPublished: false};
+export default class RoomPublishSetting extends React.PureComponent {
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isRoomPublished: false,
+ };
}
- onRoomPublishChange = (e) => {
+ private onRoomPublishChange = (e) => {
const valueBefore = this.state.isRoomPublished;
const newValue = !valueBefore;
this.setState({isRoomPublished: newValue});
@@ -52,11 +66,14 @@ export default class RoomPublishSetting extends React.PureComponent {
render() {
const client = MatrixClientPeg.get();
- return ( );
+ return (
+
+ );
}
}
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.tsx
similarity index 64%
rename from src/components/views/rooms/MemberList.js
rename to src/components/views/rooms/MemberList.tsx
index cb50f0fff3..5ebe5bea59 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.tsx
@@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd
+Copyright 2021 Šimon Brandner
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;
+ filteredJoinedMembers: Array;
+ filteredInvitedMembers: Array;
+ 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 {
+ private showPresence = true;
+ private mounted = false;
+ private collator: Intl.Collator;
+ private sortNames = new Map(); // 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 {
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): 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 {
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 {
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 (
{
+ 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 : "") + ", " + (u ? u.getLastActiveTs() : "") + ", " + (u ? u.currentlyActive : "") + ", " + (u ? u.presence : "") + ")";
+ return (
+ "(" +
+ member.name +
+ ", " +
+ member.powerLevel +
+ ", " +
+ (u ? u.lastActiveAgo : "") +
+ ", " +
+ (u ? u.getLastActiveTs() : "") +
+ ", " +
+ (u ? u.currentlyActive : "") +
+ ", " +
+ (u ? u.presence : "") +
+ ")"
+ );
}
}
// 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, membership: string, query?: string): Array {
return members.filter((m) => {
if (query) {
query = query.toLowerCase();
@@ -389,7 +437,7 @@ export default class MemberList extends React.Component {
});
}
- _getPending3PidInvites() {
+ private getPending3PidInvites(): Array {
// 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) {
return members.map((m) => {
- if (m.userId) {
+ if (m instanceof RoomMember) {
// Is a Matrix invite
- return ;
+ return ;
} else {
// Is a 3pid invite
return 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 => {
+ 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 => {
+ 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 ;
}
- 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 =
-
+ inviteButton = (
+
{ inviteButtonText }
- ;
+
+ );
}
let invitedHeader;
let invitedSection;
- if (this._getChildCountInvited() > 0) {
+ if (this.getChildCountInvited() > 0) {
invitedHeader = { _t("Invited") } ;
- invitedSection = ;
+ invitedSection = (
+
+ );
}
const footer = (
@@ -517,17 +568,19 @@ export default class MemberList extends React.Component {
previousPhase={previousPhase}
>
-
+
{ invitedHeader }
{ invitedSection }
;
}
- onInviteButtonClick = () => {
+ onInviteButtonClick = (): void => {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
return;
diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx
index d277a69907..0b79f7b52e 100644
--- a/src/components/views/rooms/RoomList.tsx
+++ b/src/components/views/rooms/RoomList.tsx
@@ -466,6 +466,7 @@ export default class RoomList extends React.PureComponent {
}
private renderCommunityInvites(): ReactComponentElement[] {
+ if (SettingsStore.getValue("feature_spaces")) return [];
// TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/element-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => {
diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx
index 20d8c9c5d4..122ba0ca0b 100644
--- a/src/components/views/rooms/VoiceRecordComposerTile.tsx
+++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx
@@ -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
diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js
index 0cd1a64ada..43a13a48a7 100644
--- a/src/components/views/settings/CrossSigningPanel.js
+++ b/src/components/views/settings/CrossSigningPanel.js
@@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
async _getUpdatedStatus() {
const cli = MatrixClientPeg.get();
const pkCache = cli.getCrossSigningCacheCallbacks();
- const crossSigning = cli.crypto._crossSigningInfo;
- const secretStorage = cli.crypto._secretStorage;
+ const crossSigning = cli.crypto.crossSigningInfo;
+ const secretStorage = cli.crypto.secretStorage;
const crossSigningPublicKeysOnDevice = crossSigning.getId();
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js
index 4f3eb0bdf6..abfd18f0d3 100644
--- a/src/components/views/settings/SecureBackupPanel.js
+++ b/src/components/views/settings/SecureBackupPanel.js
@@ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent {
async _getUpdatedDiagnostics() {
const cli = MatrixClientPeg.get();
- const secretStorage = cli.crypto._secretStorage;
+ const secretStorage = cli.crypto.secretStorage;
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
diff --git a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
similarity index 61%
rename from src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
rename to src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
index 28aad65129..c4963d0154 100644
--- a/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 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,68 +15,76 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import {_t} from "../../../../../languageHandler";
-import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
-import * as sdk from "../../../../..";
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+
+import { _t } from "../../../../../languageHandler";
+import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../../elements/AccessibleButton";
+import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
+import DevtoolsDialog from "../../../dialogs/DevtoolsDialog";
import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher";
-import {replaceableComponent} from "../../../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../../../utils/replaceableComponent";
+
+interface IProps {
+ roomId: string;
+ closeSettingsFn(): void;
+}
+
+interface IRecommendedVersion {
+ version: string;
+ needsUpgrade: boolean;
+ urgent: boolean;
+}
+
+interface IState {
+ upgradeRecommendation?: IRecommendedVersion;
+ oldRoomId?: string;
+ oldEventId?: string;
+ upgraded?: boolean;
+}
@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
-export default class AdvancedRoomSettingsTab extends React.Component {
- static propTypes = {
- roomId: PropTypes.string.isRequired,
- closeSettingsFn: PropTypes.func.isRequired,
- };
-
- constructor(props) {
- super(props);
+export default class AdvancedRoomSettingsTab extends React.Component {
+ constructor(props, context) {
+ super(props, context);
this.state = {
// This is eventually set to the value of room.getRecommendedVersion()
upgradeRecommendation: null,
};
- }
- // TODO: [REACT-WARNING] Move this to constructor
- UNSAFE_componentWillMount() { // eslint-disable-line camelcase
// we handle lack of this object gracefully later, so don't worry about it failing here.
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
room.getRecommendedVersion().then((v) => {
- const tombstone = room.currentState.getStateEvents("m.room.tombstone", "");
+ const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, "");
- const additionalStateChanges = {};
- const createEvent = room.currentState.getStateEvents("m.room.create", "");
+ const additionalStateChanges: Partial = {};
+ const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
const predecessor = createEvent ? createEvent.getContent().predecessor : null;
if (predecessor && predecessor.room_id) {
- additionalStateChanges['oldRoomId'] = predecessor.room_id;
- additionalStateChanges['oldEventId'] = predecessor.event_id;
- additionalStateChanges['hasPreviousRoom'] = true;
+ additionalStateChanges.oldRoomId = predecessor.room_id;
+ additionalStateChanges.oldEventId = predecessor.event_id;
}
-
this.setState({
- upgraded: tombstone && tombstone.getContent().replacement_room,
+ upgraded: !!tombstone?.getContent().replacement_room,
upgradeRecommendation: v,
...additionalStateChanges,
});
});
}
- _upgradeRoom = (e) => {
- const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
+ private upgradeRoom = (e) => {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
- Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room});
+ Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room });
};
- _openDevtools = (e) => {
- const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
+ private openDevtools = (e) => {
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
};
- _onOldRoomClicked = (e) => {
+ private onOldRoomClicked = (e) => {
e.preventDefault();
e.stopPropagation();
@@ -93,9 +101,9 @@ export default class AdvancedRoomSettingsTab extends React.Component {
const room = client.getRoom(this.props.roomId);
let unfederatableSection;
- const createEvent = room.currentState.getStateEvents('m.room.create', '');
+ const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
if (createEvent && createEvent.getContent()['m.federate'] === false) {
- unfederatableSection = {_t('This room is not accessible by remote Matrix servers')}
;
+ unfederatableSection = { _t('This room is not accessible by remote Matrix servers') }
;
}
let roomUpgradeButton;
@@ -103,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component {
roomUpgradeButton = (
- {_t(
+ { _t(
"Warning : Upgrading a room will not automatically migrate room members " +
"to the new version of the room. We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
@@ -111,51 +119,53 @@ export default class AdvancedRoomSettingsTab extends React.Component {
"b": (sub) => {sub} ,
"i": (sub) => {sub} ,
},
- )}
+ ) }
-
- {_t("Upgrade this room to the recommended room version")}
+
+ { _t("Upgrade this room to the recommended room version") }
);
}
let oldRoomLink;
- if (this.state.hasPreviousRoom) {
+ if (this.state.oldRoomId) {
let name = _t("this room");
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (room && room.name) name = room.name;
oldRoomLink = (
-
- {_t("View older messages in %(roomName)s.", {roomName: name})}
+
+ { _t("View older messages in %(roomName)s.", { roomName: name }) }
);
}
return (
-
{_t("Advanced")}
+
{ _t("Advanced") }
-
{_t("Room information")}
+
+ { room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
+
- {_t("Internal room ID:")}
- {this.props.roomId}
+ { _t("Internal room ID:") }
+ { this.props.roomId }
- {unfederatableSection}
+ { unfederatableSection }
-
{_t("Room version")}
+
{ _t("Room version") }
- {_t("Room version:")}
- {room.getVersion()}
+ { _t("Room version:") }
+ { room.getVersion() }
- {oldRoomLink}
- {roomUpgradeButton}
+ { oldRoomLink }
+ { roomUpgradeButton }
-
{_t("Developer options")}
-
- {_t("Open Devtools")}
+ { _t("Developer options") }
+
+ { _t("Open Devtools") }
diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
index 139cfd5fbd..10c93c5dca 100644
--- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
@@ -60,7 +60,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
- const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
@@ -100,7 +99,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
+ canonicalAliasEvent={canonicalAliasEv} />
{_t("Other")}
{ flairSection }
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
index 02bbcfb751..bb7e194253 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
@@ -29,19 +29,19 @@ import {UIFeature} from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
// Knock and private are reserved keywords which are not yet implemented.
-enum JoinRule {
+export enum JoinRule {
Public = "public",
Knock = "knock",
Invite = "invite",
Private = "private",
}
-enum GuestAccess {
+export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
}
-enum HistoryVisibility {
+export enum HistoryVisibility {
Invited = "invited",
Joined = "joined",
Shared = "shared",
@@ -121,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
+ private onEncryptionChange = () => {
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
title: _t('Enable encryption?'),
description: _t(
diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
index 362059f8ed..f730406eed 100644
--- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
@@ -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 = (
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 977cd4a9aa..2d096e1b9f 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -35,6 +35,7 @@ import withValidation from "../elements/Validation";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import { Preset } from "matrix-js-sdk/src/@types/partials";
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
+import RoomAliasField from "../elements/RoomAliasField";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return (
@@ -60,6 +61,11 @@ const spaceNameValidator = withValidation({
],
});
+const nameToAlias = (name: string, domain: string): string => {
+ const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
+ return `#${localpart}:${domain}`;
+};
+
const SpaceCreateMenu = ({ onFinished }) => {
const cli = useContext(MatrixClientContext);
const [visibility, setVisibility] = useState(null);
@@ -67,6 +73,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
const [name, setName] = useState("");
const spaceNameField = useRef();
+ const [alias, setAlias] = useState("");
+ const spaceAliasField = useRef();
const [avatar, setAvatar] = useState(null);
const [topic, setTopic] = useState("");
@@ -82,6 +90,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
setBusy(false);
return;
}
+ // validate the space name alias field but do not require it
+ if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
+ spaceAliasField.current.focus();
+ spaceAliasField.current.validate({ allowEmpty: true, focused: true });
+ setBusy(false);
+ return;
+ }
const initialState: ICreateRoomStateEvent[] = [
{
@@ -99,12 +114,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
content: { url },
});
}
- if (topic) {
- initialState.push({
- type: EventType.RoomTopic,
- content: { topic },
- });
- }
try {
await createRoom({
@@ -112,7 +121,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
name,
creation_content: {
- // Based on MSC1840
[RoomCreateTypeField]: RoomType.Space,
},
initial_state: initialState,
@@ -121,6 +129,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
events_default: 100,
...Visibility.Public ? { invite: 0 } : {},
},
+ room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined,
+ topic,
},
spinner: false,
encryption: false,
@@ -159,6 +169,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
;
} else {
+ const domain = cli.getDomain();
body =
{
label={_t("Name")}
autoFocus={true}
value={name}
- onChange={ev => setName(ev.target.value)}
+ onChange={ev => {
+ const newName = ev.target.value;
+ if (!alias || alias === nameToAlias(name, domain)) {
+ setAlias(nameToAlias(newName, domain));
+ }
+ setName(newName);
+ }}
ref={spaceNameField}
onValidate={spaceNameValidator}
disabled={busy}
/>
+ { visibility === Visibility.Public
+ ?
+ : null
+ }
+
{
return [invites, spaces, activeSpace];
};
+interface IInnerSpacePanelProps {
+ children?: ReactNode;
+ isPanelCollapsed: boolean;
+ setPanelCollapsed: Dispatch>;
+}
+
+// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
+const InnerSpacePanel = React.memo(({ children, isPanelCollapsed, setPanelCollapsed }) => {
+ const [invites, spaces, activeSpace] = useSpaces();
+ const activeSpaces = activeSpace ? [activeSpace] : [];
+
+ const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
+ ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
+
+ return
+ SpaceStore.instance.setActiveSpace(null)}
+ selected={!activeSpace}
+ tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
+ notificationState={homeNotificationState}
+ isNarrow={isPanelCollapsed}
+ />
+ { invites.map(s => (
+ setPanelCollapsed(false)}
+ />
+ )) }
+ { spaces.map((s, i) => (
+
+ {(provided, snapshot) => (
+ setPanelCollapsed(false)}
+ />
+ )}
+
+ )) }
+ { children }
+
;
+});
+
const SpacePanel = () => {
// We don't need the handle as we position the menu in a constant location
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
- const [invites, spaces, activeSpace] = useSpaces();
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
useEffect(() => {
@@ -135,10 +190,6 @@ const SpacePanel = () => {
}
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
- const newClasses = classNames("mx_SpaceButton_new", {
- mx_SpaceButton_newCancel: menuDisplayed,
- });
-
let contextMenu = null;
if (menuDisplayed) {
contextMenu = ;
@@ -205,63 +256,61 @@ const SpacePanel = () => {
}
};
- const activeSpaces = activeSpace ? [activeSpace] : [];
- const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
+ const onNewClick = menuDisplayed ? closeMenu : () => {
+ if (!isPanelCollapsed) setPanelCollapsed(true);
+ openMenu();
+ };
- const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
- ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
+ return (
+ {
+ if (!result.destination) return; // dropped outside the list
+ SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
+ }}>
+
+ {({onKeyDownHandler}) => (
+
+
+ {(provided, snapshot) => (
+
+
+ { provided.placeholder }
+
- // TODO drag and drop for re-arranging order
- return
- {({onKeyDownHandler}) => (
-
-
-
-
SpaceStore.instance.setActiveSpace(null)}
- selected={!activeSpace}
- tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
- notificationState={homeNotificationState}
- isNarrow={isPanelCollapsed}
+
+
+ )}
+
+ setPanelCollapsed(!isPanelCollapsed)}
+ title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")}
/>
- { invites.map(s => setPanelCollapsed(false)}
- />) }
- { spaces.map(s => setPanelCollapsed(false)}
- />) }
-
- {
- if (!isPanelCollapsed) setPanelCollapsed(true);
- openMenu();
- }}
- isNarrow={isPanelCollapsed}
- />
-
- setPanelCollapsed(!isPanelCollapsed)}
- title={expandCollapseButtonTitle}
- />
- { contextMenu }
-
- )}
-
+ { contextMenu }
+
+ )}
+
+
+ );
};
export default SpacePanel;
diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
new file mode 100644
index 0000000000..3afdc629e4
--- /dev/null
+++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx
@@ -0,0 +1,143 @@
+/*
+Copyright 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.
+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, { useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
+import { _t } from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
+import SpaceBasicSettings from "./SpaceBasicSettings";
+import { avatarUrlForRoom } from "../../../Avatar";
+import { IDialogProps } from "../dialogs/IDialogProps";
+import { getTopic } from "../elements/RoomTopic";
+import { defaultDispatcher } from "../../../dispatcher/dispatcher";
+
+interface IProps extends IDialogProps {
+ matrixClient: MatrixClient;
+ space: Room;
+}
+
+const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProps) => {
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState("");
+
+ const userId = cli.getUserId();
+
+ const [newAvatar, setNewAvatar] = useState(null); // undefined means to remove avatar
+ const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
+ const avatarChanged = newAvatar !== null;
+
+ const [name, setName] = useState(space.name);
+ const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
+ const nameChanged = name !== space.name;
+
+ const currentTopic = getTopic(space);
+ const [topic, setTopic] = useState(currentTopic);
+ const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
+ const topicChanged = topic !== currentTopic;
+
+ const onCancel = () => {
+ setNewAvatar(null);
+ setName(space.name);
+ setTopic(currentTopic);
+ };
+
+ const onSave = async () => {
+ setBusy(true);
+ const promises = [];
+
+ if (avatarChanged) {
+ if (newAvatar) {
+ promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
+ url: await cli.uploadContent(newAvatar),
+ }, ""));
+ } else {
+ promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
+ }
+ }
+
+ if (nameChanged) {
+ promises.push(cli.setRoomName(space.roomId, name));
+ }
+
+ if (topicChanged) {
+ promises.push(cli.setRoomTopic(space.roomId, topic));
+ }
+
+ const results = await Promise.allSettled(promises);
+ setBusy(false);
+ const failures = results.filter(r => r.status === "rejected");
+ if (failures.length > 0) {
+ console.error("Failed to save space settings: ", failures);
+ setError(_t("Failed to save space settings."));
+ }
+ };
+
+ return
+
{ _t("General") }
+
+
{ _t("Edit settings relating to your space.") }
+
+ { error &&
{ error }
}
+
+
onFinished(false)} />
+
+
+
+
+
+ { _t("Cancel") }
+
+
+ { busy ? _t("Saving...") : _t("Save Changes") }
+
+
+
+ {_t("Leave Space")}
+
+
{
+ defaultDispatcher.dispatch({
+ action: "leave_room",
+ room_id: space.roomId,
+ });
+ }}
+ >
+ { _t("Leave Space") }
+
+
+ ;
+};
+
+export default SpaceSettingsGeneralTab;
diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
new file mode 100644
index 0000000000..263823603b
--- /dev/null
+++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
@@ -0,0 +1,187 @@
+/*
+Copyright 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.
+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, { useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
+import { _t } from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import AliasSettings from "../room_settings/AliasSettings";
+import { useStateToggle } from "../../../hooks/useStateToggle";
+import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
+import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab";
+import StyledRadioGroup from "../elements/StyledRadioGroup";
+
+interface IProps {
+ matrixClient: MatrixClient;
+ space: Room;
+}
+
+enum SpaceVisibility {
+ Unlisted = "unlisted",
+ Private = "private",
+}
+
+const useLocalEcho = (
+ currentFactory: () => T,
+ setterFn: (value: T) => Promise,
+ errorFn: (error: Error) => void,
+): [value: T, handler: (value: T) => void] => {
+ const [value, setValue] = useState(currentFactory);
+ const handler = async (value: T) => {
+ setValue(value);
+ try {
+ await setterFn(value);
+ } catch (e) {
+ setValue(currentFactory());
+ errorFn(e);
+ }
+ };
+
+ return [value, handler];
+};
+
+const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
+ const [error, setError] = useState("");
+
+ const userId = cli.getUserId();
+
+ const [visibility, setVisibility] = useLocalEcho(
+ () => space.getJoinRule() === JoinRule.Private ? SpaceVisibility.Private : SpaceVisibility.Unlisted,
+ visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, {
+ join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Private,
+ }, ""),
+ () => setError(_t("Failed to update the visibility of this space")),
+ );
+ const [guestAccessEnabled, setGuestAccessEnabled] = useLocalEcho(
+ () => space.currentState.getStateEvents(EventType.RoomGuestAccess, "")
+ ?.getContent()?.guest_access === GuestAccess.CanJoin,
+ guestAccessEnabled => cli.sendStateEvent(space.roomId, EventType.RoomGuestAccess, {
+ guest_access: guestAccessEnabled ? GuestAccess.CanJoin : GuestAccess.Forbidden,
+ }, ""),
+ () => setError(_t("Failed to update the guest access of this space")),
+ );
+ const [historyVisibility, setHistoryVisibility] = useLocalEcho(
+ () => space.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")
+ ?.getContent()?.history_visibility || HistoryVisibility.Shared,
+ historyVisibility => cli.sendStateEvent(space.roomId, EventType.RoomHistoryVisibility, {
+ history_visibility: historyVisibility,
+ }, ""),
+ () => setError(_t("Failed to update the history visibility of this space")),
+ );
+
+ const [showAdvancedSection, toggleAdvancedSection] = useStateToggle();
+
+ const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
+ const canSetGuestAccess = space.currentState.maySendStateEvent(EventType.RoomGuestAccess, userId);
+ const canSetHistoryVisibility = space.currentState.maySendStateEvent(EventType.RoomHistoryVisibility, userId);
+ const canSetCanonical = space.currentState.mayClientSendStateEvent(EventType.RoomCanonicalAlias, cli);
+ const canonicalAliasEv = space.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
+
+ let advancedSection;
+ if (showAdvancedSection) {
+ advancedSection = <>
+
+ { _t("Hide advanced") }
+
+
+
+
+ { _t("Guests can join a space without having an account.") }
+
+ { _t("This may be useful for public spaces.") }
+
+ >;
+ } else {
+ advancedSection = <>
+
+ { _t("Show advanced") }
+
+ >;
+ }
+
+ let addressesSection;
+ if (visibility !== SpaceVisibility.Private) {
+ addressesSection = <>
+ {_t("Address")}
+
+ >;
+ }
+
+ return
+
{ _t("Visibility") }
+
+ { error &&
{ error }
}
+
+
+
+ { _t("Decide who can view and join %(spaceName)s.", { spaceName: space.name }) }
+
+
+
+
+
+
+ { advancedSection }
+
+
{
+ setHistoryVisibility(checked ? HistoryVisibility.WorldReadable : HistoryVisibility.Shared);
+ }}
+ disabled={!canSetHistoryVisibility}
+ label={_t("Preview Space")}
+ />
+ { _t("Allow people to preview your space before they join.") }
+ { _t("Recommended for public spaces.") }
+
+
+ { addressesSection }
+
;
+};
+
+export default SpaceSettingsVisibilityTab;
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index f34baf256b..75ca641320 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -14,23 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { createRef, InputHTMLAttributes, LegacyRef } from "react";
import classNames from "classnames";
-import {Room} from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
import RoomAvatar from "../avatars/RoomAvatar";
import SpaceStore from "../../../stores/SpaceStore";
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
import NotificationBadge from "../rooms/NotificationBadge";
-import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
-import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
+import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
-import {_t} from "../../../languageHandler";
-import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
-import {toRightOf} from "../../structures/ContextMenu";
+import { _t } from "../../../languageHandler";
+import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
+import { toRightOf } from "../../structures/ContextMenu";
import {
shouldShowSpaceSettings,
showAddExistingRooms,
@@ -39,33 +38,38 @@ import {
showSpaceSettings,
} from "../../../utils/space";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton";
+import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
-import {Action} from "../../../dispatcher/actions";
+import { Action } from "../../../dispatcher/actions";
import RoomViewStore from "../../../stores/RoomViewStore";
-import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
-import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
-import {EventType} from "matrix-js-sdk/src/@types/event";
-import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
-import {NotificationColor} from "../../../stores/notifications/NotificationColor";
+import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
+import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
+import { NotificationColor } from "../../../stores/notifications/NotificationColor";
+import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
-interface IItemProps {
+interface IItemProps extends InputHTMLAttributes {
space?: Room;
activeSpaces: Room[];
isNested?: boolean;
isPanelCollapsed?: boolean;
onExpand?: Function;
parents?: Set;
+ innerRef?: LegacyRef;
}
interface IItemState {
collapsed: boolean;
contextMenuPosition: Pick;
+ childSpaces: Room[];
}
export class SpaceItem extends React.PureComponent {
static contextType = MatrixClientContext;
+ private buttonRef = createRef();
+
constructor(props) {
super(props);
@@ -78,14 +82,36 @@ export class SpaceItem extends React.PureComponent {
this.state = {
collapsed: collapsed,
contextMenuPosition: null,
+ childSpaces: this.childSpaces,
};
+
+ SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
}
- private toggleCollapse(evt) {
- if (this.props.onExpand && this.state.collapsed) {
+ componentWillUnmount() {
+ SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate);
+ }
+
+ private onSpaceUpdate = () => {
+ this.setState({
+ childSpaces: this.childSpaces,
+ });
+ };
+
+ private get childSpaces() {
+ return SpaceStore.instance.getChildSpaces(this.props.space.roomId)
+ .filter(s => !this.props.parents?.has(s.roomId));
+ }
+
+ private get isCollapsed() {
+ return this.state.collapsed || this.props.isPanelCollapsed;
+ }
+
+ private toggleCollapse = evt => {
+ if (this.props.onExpand && this.isCollapsed) {
this.props.onExpand();
}
- const newCollapsedState = !this.state.collapsed;
+ const newCollapsedState = !this.isCollapsed;
SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState(
this.props.space.roomId,
@@ -96,7 +122,7 @@ export class SpaceItem extends React.PureComponent {
// don't bubble up so encapsulating button for space
// doesn't get triggered
evt.stopPropagation();
- }
+ };
private onContextMenu = (ev: React.MouseEvent) => {
if (this.props.space.getMyMembership() !== "join") return;
@@ -111,6 +137,43 @@ export class SpaceItem extends React.PureComponent {
});
}
+ private onKeyDown = (ev: React.KeyboardEvent) => {
+ let handled = true;
+ const action = getKeyBindingsManager().getRoomListAction(ev);
+ const hasChildren = this.state.childSpaces?.length;
+ switch (action) {
+ case RoomListAction.CollapseSection:
+ if (hasChildren && !this.isCollapsed) {
+ this.toggleCollapse(ev);
+ } else {
+ const parentItem = this.buttonRef?.current?.parentElement?.parentElement;
+ const parentButton = parentItem?.previousElementSibling as HTMLElement;
+ parentButton?.focus();
+ }
+ break;
+
+ case RoomListAction.ExpandSection:
+ if (hasChildren) {
+ if (this.isCollapsed) {
+ this.toggleCollapse(ev);
+ } else {
+ const childLevel = this.buttonRef?.current?.nextElementSibling;
+ const firstSpaceItemChild = childLevel?.querySelector(".mx_SpaceItem");
+ firstSpaceItemChild?.querySelector(".mx_SpaceButton")?.focus();
+ }
+ }
+ break;
+
+ default:
+ handled = false;
+ }
+
+ if (handled) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ }
+ };
+
private onClick = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
@@ -300,27 +363,25 @@ export class SpaceItem extends React.PureComponent {
}
render() {
- const {space, activeSpaces, isNested} = this.props;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef,
+ ...otherProps } = this.props;
- const forceCollapsed = this.props.isPanelCollapsed;
- const isNarrow = this.props.isPanelCollapsed;
- const collapsed = this.state.collapsed || forceCollapsed;
+ const collapsed = this.isCollapsed;
- const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId)
- .filter(s => !this.props.parents?.has(s.roomId));
const isActive = activeSpaces.includes(space);
- const itemClasses = classNames({
+ const itemClasses = classNames(this.props.className, {
"mx_SpaceItem": true,
- "mx_SpaceItem_narrow": isNarrow,
+ "mx_SpaceItem_narrow": isPanelCollapsed,
"collapsed": collapsed,
- "hasSubSpaces": childSpaces && childSpaces.length,
+ "hasSubSpaces": this.state.childSpaces?.length,
});
const isInvite = space.getMyMembership() === "invite";
const classes = classNames("mx_SpaceButton", {
mx_SpaceButton_active: isActive,
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
- mx_SpaceButton_narrow: isNarrow,
+ mx_SpaceButton_narrow: isPanelCollapsed,
mx_SpaceButton_invite: isInvite,
});
const notificationState = isInvite
@@ -328,12 +389,12 @@ export class SpaceItem extends React.PureComponent {
: SpaceStore.instance.getNotificationState(space.roomId);
let childItems;
- if (childSpaces && !collapsed) {
+ if (this.state.childSpaces?.length && !collapsed) {
childItems = ;
}
@@ -346,53 +407,36 @@ export class SpaceItem extends React.PureComponent {
const avatarSize = isNested ? 24 : 32;
- const toggleCollapseButton = childSpaces && childSpaces.length ?
+ const toggleCollapseButton = this.state.childSpaces?.length ?
this.toggleCollapse(evt)}
+ onClick={this.toggleCollapse}
+ tabIndex={-1}
+ aria-label={collapsed ? _t("Expand") : _t("Collapse")}
/> : null;
- let button;
- if (isNarrow) {
- button = (
+ return (
+
{ toggleCollapseButton }
+ { !isPanelCollapsed && { space.name } }
{ notifBadge }
{ this.renderContextMenu() }
- );
- } else {
- button = (
-
- { toggleCollapseButton }
-
-
- { space.name }
- { notifBadge }
- { this.renderContextMenu() }
-
-
- );
- }
- return (
-
- { button }
{ childItems }
);
diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx
index c78f0c0fc8..d29caf789e 100644
--- a/src/components/views/voip/AudioFeed.tsx
+++ b/src/components/views/voip/AudioFeed.tsx
@@ -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 {
private element = createRef();
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 {
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;
diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts
new file mode 100644
index 0000000000..e778acf8a9
--- /dev/null
+++ b/src/hooks/useRoomState.ts
@@ -0,0 +1,46 @@
+/*
+Copyright 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.
+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 { useCallback, useEffect, useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomState } from "matrix-js-sdk/src/models/room-state";
+
+import { useEventEmitter } from "./useEventEmitter";
+
+type Mapper = (roomState: RoomState) => T;
+const defaultMapper: Mapper = (roomState: RoomState) => roomState;
+
+// Hook to simplify watching Matrix Room state
+export const useRoomState = (
+ room: Room,
+ mapper: Mapper = defaultMapper as Mapper,
+): T => {
+ const [value, setValue] = useState(room ? mapper(room.currentState) : undefined);
+
+ const update = useCallback(() => {
+ if (!room) return;
+ setValue(mapper(room.currentState));
+ }, [room, mapper]);
+
+ useEventEmitter(room?.currentState, "RoomState.events", update);
+ useEffect(() => {
+ update();
+ return () => {
+ setValue(undefined);
+ };
+ }, [update]);
+ return value;
+};
diff --git a/src/hooks/useStateToggle.ts b/src/hooks/useStateToggle.ts
index b50a923234..33701c4f16 100644
--- a/src/hooks/useStateToggle.ts
+++ b/src/hooks/useStateToggle.ts
@@ -18,7 +18,7 @@ import {Dispatch, SetStateAction, useState} from "react";
// Hook to simplify toggling of a boolean state value
// Returns value, method to toggle boolean value and method to set the boolean value
-export const useStateToggle = (initialValue: boolean): [boolean, () => void, Dispatch>] => {
+export const useStateToggle = (initialValue = false): [boolean, () => void, Dispatch>] => {
const [value, setValue] = useState(initialValue);
const toggleValue = () => {
setValue(!value);
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a94b608f2b..bc62868a0f 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -396,7 +396,8 @@
"Failed to invite": "Failed to invite",
"Operation failed": "Operation failed",
"Failed to invite users to the room:": "Failed to invite users to the room:",
- "Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
+ "We sent the others, but the below people couldn't be invited to ": "We sent the others, but the below people couldn't be invited to ",
+ "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.",
@@ -489,24 +490,27 @@
"Converts the room to a DM": "Converts the room to a DM",
"Converts the DM to a room": "Converts the DM to a room",
"Displays action": "Displays action",
- "Reason": "Reason",
- "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
- "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
- "%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.",
- "%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.",
- "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.",
- "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
- "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s removed their display name (%(oldDisplayName)s).",
- "%(senderName)s removed their profile picture.": "%(senderName)s removed their profile picture.",
- "%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
- "%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
- "%(senderName)s made no change.": "%(senderName)s made no change.",
- "%(targetName)s joined the room.": "%(targetName)s joined the room.",
- "%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.",
- "%(targetName)s left the room.": "%(targetName)s left the room.",
- "%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.",
- "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.",
- "%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.",
+ "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepted the invitation for %(displayName)s",
+ "%(targetName)s accepted an invitation": "%(targetName)s accepted an invitation",
+ "%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
+ "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s banned %(targetName)s: %(reason)s",
+ "%(senderName)s banned %(targetName)s": "%(senderName)s banned %(targetName)s",
+ "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s changed their display name to %(displayName)s",
+ "%(senderName)s set their display name to %(displayName)s": "%(senderName)s set their display name to %(displayName)s",
+ "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s removed their display name (%(oldDisplayName)s)",
+ "%(senderName)s removed their profile picture": "%(senderName)s removed their profile picture",
+ "%(senderName)s changed their profile picture": "%(senderName)s changed their profile picture",
+ "%(senderName)s set a profile picture": "%(senderName)s set a profile picture",
+ "%(senderName)s made no change": "%(senderName)s made no change",
+ "%(targetName)s joined the room": "%(targetName)s joined the room",
+ "%(targetName)s rejected the invitation": "%(targetName)s rejected the invitation",
+ "%(targetName)s left the room: %(reason)s": "%(targetName)s left the room: %(reason)s",
+ "%(targetName)s left the room": "%(targetName)s left the room",
+ "%(senderName)s unbanned %(targetName)s": "%(senderName)s unbanned %(targetName)s",
+ "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s",
+ "%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s withdrew %(targetName)s's invitation",
+ "%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s kicked %(targetName)s: %(reason)s",
+ "%(senderName)s kicked %(targetName)s": "%(senderName)s kicked %(targetName)s",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.",
@@ -1023,18 +1027,42 @@
"Your private space": "Your private space",
"Add some details to help people recognise it.": "Add some details to help people recognise it.",
"You can change these anytime.": "You can change these anytime.",
+ "e.g. my-space": "e.g. my-space",
+ "Address": "Address",
"Creating...": "Creating...",
"Create": "Create",
- "Expand space panel": "Expand space panel",
- "Collapse space panel": "Collapse space panel",
"All rooms": "All rooms",
"Home": "Home",
+ "Expand space panel": "Expand space panel",
+ "Collapse space panel": "Collapse space panel",
"Click to copy": "Click to copy",
"Copied!": "Copied!",
"Failed to copy": "Failed to copy",
"Share invite link": "Share invite link",
"Invite people": "Invite people",
"Invite with email or username": "Invite with email or username",
+ "Failed to save space settings.": "Failed to save space settings.",
+ "General": "General",
+ "Edit settings relating to your space.": "Edit settings relating to your space.",
+ "Saving...": "Saving...",
+ "Save Changes": "Save Changes",
+ "Leave Space": "Leave Space",
+ "Failed to update the visibility of this space": "Failed to update the visibility of this space",
+ "Failed to update the guest access of this space": "Failed to update the guest access of this space",
+ "Failed to update the history visibility of this space": "Failed to update the history visibility of this space",
+ "Hide advanced": "Hide advanced",
+ "Enable guest access": "Enable guest access",
+ "Guests can join a space without having an account.": "Guests can join a space without having an account.",
+ "This may be useful for public spaces.": "This may be useful for public spaces.",
+ "Show advanced": "Show advanced",
+ "Visibility": "Visibility",
+ "Decide who can view and join %(spaceName)s.": "Decide who can view and join %(spaceName)s.",
+ "anyone with the link can view and join": "anyone with the link can view and join",
+ "Invite only": "Invite only",
+ "only invited people can view and join": "only invited people can view and join",
+ "Preview Space": "Preview Space",
+ "Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
+ "Recommended for public spaces.": "Recommended for public spaces.",
"Settings": "Settings",
"Leave space": "Leave space",
"Create new room": "Create new room",
@@ -1043,6 +1071,8 @@
"Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms",
"Space options": "Space options",
+ "Expand": "Expand",
+ "Collapse": "Collapse",
"Remove": "Remove",
"This bridge was provisioned by .": "This bridge was provisioned by .",
"This bridge is managed by .": "This bridge is managed by .",
@@ -1229,8 +1259,6 @@
"Custom theme URL": "Custom theme URL",
"Add theme": "Add theme",
"Theme": "Theme",
- "Hide advanced": "Hide advanced",
- "Show advanced": "Show advanced",
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
"Customise your appearance": "Customise your appearance",
@@ -1251,7 +1279,6 @@
"Deactivate Account": "Deactivate Account",
"Deactivate account": "Deactivate account",
"Discovery": "Discovery",
- "General": "General",
"Legal": "Legal",
"Credits": "Credits",
"For help with using %(brand)s, click here .": "For help with using %(brand)s, click here .",
@@ -1357,6 +1384,7 @@
"Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version",
"this room": "this room",
"View older messages in %(roomName)s.": "View older messages in %(roomName)s.",
+ "Space information": "Space information",
"Room information": "Room information",
"Internal room ID:": "Internal room ID:",
"Room version": "Room version",
@@ -1386,6 +1414,7 @@
"Failed to unban": "Failed to unban",
"Unban": "Unban",
"Banned by %(displayName)s": "Banned by %(displayName)s",
+ "Reason": "Reason",
"Error changing power level requirement": "Error changing power level requirement",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.",
"Error changing power level": "Error changing power level",
@@ -1682,14 +1711,18 @@
"Error removing address": "Error removing address",
"Main address": "Main address",
"not specified": "not specified",
+ "This space has no local addresses": "This space has no local addresses",
"This room has no local addresses": "This room has no local addresses",
"Local address": "Local address",
"Published Addresses": "Published Addresses",
- "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.",
+ "Published addresses can be used by anyone on any server to join your space.": "Published addresses can be used by anyone on any server to join your space.",
+ "Published addresses can be used by anyone on any server to join your room.": "Published addresses can be used by anyone on any server to join your room.",
+ "To publish an address, it needs to be set as a local address first.": "To publish an address, it needs to be set as a local address first.",
"Other published addresses:": "Other published addresses:",
"No other published addresses yet, add one below": "No other published addresses yet, add one below",
"New published address (e.g. #alias:server)": "New published address (e.g. #alias:server)",
"Local Addresses": "Local Addresses",
+ "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)",
"Show more": "Show more",
"Error updating flair": "Error updating flair",
@@ -2026,7 +2059,7 @@
"Room address": "Room address",
"e.g. my-room": "e.g. my-room",
"Some characters not allowed": "Some characters not allowed",
- "Please provide a room address": "Please provide a room address",
+ "Please provide an address": "Please provide an address",
"This address is available to use": "This address is available to use",
"This address is already in use": "This address is already in use",
"Server Options": "Server Options",
@@ -2249,7 +2282,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.",
@@ -2403,14 +2435,8 @@
"Share Room Message": "Share Room Message",
"Link to selected message": "Link to selected message",
"Command Help": "Command Help",
- "Failed to save space settings.": "Failed to save space settings.",
"Space settings": "Space settings",
- "Edit settings relating to your space.": "Edit settings relating to your space.",
- "Make this space private": "Make this space private",
- "Leave Space": "Leave Space",
- "View dev tools": "View dev tools",
- "Saving...": "Saving...",
- "Save Changes": "Save Changes",
+ "Settings - %(spaceName)s": "Settings - %(spaceName)s",
"To help us prevent this in future, please send us logs .": "To help us prevent this in future, please send us logs .",
"Missing session data": "Missing session data",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
@@ -2511,6 +2537,8 @@
"Update status": "Update status",
"Set status": "Set status",
"Set a new status...": "Set a new status...",
+ "Move up": "Move up",
+ "Move down": "Move down",
"View Community": "View Community",
"Unable to start audio streaming.": "Unable to start audio streaming.",
"Failed to start livestream": "Failed to start livestream",
@@ -2657,7 +2685,7 @@
"%(count)s messages deleted.|one": "%(count)s message deleted.",
"Your Communities": "Your Communities",
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
- "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
+ "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
"Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts
index debcb213ca..9478b2987b 100644
--- a/src/indexing/BaseEventIndexManager.ts
+++ b/src/indexing/BaseEventIndexManager.ts
@@ -17,7 +17,7 @@ limitations under the License.
// The following interfaces take their names and member names from seshat and the spec
/* eslint-disable camelcase */
-export interface MatrixEvent {
+export interface IMatrixEvent {
type: string;
sender: string;
content: {};
@@ -27,37 +27,37 @@ export interface MatrixEvent {
roomId: string;
}
-export interface MatrixProfile {
+export interface IMatrixProfile {
avatar_url: string;
displayname: string;
}
-export interface CrawlerCheckpoint {
+export interface ICrawlerCheckpoint {
roomId: string;
token: string;
fullCrawl?: boolean;
direction: string;
}
-export interface ResultContext {
- events_before: [MatrixEvent];
- events_after: [MatrixEvent];
- profile_info: Map;
+export interface IResultContext {
+ events_before: [IMatrixEvent];
+ events_after: [IMatrixEvent];
+ profile_info: Map;
}
-export interface ResultsElement {
+export interface IResultsElement {
rank: number;
- result: MatrixEvent;
- context: ResultContext;
+ result: IMatrixEvent;
+ context: IResultContext;
}
-export interface SearchResult {
+export interface ISearchResult {
count: number;
- results: [ResultsElement];
+ results: [IResultsElement];
highlights: [string];
}
-export interface SearchArgs {
+export interface ISearchArgs {
search_term: string;
before_limit: number;
after_limit: number;
@@ -65,19 +65,19 @@ export interface SearchArgs {
room_id?: string;
}
-export interface EventAndProfile {
- event: MatrixEvent;
- profile: MatrixProfile;
+export interface IEventAndProfile {
+ event: IMatrixEvent;
+ profile: IMatrixProfile;
}
-export interface LoadArgs {
+export interface ILoadArgs {
roomId: string;
limit: number;
fromEvent?: string;
direction?: string;
}
-export interface IndexStats {
+export interface IIndexStats {
size: number;
eventCount: number;
roomCount: number;
@@ -119,13 +119,13 @@ export default abstract class BaseEventIndexManager {
* Queue up an event to be added to the index.
*
* @param {MatrixEvent} ev The event that should be added to the index.
- * @param {MatrixProfile} profile The profile of the event sender at the
+ * @param {IMatrixProfile} profile The profile of the event sender at the
* time of the event receival.
*
* @return {Promise} A promise that will resolve when the was queued up for
* addition.
*/
- async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise {
+ async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise {
throw new Error("Unimplemented");
}
@@ -160,10 +160,10 @@ export default abstract class BaseEventIndexManager {
/**
* Get statistical information of the index.
*
- * @return {Promise} A promise that will resolve to the index
+ * @return {Promise} A promise that will resolve to the index
* statistics.
*/
- async getStats(): Promise {
+ async getStats(): Promise {
throw new Error("Unimplemented");
}
@@ -203,13 +203,13 @@ export default abstract class BaseEventIndexManager {
/**
* Search the event index using the given term for matching events.
*
- * @param {SearchArgs} searchArgs The search configuration for the search,
+ * @param {ISearchArgs} searchArgs The search configuration for the search,
* sets the search term and determines the search result contents.
*
- * @return {Promise<[SearchResult]>} A promise that will resolve to an array
+ * @return {Promise<[ISearchResult]>} A promise that will resolve to an array
* of search results once the search is done.
*/
- async searchEventIndex(searchArgs: SearchArgs): Promise {
+ async searchEventIndex(searchArgs: ISearchArgs): Promise {
throw new Error("Unimplemented");
}
@@ -218,12 +218,12 @@ export default abstract class BaseEventIndexManager {
*
* This is used to add a batch of events to the index.
*
- * @param {[EventAndProfile]} events The list of events and profiles that
+ * @param {[IEventAndProfile]} events The list of events and profiles that
* should be added to the event index.
- * @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that
+ * @param {[ICrawlerCheckpoint]} checkpoint A new crawler checkpoint that
* should be stored in the index which should be used to continue crawling
* the room.
- * @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used
+ * @param {[ICrawlerCheckpoint]} oldCheckpoint The checkpoint that was used
* to fetch the current batch of events. This checkpoint will be removed
* from the index.
*
@@ -231,9 +231,9 @@ export default abstract class BaseEventIndexManager {
* were already added to the index, false otherwise.
*/
async addHistoricEvents(
- events: [EventAndProfile],
- checkpoint: CrawlerCheckpoint | null,
- oldCheckpoint: CrawlerCheckpoint | null,
+ events: IEventAndProfile[],
+ checkpoint: ICrawlerCheckpoint | null,
+ oldCheckpoint: ICrawlerCheckpoint | null,
): Promise {
throw new Error("Unimplemented");
}
@@ -241,36 +241,36 @@ export default abstract class BaseEventIndexManager {
/**
* Add a new crawler checkpoint to the index.
*
- * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added
+ * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be added
* to the index.
*
* @return {Promise} A promise that will resolve once the checkpoint has
* been stored.
*/
- async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise {
+ async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise {
throw new Error("Unimplemented");
}
/**
* Add a new crawler checkpoint to the index.
*
- * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be
+ * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be
* removed from the index.
*
* @return {Promise} A promise that will resolve once the checkpoint has
* been removed.
*/
- async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise {
+ async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise {
throw new Error("Unimplemented");
}
/**
* Load the stored checkpoints from the index.
*
- * @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an
+ * @return {Promise<[ICrawlerCheckpoint]>} A promise that will resolve to an
* array of crawler checkpoints once they have been loaded from the index.
*/
- async loadCheckpoints(): Promise<[CrawlerCheckpoint]> {
+ async loadCheckpoints(): Promise {
throw new Error("Unimplemented");
}
@@ -286,11 +286,11 @@ export default abstract class BaseEventIndexManager {
* @param {string} args.direction The direction to which we should continue
* loading events from. This is used only if fromEvent is used as well.
*
- * @return {Promise<[EventAndProfile]>} A promise that will resolve to an
+ * @return {Promise<[IEventAndProfile]>} A promise that will resolve to an
* array of Matrix events that contain mxc URLs accompanied with the
* historic profile of the sender.
*/
- async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> {
+ async loadFileEvents(args: ILoadArgs): Promise {
throw new Error("Unimplemented");
}
diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts
index c36f96f368..978a2ac813 100644
--- a/src/indexing/EventIndex.ts
+++ b/src/indexing/EventIndex.ts
@@ -28,7 +28,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
import { sleep } from "../utils/promise";
import SettingsStore from "../settings/SettingsStore";
import { SettingLevel } from "../settings/SettingLevel";
-import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager";
+import { ICrawlerCheckpoint, ILoadArgs, ISearchArgs } from "./BaseEventIndexManager";
// The time in ms that the crawler will wait loop iterations if there
// have not been any checkpoints to consume in the last iteration.
@@ -45,9 +45,9 @@ interface ICrawler {
* Event indexing class that wraps the platform specific event indexing.
*/
export default class EventIndex extends EventEmitter {
- private crawlerCheckpoints: CrawlerCheckpoint[] = [];
+ private crawlerCheckpoints: ICrawlerCheckpoint[] = [];
private crawler: ICrawler = null;
- private currentCheckpoint: CrawlerCheckpoint = null;
+ private currentCheckpoint: ICrawlerCheckpoint = null;
public async init() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
@@ -111,14 +111,14 @@ export default class EventIndex extends EventEmitter {
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
- const backCheckpoint: CrawlerCheckpoint = {
+ const backCheckpoint: ICrawlerCheckpoint = {
roomId: room.roomId,
token: token,
direction: "b",
fullCrawl: true,
};
- const forwardCheckpoint: CrawlerCheckpoint = {
+ const forwardCheckpoint: ICrawlerCheckpoint = {
roomId: room.roomId,
token: token,
direction: "f",
@@ -668,13 +668,13 @@ export default class EventIndex extends EventEmitter {
/**
* Search the event index using the given term for matching events.
*
- * @param {SearchArgs} searchArgs The search configuration for the search,
+ * @param {ISearchArgs} searchArgs The search configuration for the search,
* sets the search term and determines the search result contents.
*
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
* of search results once the search is done.
*/
- public async search(searchArgs: SearchArgs) {
+ public async search(searchArgs: ISearchArgs) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.searchEventIndex(searchArgs);
}
@@ -709,7 +709,7 @@ export default class EventIndex extends EventEmitter {
const client = MatrixClientPeg.get();
const indexManager = PlatformPeg.get().getEventIndexingManager();
- const loadArgs: LoadArgs = {
+ const loadArgs: ILoadArgs = {
roomId: room.roomId,
limit: limit,
};
diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts
index 859fdf046a..57d60514da 100644
--- a/src/rageshake/submit-rageshake.ts
+++ b/src/rageshake/submit-rageshake.ts
@@ -86,8 +86,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
body.append('cross_signing_key', client.getCrossSigningId());
// add cross-signing status information
- const crossSigning = client.crypto._crossSigningInfo;
- const secretStorage = client.crypto._secretStorage;
+ const crossSigning = client.crypto.crossSigningInfo;
+ const secretStorage = client.crypto.secretStorage;
body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
body.append("cross_signing_supported_by_hs",
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index f11589485a..e498574467 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -36,6 +36,8 @@ import RoomViewStore from "./RoomViewStore";
import { Action } from "../dispatcher/actions";
import { arrayHasDiff } from "../utils/arrays";
import { objectDiff } from "../utils/objects";
+import { arrayHasOrderChange } from "../utils/arrays";
+import { reorderLexicographically } from "../utils/stringOrderField";
type SpaceKey = string | symbol;
@@ -67,18 +69,18 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces,
}, [[], []]);
};
-// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
-export const getOrder = (order: string, creationTs: number, roomId: string): Array>> => {
- let validatedOrder: string = null;
-
- if (typeof order === "string" && Array.from(order).every((c: string) => {
+const validOrder = (order: string): string | undefined => {
+ if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => {
const charCode = c.charCodeAt(0);
return charCode >= 0x20 && charCode <= 0x7E;
})) {
- validatedOrder = order;
+ return order;
}
+};
- return [validatedOrder, creationTs, roomId];
+// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
+export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => {
+ return [validOrder(order), creationTs, roomId];
}
const getRoomFn: FetchRoomFn = (room: Room) => {
@@ -104,6 +106,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
private _activeSpace?: Room = null;
private _suggestedRooms: ISuggestedRoom[] = [];
private _invitedSpaces = new Set();
+ private spaceOrderLocalEchoMap = new Map();
public get invitedSpaces(): Room[] {
return Array.from(this._invitedSpaces);
@@ -223,7 +226,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const roomId = ev.getStateKey();
const childRoom = this.matrixClient?.getRoom(roomId);
const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs();
- return getOrder(ev.getContent().order, createTs, roomId);
+ return getChildOrder(ev.getContent().order, createTs, roomId);
}).map(ev => {
return this.matrixClient.getRoom(ev.getStateKey());
}).filter(room => {
@@ -336,7 +339,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// });
this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId));
- this.rootSpaces = rootSpaces;
+ this.rootSpaces = this.sortRootSpaces(rootSpaces);
this.parentMap = backrefs;
// if the currently selected space no longer exists, remove its selection
@@ -348,7 +351,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
- this._invitedSpaces = new Set(invitedSpaces);
+ this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces));
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
}, 100, {trailing: true, leading: true});
@@ -524,6 +527,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
};
+ private notifyIfOrderChanged(): void {
+ const rootSpaces = this.sortRootSpaces(this.rootSpaces);
+ if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) {
+ this.rootSpaces = rootSpaces;
+ this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
+ }
+ }
+
private onRoomState = (ev: MatrixEvent) => {
const room = this.matrixClient.getRoom(ev.getRoomId());
if (!room) return;
@@ -555,10 +566,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
};
- private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => {
- if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) {
+ private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => {
+ if (!room.isSpaceRoom()) return;
+
+ if (ev.getType() === EventType.SpaceOrder) {
+ this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo
+ const order = ev.getContent()?.order;
+ const lastOrder = lastEv?.getContent()?.order;
+ if (order !== lastOrder) {
+ this.notifyIfOrderChanged();
+ }
+ } else if (ev.getType() === EventType.Tag && !SettingsStore.getValue("feature_spaces.all_rooms")) {
// If the room was in favourites and now isn't or the opposite then update its position in the trees
- const oldTags = lastEvent?.getContent()?.tags || {};
+ const oldTags = lastEv?.getContent()?.tags || {};
const newTags = ev.getContent()?.tags || {};
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
this.onRoomUpdate(room);
@@ -600,9 +620,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (this.matrixClient) {
this.matrixClient.removeListener("Room", this.onRoom);
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
+ this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
- this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
this.matrixClient.removeListener("accountData", this.onAccountData);
}
}
@@ -613,9 +633,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (!SettingsStore.getValue("feature_spaces")) return;
this.matrixClient.on("Room", this.onRoom);
this.matrixClient.on("Room.myMembership", this.onRoom);
+ this.matrixClient.on("Room.accountData", this.onRoomAccountData);
this.matrixClient.on("RoomState.events", this.onRoomState);
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
- this.matrixClient.on("Room.accountData", this.onRoomAccountData);
this.matrixClient.on("accountData", this.onAccountData);
}
@@ -700,6 +720,38 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath));
}
+
+ private getSpaceTagOrdering = (space: Room): string | undefined => {
+ if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId);
+ return validOrder(space.getAccountData(EventType.SpaceOrder)?.getContent()?.order);
+ };
+
+ private sortRootSpaces(spaces: Room[]): Room[] {
+ return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]);
+ }
+
+ private async setRootSpaceOrder(space: Room, order: string): Promise {
+ this.spaceOrderLocalEchoMap.set(space.roomId, order);
+ try {
+ await this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order });
+ } catch (e) {
+ console.warn("Failed to set root space order", e);
+ if (this.spaceOrderLocalEchoMap.get(space.roomId) === order) {
+ this.spaceOrderLocalEchoMap.delete(space.roomId);
+ }
+ }
+ }
+
+ public moveRootSpace(fromIndex: number, toIndex: number): void {
+ const currentOrders = this.rootSpaces.map(this.getSpaceTagOrdering);
+ const changes = reorderLexicographically(currentOrders, fromIndex, toIndex);
+
+ changes.forEach(({ index, order }) => {
+ this.setRootSpaceOrder(this.rootSpaces[index], order);
+ });
+
+ this.notifyIfOrderChanged();
+ }
}
export default class SpaceStore {
diff --git a/src/utils/EditorStateTransfer.ts b/src/utils/EditorStateTransfer.ts
index 42e1a316d6..ba303f9b73 100644
--- a/src/utils/EditorStateTransfer.ts
+++ b/src/utils/EditorStateTransfer.ts
@@ -30,24 +30,24 @@ export default class EditorStateTransfer {
constructor(private readonly event: MatrixEvent) {}
- setEditorState(caret: Caret, serializedParts: SerializedPart[]) {
+ public setEditorState(caret: Caret, serializedParts: SerializedPart[]) {
this.caret = caret;
this.serializedParts = serializedParts;
}
- hasEditorState() {
+ public hasEditorState() {
return !!this.serializedParts;
}
- getSerializedParts() {
+ public getSerializedParts() {
return this.serializedParts;
}
- getCaret() {
+ public getCaret() {
return this.caret;
}
- getEvent() {
+ public getEvent() {
return this.event;
}
}
diff --git a/src/utils/MessageDiffUtils.tsx b/src/utils/MessageDiffUtils.tsx
index b5d5e31432..5ee9970ec2 100644
--- a/src/utils/MessageDiffUtils.tsx
+++ b/src/utils/MessageDiffUtils.tsx
@@ -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") {
diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.ts
similarity index 66%
rename from src/utils/MultiInviter.js
rename to src/utils/MultiInviter.ts
index 78f956b91b..f6a994484e 100644
--- a/src/utils/MultiInviter.js
+++ b/src/utils/MultiInviter.ts
@@ -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;
/**
* 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 = {}; // { address: {errorText, errcode} }
+ private deferred: IDeferred = 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 {
+ 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();
+ 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 {
+ return new Promise((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));
}
}
diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts
index e527f43c29..6524debfb7 100644
--- a/src/utils/arrays.ts
+++ b/src/utils/arrays.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {percentageOf, percentageWithin} from "./numbers";
+import { percentageOf, percentageWithin } from "./numbers";
/**
* Quickly resample an array to have less/more data points. If an input which is larger
@@ -223,6 +223,21 @@ export function arrayMerge(...a: T[][]): T[] {
}, new Set()));
}
+/**
+ * Moves a single element from fromIndex to toIndex.
+ * @param {array} list the list from which to construct the new list.
+ * @param {number} fromIndex the index of the element to move.
+ * @param {number} toIndex the index of where to put the element.
+ * @returns {array} A new array with the requested value moved.
+ */
+export function moveElement(list: T[], fromIndex: number, toIndex: number): T[] {
+ const result = Array.from(list);
+ const [removed] = result.splice(fromIndex, 1);
+ result.splice(toIndex, 0, removed);
+
+ return result;
+}
+
/**
* Helper functions to perform LINQ-like queries on arrays.
*/
diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts
new file mode 100644
index 0000000000..da840792ee
--- /dev/null
+++ b/src/utils/stringOrderField.ts
@@ -0,0 +1,148 @@
+/*
+Copyright 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.
+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 { alphabetPad, baseToString, stringToBase, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
+
+import { moveElement } from "./arrays";
+
+export function midPointsBetweenStrings(
+ a: string,
+ b: string,
+ count: number,
+ maxLen: number,
+ alphabet = DEFAULT_ALPHABET,
+): string[] {
+ const padN = Math.min(Math.max(a.length, b.length), maxLen);
+ const padA = alphabetPad(a, padN, alphabet);
+ const padB = alphabetPad(b, padN, alphabet);
+ const baseA = stringToBase(padA, alphabet);
+ const baseB = stringToBase(padB, alphabet);
+
+ if (baseB - baseA - BigInt(1) < count) {
+ if (padN < maxLen) {
+ // this recurses once at most due to the new limit of n+1
+ return midPointsBetweenStrings(
+ alphabetPad(padA, padN + 1, alphabet),
+ alphabetPad(padB, padN + 1, alphabet),
+ count,
+ padN + 1,
+ alphabet,
+ );
+ }
+ return [];
+ }
+
+ const step = (baseB - baseA) / BigInt(count + 1);
+ const start = BigInt(baseA + step);
+ return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet));
+}
+
+interface IEntry {
+ index: number;
+ order: string;
+}
+
+export const reorderLexicographically = (
+ orders: Array,
+ fromIndex: number,
+ toIndex: number,
+ maxLen = 50,
+): IEntry[] => {
+ // sanity check inputs
+ if (
+ fromIndex < 0 || toIndex < 0 ||
+ fromIndex > orders.length || toIndex > orders.length ||
+ fromIndex === toIndex
+ ) {
+ return [];
+ }
+
+ // zip orders with their indices to simplify later index wrangling
+ const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order }));
+ // apply the fundamental order update to the zipped array
+ const newOrder = moveElement(ordersWithIndices, fromIndex, toIndex);
+
+ // check if we have to fill undefined orders to complete placement
+ const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined;
+
+ let leftBoundIdx = toIndex;
+ let rightBoundIdx = toIndex;
+
+ let canMoveLeft = true;
+ const nextBase = newOrder[toIndex + 1]?.order !== undefined
+ ? stringToBase(newOrder[toIndex + 1].order)
+ : BigInt(Number.MAX_VALUE);
+
+ // check how far left we would have to mutate to fit in that direction
+ for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) {
+ if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break;
+ leftBoundIdx = i;
+ }
+
+ // verify the left move would be sufficient
+ const firstOrderBase = newOrder[0].order === undefined ? undefined : stringToBase(newOrder[0].order);
+ const bigToIndex = BigInt(toIndex);
+ if (leftBoundIdx === 0 &&
+ firstOrderBase !== undefined &&
+ nextBase - firstOrderBase <= bigToIndex &&
+ firstOrderBase <= bigToIndex
+ ) {
+ canMoveLeft = false;
+ }
+
+ const canDisplaceRight = !orderToLeftUndefined;
+ let canMoveRight = canDisplaceRight;
+ if (canDisplaceRight) {
+ const prevBase = newOrder[toIndex - 1]?.order !== undefined
+ ? stringToBase(newOrder[toIndex - 1]?.order)
+ : BigInt(Number.MIN_VALUE);
+
+ // check how far right we would have to mutate to fit in that direction
+ for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) {
+ if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break;
+ rightBoundIdx = i;
+ }
+
+ // verify the right move would be sufficient
+ if (rightBoundIdx === newOrder.length - 1 &&
+ (newOrder[rightBoundIdx]
+ ? stringToBase(newOrder[rightBoundIdx].order)
+ : BigInt(Number.MAX_VALUE)) - prevBase <= (rightBoundIdx - toIndex)
+ ) {
+ canMoveRight = false;
+ }
+ }
+
+ // pick the cheaper direction
+ const leftDiff = canMoveLeft ? toIndex - leftBoundIdx : Number.MAX_SAFE_INTEGER;
+ const rightDiff = canMoveRight ? rightBoundIdx - toIndex : Number.MAX_SAFE_INTEGER;
+ if (orderToLeftUndefined || leftDiff < rightDiff) {
+ rightBoundIdx = toIndex;
+ } else {
+ leftBoundIdx = toIndex;
+ }
+
+ const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? "";
+ const nextOrder = newOrder[rightBoundIdx + 1]?.order
+ ?? DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1).repeat(prevOrder.length || 1);
+
+ const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen);
+
+ return changes.map((order, i) => ({
+ index: newOrder[leftBoundIdx + i].index,
+ order,
+ }));
+};
diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts
index fde5779fa2..8f9e03bb8e 100644
--- a/src/voice/VoiceRecording.ts
+++ b/src/voice/VoiceRecording.ts
@@ -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({
diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.tsx
similarity index 88%
rename from test/components/views/rooms/MemberList-test.js
rename to test/components/views/rooms/MemberList-test.tsx
index 28fead770c..8012c43c4b 100644
--- a/test/components/views/rooms/MemberList-test.js
+++ b/test/components/views/rooms/MemberList-test.tsx
@@ -1,21 +1,36 @@
+/*
+Copyright 2021 Šimon Brandner
+
+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( , parentDiv);
+ root = ReactDOM.render(
+ (
+
+ ),
+ 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);
diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js
index bfb8e1afd4..6aad6a90fd 100644
--- a/test/components/views/rooms/RoomList-test.js
+++ b/test/components/views/rooms/RoomList-test.js
@@ -6,7 +6,6 @@ import * as TestUtils from '../../../test-utils';
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
import sdk from '../../../skinned-sdk';
-import { DragDropContext } from 'react-beautiful-dnd';
import dis from '../../../../src/dispatcher/dispatcher';
import DMRoomMap from '../../../../src/utils/DMRoomMap';
@@ -68,9 +67,7 @@ describe('RoomList', () => {
const RoomList = sdk.getComponent('views.rooms.RoomList');
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
root = ReactDOM.render(
-
- {}} />
- ,
+ {}} />,
parentDiv,
);
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js
index abd4488db2..654c461296 100644
--- a/test/end-to-end-tests/src/usecases/room-settings.js
+++ b/test/end-to-end-tests/src/usecases/room-settings.js
@@ -140,8 +140,6 @@ async function changeRoomSettings(session, settings) {
if (settings.alias) {
session.log.step(`sets alias to ${settings.alias}`);
- const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
- await summary.click();
const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details input[type=text]");
await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":")));
const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details .mx_AccessibleButton");
diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts
new file mode 100644
index 0000000000..331627dfc0
--- /dev/null
+++ b/test/utils/stringOrderField-test.ts
@@ -0,0 +1,291 @@
+/*
+Copyright 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.
+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 { sortBy } from "lodash";
+import { averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
+
+import { midPointsBetweenStrings, reorderLexicographically } from "../../src/utils/stringOrderField";
+
+const moveLexicographicallyTest = (
+ orders: Array,
+ fromIndex: number,
+ toIndex: number,
+ expectedChanges: number,
+ maxLength?: number,
+): void => {
+ const ops = reorderLexicographically(orders, fromIndex, toIndex, maxLength);
+
+ const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]);
+ ops.forEach(({ index, order }) => {
+ zipped[index][1] = order;
+ });
+
+ const newOrders = sortBy(zipped, i => i[1]);
+ expect(newOrders[toIndex][0]).toBe(fromIndex);
+ expect(ops).toHaveLength(expectedChanges);
+};
+
+describe("stringOrderField", () => {
+ describe("midPointsBetweenStrings", () => {
+ it("should work", () => {
+ expect(averageBetweenStrings("!!", "##")).toBe('""');
+ const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort();
+ expect(midpoints[0]).toBe("a");
+ expect(midpoints[4]).toBe("e");
+ expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]);
+ });
+
+ it("should return empty array when the request is not possible", () => {
+ expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]);
+ expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]);
+ });
+ });
+
+ describe("reorderLexicographically", () => {
+ it("should work when moving left", () => {
+ moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1);
+ });
+
+ it("should work when moving right", () => {
+ moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1);
+ });
+
+ it("should work when all orders are undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, undefined],
+ 4,
+ 1,
+ 2,
+ );
+ });
+
+ it("should work when moving to end and all orders are undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, undefined],
+ 1,
+ 4,
+ 5,
+ );
+ });
+
+ it("should work when moving left and some orders are undefined", () => {
+ moveLexicographicallyTest(
+ ["a", "c", "e", undefined, undefined, undefined],
+ 5,
+ 2,
+ 1,
+ );
+
+ moveLexicographicallyTest(
+ ["a", "a", "e", undefined, undefined, undefined],
+ 5,
+ 1,
+ 2,
+ );
+ });
+
+ it("should work moving to the start when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined],
+ 2,
+ 0,
+ 1,
+ );
+ });
+
+ it("should work moving to the end when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined],
+ 1,
+ 3,
+ 4,
+ );
+ });
+
+ it("should work moving left when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, undefined],
+ 4,
+ 1,
+ 2,
+ );
+ });
+
+ it("should work moving right when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined],
+ 1,
+ 2,
+ 3,
+ );
+ });
+
+ it("should work moving more right when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined],
+ 1,
+ 4,
+ 5,
+ );
+ });
+
+ it("should work moving left when right is undefined", () => {
+ moveLexicographicallyTest(
+ ["20", undefined, undefined, undefined, undefined, undefined],
+ 4,
+ 2,
+ 2,
+ );
+ });
+
+ it("should work moving right when right is undefined", () => {
+ moveLexicographicallyTest(
+ ["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined],
+ 1,
+ 4,
+ 4,
+ );
+ });
+
+ it("should work moving left when right is defined", () => {
+ moveLexicographicallyTest(
+ ["10", "20", "30", "40", undefined, undefined],
+ 3,
+ 1,
+ 1,
+ );
+ });
+
+ it("should work moving right when right is defined", () => {
+ moveLexicographicallyTest(
+ ["10", "20", "30", "40", "50", undefined],
+ 1,
+ 3,
+ 1,
+ );
+ });
+
+ it("should work moving left when all is defined", () => {
+ moveLexicographicallyTest(
+ ["11", "13", "15", "17", "19"],
+ 2,
+ 1,
+ 1,
+ );
+ });
+
+ it("should work moving right when all is defined", () => {
+ moveLexicographicallyTest(
+ ["11", "13", "15", "17", "19"],
+ 1,
+ 2,
+ 1,
+ );
+ });
+
+ it("should work moving left into no left space", () => {
+ moveLexicographicallyTest(
+ ["11", "12", "13", "14", "19"],
+ 3,
+ 1,
+ 2,
+ 2,
+ );
+
+ moveLexicographicallyTest(
+ [
+ DEFAULT_ALPHABET.charAt(0),
+ // Target
+ DEFAULT_ALPHABET.charAt(1),
+ DEFAULT_ALPHABET.charAt(2),
+ DEFAULT_ALPHABET.charAt(3),
+ DEFAULT_ALPHABET.charAt(4),
+ DEFAULT_ALPHABET.charAt(5),
+ ],
+ 5,
+ 1,
+ 5,
+ 1,
+ );
+ });
+
+ it("should work moving right into no right space", () => {
+ moveLexicographicallyTest(
+ ["15", "16", "17", "18", "19"],
+ 1,
+ 3,
+ 3,
+ 2,
+ );
+
+ moveLexicographicallyTest(
+ [
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
+ ],
+ 1,
+ 3,
+ 3,
+ 1,
+ );
+ });
+
+ it("should work moving right into no left space", () => {
+ moveLexicographicallyTest(
+ ["11", "12", "13", "14", "15", "16", undefined],
+ 1,
+ 3,
+ 3,
+ );
+
+ moveLexicographicallyTest(
+ ["0", "1", "2", "3", "4", "5"],
+ 1,
+ 3,
+ 3,
+ 1,
+ );
+ });
+
+ it("should work moving left into no right space", () => {
+ moveLexicographicallyTest(
+ ["15", "16", "17", "18", "19"],
+ 4,
+ 3,
+ 4,
+ 2,
+ );
+
+ moveLexicographicallyTest(
+ [
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
+ ],
+ 4,
+ 3,
+ 4,
+ 1,
+ );
+ });
+ });
+});
+
diff --git a/yarn.lock b/yarn.lock
index 5bd409e612..3bcb8de404 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1017,13 +1017,20 @@
pirates "^4.0.0"
source-map-support "^0.5.16"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2":
+ version "7.14.6"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
+ integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
@@ -1327,6 +1334,7 @@
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
version "3.2.3"
+ uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
@@ -1509,6 +1517,14 @@
dependencies:
"@types/node" "*"
+"@types/hoist-non-react-statics@^3.3.0":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+ integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+ dependencies:
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -1625,12 +1641,29 @@
dependencies:
"@types/node" "*"
-"@types/react-dom@^16.9.10":
- version "16.9.10"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f"
- integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw==
+"@types/react-beautiful-dnd@^13.0.0":
+ version "13.0.0"
+ resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4"
+ integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==
dependencies:
- "@types/react" "^16"
+ "@types/react" "*"
+
+"@types/react-dom@^17.0.2":
+ version "17.0.8"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.8.tgz#3180de6d79bf53762001ad854e3ce49f36dd71fc"
+ integrity sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A==
+ dependencies:
+ "@types/react" "*"
+
+"@types/react-redux@^7.1.16":
+ version "7.1.16"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21"
+ integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==
+ dependencies:
+ "@types/hoist-non-react-statics" "^3.3.0"
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+ redux "^4.0.0"
"@types/react-transition-group@^4.4.0":
version "4.4.0"
@@ -1639,12 +1672,13 @@
dependencies:
"@types/react" "*"
-"@types/react@*", "@types/react@^16", "@types/react@^16.14", "@types/react@^16.9":
- version "16.14.2"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c"
- integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ==
+"@types/react@*", "@types/react@^17.0.2":
+ version "17.0.11"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451"
+ integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==
dependencies:
"@types/prop-types" "*"
+ "@types/scheduler" "*"
csstype "^3.0.2"
"@types/sanitize-html@^2.3.1":
@@ -1654,6 +1688,11 @@
dependencies:
htmlparser2 "^6.0.0"
+"@types/scheduler@*":
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
+ integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
+
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -2121,14 +2160,6 @@ babel-preset-jest@^26.6.2:
babel-plugin-jest-hoist "^26.6.2"
babel-preset-current-node-syntax "^1.0.0"
-babel-runtime@^6.26.0:
- version "6.26.0"
- resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
- integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
- dependencies:
- core-js "^2.4.0"
- regenerator-runtime "^0.11.0"
-
bail@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
@@ -2647,11 +2678,6 @@ core-js@^1.0.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
-core-js@^2.4.0:
- version "2.6.12"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
- integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
-
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -2711,6 +2737,13 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
+css-box-model@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
+ integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
+ dependencies:
+ tiny-invariant "^1.0.6"
+
css-select@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286"
@@ -4240,7 +4273,7 @@ highlight.js@^10.5.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f"
integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==
-hoist-non-react-statics@^3.3.0:
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -4455,13 +4488,6 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
-invariant@^2.2.2, invariant@^2.2.4:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
- integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
- dependencies:
- loose-envify "^1.0.0"
-
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -5599,11 +5625,6 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
-lodash-es@^4.2.1:
- version "4.17.20"
- resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7"
- integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==
-
lodash.escape@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
@@ -5624,7 +5645,7 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
-lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1:
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -5752,10 +5773,10 @@ matrix-react-test-utils@^0.2.3:
"@babel/traverse" "^7.13.17"
walk "^2.3.14"
-matrix-widget-api@^0.1.0-beta.14:
- version "0.1.0-beta.14"
- resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70"
- integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ==
+matrix-widget-api@^0.1.0-beta.15:
+ version "0.1.0-beta.15"
+ resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745"
+ integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"
@@ -5793,10 +5814,10 @@ mdurl@~1.0.1:
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
-memoize-one@^3.0.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17"
- integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA==
+memoize-one@^5.1.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
+ integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
meow@^9.0.0:
version "9.0.0"
@@ -6442,11 +6463,6 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-performance-now@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
- integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=
-
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -6656,7 +6672,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
-prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2:
+prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -6733,12 +6749,12 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
-raf-schd@^2.1.0:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-2.1.2.tgz#ec622b5167f2912089f054dc03ebd5bcf33c8f62"
- integrity sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g==
+raf-schd@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
+ integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
-raf@^3.1.0, raf@^3.4.1:
+raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
@@ -6765,21 +6781,18 @@ re-resizable@^6.9.0:
dependencies:
fast-memoize "^2.5.1"
-react-beautiful-dnd@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81"
- integrity sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA==
+react-beautiful-dnd@^13.1.0:
+ version "13.1.0"
+ resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
+ integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
dependencies:
- babel-runtime "^6.26.0"
- invariant "^2.2.2"
- memoize-one "^3.0.1"
- prop-types "^15.6.0"
- raf-schd "^2.1.0"
- react-motion "^0.5.2"
- react-redux "^5.0.6"
- redux "^3.7.2"
- redux-thunk "^2.2.0"
- reselect "^3.0.1"
+ "@babel/runtime" "^7.9.2"
+ css-box-model "^1.2.0"
+ memoize-one "^5.1.1"
+ raf-schd "^4.0.2"
+ react-redux "^7.2.0"
+ redux "^4.0.4"
+ use-memo-one "^1.1.1"
react-clientside-effect@^1.2.2:
version "1.2.3"
@@ -6814,7 +6827,7 @@ react-focus-lock@^2.5.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
-react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -6824,32 +6837,17 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
-react-lifecycles-compat@^3.0.0:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
- integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
-
-react-motion@^0.5.2:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
- integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==
+react-redux@^7.2.0:
+ version "7.2.4"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
+ integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
dependencies:
- performance-now "^0.2.0"
- prop-types "^15.5.8"
- raf "^3.1.0"
-
-react-redux@^5.0.6:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57"
- integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==
- dependencies:
- "@babel/runtime" "^7.1.2"
- hoist-non-react-statics "^3.3.0"
- invariant "^2.2.4"
- loose-envify "^1.1.0"
- prop-types "^15.6.1"
- react-is "^16.6.0"
- react-lifecycles-compat "^3.0.0"
+ "@babel/runtime" "^7.12.1"
+ "@types/react-redux" "^7.1.16"
+ hoist-non-react-statics "^3.3.2"
+ loose-envify "^1.4.0"
+ prop-types "^15.7.2"
+ react-is "^16.13.1"
react-shallow-renderer@^16.13.1:
version "16.14.1"
@@ -6978,20 +6976,12 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
-redux-thunk@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
- integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
-
-redux@^3.7.2:
- version "3.7.2"
- resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
- integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==
+redux@^4.0.0, redux@^4.0.4:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4"
+ integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==
dependencies:
- lodash "^4.2.1"
- lodash-es "^4.2.1"
- loose-envify "^1.1.0"
- symbol-observable "^1.0.3"
+ "@babel/runtime" "^7.9.2"
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
@@ -7005,11 +6995,6 @@ regenerate@^1.4.0:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
-regenerator-runtime@^0.11.0:
- version "0.11.1"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
- integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
-
regenerator-runtime@^0.13.4:
version "0.13.7"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
@@ -7167,11 +7152,6 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
-reselect@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
- integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=
-
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
@@ -7894,11 +7874,6 @@ svg-tags@^1.0.0:
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
-symbol-observable@^1.0.3:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
- integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
-
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@@ -7961,6 +7936,11 @@ through@^2.3.6:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+tiny-invariant@^1.0.6:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
+ integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
+
tmatch@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf"
@@ -8276,6 +8256,11 @@ use-callback-ref@^1.2.1:
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==
+use-memo-one@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
+ integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
+
use-sidecar@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46"