From 29a81bbe85ca1ca1c4c54534fe94e9d477aef22c Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Tue, 10 Nov 2020 15:04:01 -0600 Subject: [PATCH 001/330] Warn when you attempt to leave room that you are the only member of Signed-off-by: Aaron Raimist --- src/components/structures/MatrixChat.tsx | 17 ++++++++++++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 22cd73eff7..43e1798c6e 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1056,8 +1056,21 @@ export default class MatrixChat extends React.PureComponent { private leaveRoomWarnings(roomId: string) { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); // Show a warning if there are additional complications. - const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', ''); const warnings = []; + + const memberCount = roomToLeave.currentState.getJoinedMemberCount(); + if (memberCount === 1) { + warnings.push( + + {' '/* Whitespace, otherwise the sentences get smashed together */ } + { _t("You are the only member of this room. This room will become unjoinable if you leave.") } + + ); + + return warnings; + } + + const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', ''); if (joinRules) { const rule = joinRules.getContent().join_rule; if (rule !== "public") { @@ -1076,6 +1089,7 @@ export default class MatrixChat extends React.PureComponent { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); + const hasWarnings = warnings.length > 0; Modal.createTrackedDialog('Leave room', '', QuestionDialog, { title: _t("Leave room"), @@ -1086,6 +1100,7 @@ export default class MatrixChat extends React.PureComponent { ), button: _t("Leave"), + danger: hasWarnings, onFinished: (shouldLeave) => { if (shouldLeave) { const d = leaveRoomBehaviour(roomId); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 78340447f3..b412db5ca0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2317,6 +2317,7 @@ "Cannot create rooms in this community": "Cannot create rooms in this community", "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", + "You are the only member of this room. This room will become unjoinable if you leave.": "You are the only member of this room. This room will become unjoinable if you leave.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", "Signed Out": "Signed Out", From 80c4d54ccc56396e15a4979de84d9d18c83a70ad Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Wed, 18 Nov 2020 13:54:49 -0600 Subject: [PATCH 002/330] Fix lint Signed-off-by: Aaron Raimist --- src/components/structures/MatrixChat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 43e1798c6e..17c21a2016 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1060,12 +1060,12 @@ export default class MatrixChat extends React.PureComponent { const memberCount = roomToLeave.currentState.getJoinedMemberCount(); if (memberCount === 1) { - warnings.push( + warnings.push(( {' '/* Whitespace, otherwise the sentences get smashed together */ } { _t("You are the only member of this room. This room will become unjoinable if you leave.") } - ); + )); return warnings; } From 9ffef8f0726414490074110224804a1257223c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 1 Mar 2021 12:53:10 +0100 Subject: [PATCH 003/330] Fix VoIP PIP frame color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallView.scss | 3 ++- res/themes/dark/css/_dark.scss | 2 +- res/themes/light/css/_light.scss | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 7eb329594a..2d5ddec2a4 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_CallView { border-radius: 8px; - background-color: $voipcall-plinth-color; + background-color: $dark-panel-bg-color; padding-left: 8px; padding-right: 8px; // XXX: CallContainer sets pointer-events: none - should probably be set back in a better place @@ -37,6 +37,7 @@ limitations under the License. width: 320px; padding-bottom: 8px; margin-top: 10px; + background-color: $voipcall-plinth-color; box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); border-radius: 8px; diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index a878aa3cdd..0ad4ba7c58 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -109,7 +109,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #21262c; +$voipcall-plinth-color: #24292f; // ******************** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 1c89d83c01..bb673c28c9 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -167,7 +167,7 @@ $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #f2f5f8; +$voipcall-plinth-color: #dddfe2; // ******************** From ef3d87f8e8b371d058c535bd348ae0f4617fb02e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 25 Mar 2021 16:15:41 +0100 Subject: [PATCH 004/330] First implementation of context switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/MatrixChat.tsx | 10 +++++++++- src/stores/SpaceStore.tsx | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 689561fd60..9a4e83828c 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -79,7 +79,7 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import DialPadModal from "../views/voip/DialPadModal"; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { shouldUseLoginForWelcome } from "../../utils/pages"; -import SpaceStore from "../../stores/SpaceStore"; +import SpaceStore, {LAST_VIEWED_ROOMS, LAST_VIEWED_ROOMS_HOME} from "../../stores/SpaceStore"; import SpaceRoomDirectory from "./SpaceRoomDirectory"; import {replaceableComponent} from "../../utils/replaceableComponent"; import RoomListStore from "../../stores/room-list/RoomListStore"; @@ -875,6 +875,14 @@ export default class MatrixChat extends React.PureComponent { private viewRoom(roomInfo: IRoomInfo) { this.focusComposer = true; + // persist last viewed room from a space + const activeSpace = SpaceStore.instance.activeSpace; + const activeSpaceId = activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; + const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; + + lastViewedRooms[activeSpaceId] = roomInfo.room_id; + window.localStorage.setItem(LAST_VIEWED_ROOMS, JSON.stringify(lastViewedRooms)); + if (roomInfo.room_alias) { console.log( `Switching to room alias ${roomInfo.room_alias} at event ` + diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index bcf95a82be..20223cb8a6 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -41,6 +41,10 @@ type SpaceKey = string | symbol; interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; +export const LAST_VIEWED_ROOMS = "mx_last_viewed_rooms"; + +// We can't use HOME_SPACE here because JSON.stringify() will ignore any Symbols +export const LAST_VIEWED_ROOMS_HOME = "home_space"; export const HOME_SPACE = Symbol("home-space"); export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); @@ -111,6 +115,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); + // view last selected room from space + const spaceId = space?.roomId || LAST_VIEWED_ROOMS_HOME; + const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)); + const roomId = lastViewedRooms[spaceId]; + + if (roomId) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: roomId, + }); + } // TODO: Handle else + // persist space selected if (space) { window.localStorage.setItem(ACTIVE_SPACE_LS_KEY, space.roomId); From 67dcb3a448b923254cc1516e54be684abec70cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 25 Mar 2021 20:51:21 +0100 Subject: [PATCH 005/330] If no roomId was saved go to space home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 20223cb8a6..063256f421 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -125,7 +125,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient { action: "view_room", room_id: roomId, }); - } // TODO: Handle else + } else { + if (space) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }); + } else { + // TODO: Switch to first room in the RoomList + } + } // persist space selected if (space) { From 2dcb60b489e0c53101ab9586f9578277baf2e7ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 07:53:07 +0100 Subject: [PATCH 006/330] Move persisting of last viewed into SpaceStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/MatrixChat.tsx | 10 +--------- src/stores/SpaceStore.tsx | 8 ++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 9a4e83828c..689561fd60 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -79,7 +79,7 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import DialPadModal from "../views/voip/DialPadModal"; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { shouldUseLoginForWelcome } from "../../utils/pages"; -import SpaceStore, {LAST_VIEWED_ROOMS, LAST_VIEWED_ROOMS_HOME} from "../../stores/SpaceStore"; +import SpaceStore from "../../stores/SpaceStore"; import SpaceRoomDirectory from "./SpaceRoomDirectory"; import {replaceableComponent} from "../../utils/replaceableComponent"; import RoomListStore from "../../stores/room-list/RoomListStore"; @@ -875,14 +875,6 @@ export default class MatrixChat extends React.PureComponent { private viewRoom(roomInfo: IRoomInfo) { this.focusComposer = true; - // persist last viewed room from a space - const activeSpace = SpaceStore.instance.activeSpace; - const activeSpaceId = activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; - const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; - - lastViewedRooms[activeSpaceId] = roomInfo.room_id; - window.localStorage.setItem(LAST_VIEWED_ROOMS, JSON.stringify(lastViewedRooms)); - if (roomInfo.room_alias) { console.log( `Switching to room alias ${roomInfo.room_alias} at event ` + diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 063256f421..1f6cbb33b5 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -490,6 +490,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (!SettingsStore.getValue("feature_spaces")) return; switch (payload.action) { case "view_room": { + // persist last viewed room from a space + const activeSpace = SpaceStore.instance.activeSpace; + const activeSpaceId = activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; + const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; + + lastViewedRooms[activeSpaceId] = payload.room_id; + window.localStorage.setItem(LAST_VIEWED_ROOMS, JSON.stringify(lastViewedRooms)); + const room = this.matrixClient?.getRoom(payload.room_id); if (room?.getMyMembership() === "join") { From e39f7caf59993031fe47cd076619f8235195ee8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 07:55:52 +0100 Subject: [PATCH 007/330] Don't export as we don't need to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 1f6cbb33b5..453cccdc17 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -41,10 +41,10 @@ type SpaceKey = string | symbol; interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; -export const LAST_VIEWED_ROOMS = "mx_last_viewed_rooms"; +const LAST_VIEWED_ROOMS = "mx_last_viewed_rooms"; // We can't use HOME_SPACE here because JSON.stringify() will ignore any Symbols -export const LAST_VIEWED_ROOMS_HOME = "home_space"; +const LAST_VIEWED_ROOMS_HOME = "home_space"; export const HOME_SPACE = Symbol("home-space"); export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); From c26da1bce6d1cfec21d8266abb5a8c4d8f44aca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 07:58:09 +0100 Subject: [PATCH 008/330] Use this.activeSpace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 453cccdc17..3d476f7077 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -491,8 +491,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { switch (payload.action) { case "view_room": { // persist last viewed room from a space - const activeSpace = SpaceStore.instance.activeSpace; - const activeSpaceId = activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; + const activeSpaceId = this.activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; lastViewedRooms[activeSpaceId] = payload.room_id; From efb8c89433a1aee5fea49dfda3c8cc2e82e149cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 08:13:50 +0100 Subject: [PATCH 009/330] Don't save if isSpaceRoom() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 3d476f7077..ef12640438 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -490,15 +490,20 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (!SettingsStore.getValue("feature_spaces")) return; switch (payload.action) { case "view_room": { - // persist last viewed room from a space - const activeSpaceId = this.activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; - const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; - - lastViewedRooms[activeSpaceId] = payload.room_id; - window.localStorage.setItem(LAST_VIEWED_ROOMS, JSON.stringify(lastViewedRooms)); - const room = this.matrixClient?.getRoom(payload.room_id); + // persist last viewed room from a space + + // We don't want to save if the room is a + // space room since it can cause problems + if (!room.isSpaceRoom()) { + const activeSpaceId = this.activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; + const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; + + lastViewedRooms[activeSpaceId] = payload.room_id; + window.localStorage.setItem(LAST_VIEWED_ROOMS, JSON.stringify(lastViewedRooms)); + } + if (room?.getMyMembership() === "join") { if (room.isSpaceRoom()) { this.setActiveSpace(room); From 7e425ce939ef5ddb30f4ae07fb94079536f1daad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 08:15:35 +0100 Subject: [PATCH 010/330] Empty object if nothing saved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This isn't nice but I'll rework this soon anyway Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index ef12640438..e5cad51240 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -117,7 +117,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // view last selected room from space const spaceId = space?.roomId || LAST_VIEWED_ROOMS_HOME; - const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)); + const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; const roomId = lastViewedRooms[spaceId]; if (roomId) { From 65ef2b845e05ad7648b1b32ddc7dd1617128b069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 08:16:39 +0100 Subject: [PATCH 011/330] Go to /#/home if there is no saved room MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index e5cad51240..1a9df5a9b3 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -132,7 +132,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient { room_id: space.roomId, }); } else { - // TODO: Switch to first room in the RoomList + defaultDispatcher.dispatch({ + action: "view_home_page", + }); } } From f64008e23916fa6b65034dcab7304d69eec43a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 13:39:16 +0100 Subject: [PATCH 012/330] Check if room is defined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sometimes it isn't and that leads to errors. We can't use ? here because we also use ! Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 1a9df5a9b3..f07616aed3 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -498,7 +498,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // We don't want to save if the room is a // space room since it can cause problems - if (!room.isSpaceRoom()) { + if (room && !room.isSpaceRoom()) { const activeSpaceId = this.activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; From f62e2c0042eb2bb9a76eca417aff46bc0010a4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 13:44:51 +0100 Subject: [PATCH 013/330] Use compound keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index f07616aed3..c58340356d 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -117,8 +117,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // view last selected room from space const spaceId = space?.roomId || LAST_VIEWED_ROOMS_HOME; - const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; - const roomId = lastViewedRooms[spaceId]; + const roomId = window.localStorage.getItem(`${LAST_VIEWED_ROOMS}_${spaceId}`); if (roomId) { defaultDispatcher.dispatch({ @@ -500,10 +499,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // space room since it can cause problems if (room && !room.isSpaceRoom()) { const activeSpaceId = this.activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; - const lastViewedRooms = JSON.parse(window.localStorage.getItem(LAST_VIEWED_ROOMS)) || {}; - - lastViewedRooms[activeSpaceId] = payload.room_id; - window.localStorage.setItem(LAST_VIEWED_ROOMS, JSON.stringify(lastViewedRooms)); + window.localStorage.setItem(`${LAST_VIEWED_ROOMS}_${activeSpaceId}`, payload.room_id); } if (room?.getMyMembership() === "join") { From a707524aade0e1b6a0cf35829af363dbec1f3cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 13:49:01 +0100 Subject: [PATCH 014/330] Delete comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index c58340356d..2eec2afbb3 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -43,7 +43,6 @@ interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; const LAST_VIEWED_ROOMS = "mx_last_viewed_rooms"; -// We can't use HOME_SPACE here because JSON.stringify() will ignore any Symbols const LAST_VIEWED_ROOMS_HOME = "home_space"; export const HOME_SPACE = Symbol("home-space"); From d68afcc4ce278d89d9b9bff0dc0ca7490445a627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 26 Mar 2021 13:52:41 +0100 Subject: [PATCH 015/330] Use else if MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maybe it looks a little nicer, I don't know Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 2eec2afbb3..1bf98617a3 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -123,17 +123,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient { action: "view_room", room_id: roomId, }); + } else if (space) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }); } else { - if (space) { - defaultDispatcher.dispatch({ - action: "view_room", - room_id: space.roomId, - }); - } else { - defaultDispatcher.dispatch({ - action: "view_home_page", - }); - } + defaultDispatcher.dispatch({ + action: "view_home_page", + }); } // persist space selected From c2b66d0dbeaa22991d4f3db9acb18401a6c44a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 6 Apr 2021 14:29:20 +0200 Subject: [PATCH 016/330] Fix inserting trailing colon after mention/pill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/SendMessageComposer.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 75bc943146..ab080f32cd 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -502,9 +502,10 @@ export default class SendMessageComposer extends React.Component { member.rawDisplayName : userId; const caret = this._editorRef.getCaret(); const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - // index is -1 if there are no parts but we only care for if this would be the part in position 0 - const insertIndex = position.index > 0 ? position.index : 0; - const parts = partCreator.createMentionParts(insertIndex, displayName, userId); + // createMentionParts() assumes that the mention already has it's own part + // which isn't true, therefore we increase the position.index by 1. This + // also solves the problem of the index being -1 when the composer is empty. + const parts = partCreator.createMentionParts(position.index + 1, displayName, userId); model.transform(() => { const addedLen = model.insert(parts, position); return model.positionForOffset(caret.offset + addedLen, true); From 715fff6f0cb95b3ba64ae0f0a6c183321d3161cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Apr 2021 11:52:07 +0200 Subject: [PATCH 017/330] Redo and fix trailing characters in user pills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This removes the handling of trailing chars from createMentionParts as we need to determine whether or not to insert the trailing char differently in different situations Signed-off-by: Šimon Brandner --- src/components/views/rooms/SendMessageComposer.js | 6 ++---- src/editor/autocomplete.ts | 6 ++---- src/editor/parts.ts | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index ab080f32cd..3aedbd4d92 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -502,10 +502,8 @@ export default class SendMessageComposer extends React.Component { member.rawDisplayName : userId; const caret = this._editorRef.getCaret(); const position = model.positionForOffset(caret.offset, caret.atNodeEnd); - // createMentionParts() assumes that the mention already has it's own part - // which isn't true, therefore we increase the position.index by 1. This - // also solves the problem of the index being -1 when the composer is empty. - const parts = partCreator.createMentionParts(position.index + 1, displayName, userId); + // Insert suffix only if the caret is at the start of the composer + const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId); model.transform(() => { const addedLen = model.insert(parts, position); return model.positionForOffset(caret.offset + addedLen, true); diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index 2f56494ea0..240ed2d96b 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -125,10 +125,8 @@ export default class AutocompleteWrapperModel { case "at-room": return [this.partCreator.atRoomPill(completionId), this.partCreator.plain(completion.suffix)]; case "user": - // not using suffix here, because we also need to calculate - // the suffix when clicking a display name to insert a mention, - // which happens in createMentionParts - return this.partCreator.createMentionParts(this.partIndex, text, completionId); + // Insert suffix only if the pill is the part with index 0 - we are at the start of the composer + return this.partCreator.createMentionParts(this.partIndex === 0, text, completionId); case "command": // command needs special handling for auto complete, but also renders as plain texts return [(this.partCreator as CommandPartCreator).command(text)]; diff --git a/src/editor/parts.ts b/src/editor/parts.ts index ccd90da3e2..9fd7e289ae 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -543,9 +543,9 @@ export class PartCreator { return new UserPillPart(userId, displayName, member); } - createMentionParts(partIndex: number, displayName: string, userId: string) { + createMentionParts(insertTrailingCharacter: boolean, displayName: string, userId: string) { const pill = this.userPill(displayName, userId); - const postfix = this.plain(partIndex === 0 ? ": " : " "); + const postfix = this.plain(insertTrailingCharacter ? ": " : " "); return [pill, postfix]; } } From 653146a177769cff2ff9e2b25f2565e5d3d3af34 Mon Sep 17 00:00:00 2001 From: iaiz Date: Wed, 7 Apr 2021 12:32:29 +0000 Subject: [PATCH 018/330] Translated using Weblate (Spanish) Currently translated at 99.5% (2903 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index c6e84570d6..202c6d4d71 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3188,5 +3188,28 @@ "From %(deviceName)s (%(deviceId)s) at %(ip)s": "De %(deviceName)s (%(deviceId)s) en", "Check your devices": "Comprueba tus dispositivos", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Alguien está iniciando sesión a tu cuenta: %(name)s (%(deviceID)s) en %(ip)s", - "You have unverified logins": "Tienes inicios de sesión sin verificar" + "You have unverified logins": "Tienes inicios de sesión sin verificar", + "Verification requested": "Verificación solicitada", + "Avatar": "Imagen de perfil", + "Verify other login": "Verificar otro inicio de sesión", + "Consult first": "Consultar primero", + "Invited people will be able to read old messages.": "Las personas invitadas podrán leer mensajes antiguos.", + "We couldn't create your DM.": "No hemos podido crear tu mensaje directo.", + "Adding...": "Añadiendo...", + "Add existing rooms": "Añadir salas existentes", + "%(count)s people you know have already joined|one": "%(count)s persona que ya conoces se ha unido", + "%(count)s people you know have already joined|other": "%(count)s personas que ya conoces se han unido", + "Accept on your other login…": "Acepta en tu otro inicio de sesión…", + "Stop & send recording": "Parar y enviar grabación", + "Record a voice message": "Grabar un mensaje de voz", + "Quick actions": "Acciones rápidas", + "Invite to just this room": "Invitar solo a esta sala", + "Warn before quitting": "Avisar antes de salir", + "Manage & explore rooms": "Gestionar y explorar salas", + "unknown person": "persona desconocida", + "Share decryption keys for room history when inviting users": "Compartir claves para descifrar el historial de la sala al invitar a gente", + "Send and receive voice messages (in development)": "Enviar y recibir mensajes de voz (en desarrollo)", + "%(deviceId)s from %(ip)s": "%(deviceId)s desde %(ip)s", + "Review to ensure your account is safe": "Revisa que tu cuenta esté segura", + "Sends the given message as a spoiler": "Envía el mensaje como un spoiler" } From 8eabb0f2142576e7242068b53c50f72bcaeb9265 Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Wed, 7 Apr 2021 13:16:12 +0000 Subject: [PATCH 019/330] Translated using Weblate (French) Currently translated at 100.0% (2917 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 01390329bb..fcc5ec9afe 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3225,5 +3225,42 @@ "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Sur %(deviceName)s %(deviceId)s depuis %(ip)s", "Check your devices": "Vérifiez vos appareils", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Une nouvelle session a accès à votre compte : %(name)s %(deviceID)s depuis %(ip)s", - "You have unverified logins": "Vous avez des sessions non-vérifiées" + "You have unverified logins": "Vous avez des sessions non-vérifiées", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Sans vérification vous n’aurez pas accès à tous vos messages et n’apparaîtrez pas comme de confiance aux autres.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Vérifiez votre identité pour accéder aux messages chiffrés et prouver votre identité aux autres.", + "Use another login": "Utiliser un autre identifiant", + "Please choose a strong password": "Merci de choisir un mot de passe fort", + "You can add more later too, including already existing ones.": "Vous pourrez en ajouter plus tard, y compris certains déjà existant.", + "Let's create a room for each of them.": "Créons un salon pour chacun d’entre eux.", + "What are some things you want to discuss in %(spaceName)s?": "De quoi voulez vous discuter dans %(spaceName)s ?", + "Verification requested": "Vérification requise", + "Avatar": "Avatar", + "Verify other login": "Vérifier l’autre connexion", + "Reset event store": "Réinitialiser le magasin d’événements", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Si vous le faites, notes qu’aucun de vos messages ne sera supprimé, mais la recherche pourrait être dégradée pendant quelques instants, le temps de recréer l’index", + "You most likely do not want to reset your event index store": "Il est probable que vous ne vouliez pas réinitialiser votre magasin d’index d’événements", + "Reset event store?": "Réinitialiser le magasin d’événements ?", + "Consult first": "Consulter d’abord", + "Invited people will be able to read old messages.": "Les personnes invitées pourront lire les anciens messages.", + "We couldn't create your DM.": "Nous n’avons pas pu créer votre message direct.", + "Adding...": "Ajout…", + "Add existing rooms": "Ajouter des salons existants", + "%(count)s people you know have already joined|one": "%(count)s personne que vous connaissez en fait déjà partie", + "%(count)s people you know have already joined|other": "%(count)s personnes que vous connaissez en font déjà partie", + "Accept on your other login…": "Acceptez sur votre autre connexion…", + "Stop & send recording": "Terminer et envoyer l’enregistrement", + "Record a voice message": "Enregistrer un message vocal", + "Invite messages are hidden by default. Click to show the message.": "Les messages d’invitation sont masqués par défaut. Cliquez pour voir le message.", + "Quick actions": "Actions rapides", + "Invite to just this room": "Inviter seulement dans ce salon", + "Warn before quitting": "Avertir avant de quitter", + "Message search initilisation failed": "Échec de l’initialisation de la recherche de message", + "Manage & explore rooms": "Gérer et découvrir les salons", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consultation avec %(transferTarget)s. Transfert à %(transferee)s", + "unknown person": "personne inconnue", + "Share decryption keys for room history when inviting users": "Partager les clés de déchiffrement lors de l’invitation d’utilisateurs", + "Send and receive voice messages (in development)": "Envoyez et recevez des messages vocaux (en développement)", + "%(deviceId)s from %(ip)s": "%(deviceId)s depuis %(ip)s", + "Review to ensure your account is safe": "Vérifiez pour assurer la sécurité de votre compte", + "Sends the given message as a spoiler": "Envoie le message flouté" } From b04a34b37a7e897f5d4ce8c2475a8e91f3cd9b87 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 7 Apr 2021 15:41:24 +0000 Subject: [PATCH 020/330] Translated using Weblate (Hungarian) Currently translated at 100.0% (2917 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index eaa77e809d..2ec5af8a17 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3243,5 +3243,42 @@ "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Innen: %(deviceName)s (%(deviceId)s), %(ip)s", "Check your devices": "Ellenőrizze az eszközeit", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Új bejelentkezéssel hozzáférés történik a fiókjához: %(name)s (%(deviceID)s), %(ip)s", - "You have unverified logins": "Ellenőrizetlen bejelentkezései vannak" + "You have unverified logins": "Ellenőrizetlen bejelentkezései vannak", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Az ellenőrzés nélkül nem fér hozzá az összes üzenetéhez és mások számára megbízhatatlannak fog látszani.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Ellenőrizze a személyazonosságát, hogy hozzáférjen a titkosított üzeneteihez és másoknak is bizonyítani tudja személyazonosságát.", + "Use another login": "Másik munkamenet használata", + "Please choose a strong password": "Kérem válasszon erős jelszót", + "You can add more later too, including already existing ones.": "Később is hozzáadhat többet, beleértve meglévőket is.", + "Let's create a room for each of them.": "Készítsünk szobát mindhez.", + "What are some things you want to discuss in %(spaceName)s?": "Mik azok amikről beszélni szeretne itt: %(spaceName)s?", + "Verification requested": "Hitelesítés kérés elküldve", + "Avatar": "Profilkép", + "Verify other login": "Másik munkamenet ellenőrzése", + "Reset event store": "Az esemény tárolót alaphelyzetbe állítása", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Ha ezt teszi, tudnia kell, hogy az üzenetek nem kerülnek törlésre de keresés nem lesz tökéletes amíg az indexek nem készülnek el újra", + "You most likely do not want to reset your event index store": "Az esemény index tárolót nagy valószínűséggel nem szeretné alaphelyzetbe állítani", + "Reset event store?": "Az esemény tárolót alaphelyzetbe állítja?", + "Consult first": "Kérjen először véleményt", + "Invited people will be able to read old messages.": "A meghívott személyek el tudják olvasni a régi üzeneteket.", + "We couldn't create your DM.": "Nem tudjuk elkészíteni a közvetlen üzenetét.", + "Adding...": "Hozzáadás…", + "Add existing rooms": "Létező szobák hozzáadása", + "%(count)s people you know have already joined|one": "%(count)s ismerős már csatlakozott", + "%(count)s people you know have already joined|other": "%(count)s ismerős már csatlakozott", + "Accept on your other login…": "Egy másik bejelentkezésében fogadta el…", + "Stop & send recording": "Megállít és a felvétel elküldése", + "Record a voice message": "Hang üzenet felvétele", + "Invite messages are hidden by default. Click to show the message.": "A meghívók alapesetben rejtve vannak. A megjelenítéshez kattintson.", + "Quick actions": "Gyors műveletek", + "Invite to just this room": "Meghívás csak ebbe a szobába", + "Warn before quitting": "Kilépés előtt figyelmeztet", + "Message search initilisation failed": "Üzenet keresés beállítása sikertelen", + "Manage & explore rooms": "Szobák kezelése és felderítése", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Egyeztetés vele: %(transferTarget)s. Átadás ide: %(transferee)s", + "unknown person": "ismeretlen személy", + "Share decryption keys for room history when inviting users": "Visszafejtéshez szükséges kulcsok megosztása a szoba előzményekhez felhasználók meghívásakor", + "Send and receive voice messages (in development)": "Hang üzenetek küldése és fogadása (fejlesztés alatt)", + "%(deviceId)s from %(ip)s": "%(deviceId)s innen: %(ip)s", + "Review to ensure your account is safe": "Tekintse át, hogy meggyőződjön arról, hogy a fiókja biztonságban van", + "Sends the given message as a spoiler": "A megadott üzenet szpojlerként küldése" } From dfca00d2d9857f30dcc8908e7253f714bec70936 Mon Sep 17 00:00:00 2001 From: jelv Date: Wed, 7 Apr 2021 14:25:17 +0000 Subject: [PATCH 021/330] Translated using Weblate (Dutch) Currently translated at 100.0% (2917 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 43 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index ee99127e04..f1c48cc539 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -632,7 +632,7 @@ "The version of %(brand)s": "De versie van %(brand)s", "Your language of choice": "De door jou gekozen taal", "Which officially provided instance you are using, if any": "Welke officieel aangeboden instantie je eventueel gebruikt", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "Of je de tekstverwerker al dan niet in de modus voor opgemaakte tekst gebruikt", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Of u de tekstverwerker al dan niet in de modus voor opgemaakte tekst gebruikt", "Your homeserver's URL": "De URL van je homeserver", "In reply to ": "Als antwoord op ", "This room is not public. You will not be able to rejoin without an invite.": "Dit is geen openbaar gesprek. Slechts op uitnodiging zult u opnieuw kunnen toetreden.", @@ -1255,7 +1255,7 @@ "The homeserver may be unavailable or overloaded.": "De homeserver is mogelijk onbereikbaar of overbelast.", "You have %(count)s unread notifications in a prior version of this room.|other": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.", "You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen meldingen in een vorige versie van dit gesprek.", - "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of je de icoontjes voor recente gesprekken (boven de gesprekkenlijst) al dan niet gebruikt", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u de icoontjes voor recente gesprekken (boven de gesprekkenlijst) al dan niet gebruikt", "Replying With Files": "Beantwoorden met bestanden", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Het is momenteel niet mogelijk met een bestand te antwoorden. Wil je dit bestand uploaden zonder te antwoorden?", "The file '%(fileName)s' failed to upload.": "Het bestand ‘%(fileName)s’ kon niet geüpload worden.", @@ -3134,5 +3134,42 @@ "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Van %(deviceName)s (%(deviceId)s) op %(ip)s", "Check your devices": "Controleer uw apparaten", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Een nieuwe login heeft toegang tot uw account: %(name)s (%(deviceID)s) op %(ip)s", - "You have unverified logins": "U heeft ongeverifieerde logins" + "You have unverified logins": "U heeft ongeverifieerde logins", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Zonder verifiëren heeft u geen toegang tot al uw berichten en kan u als onvertrouwd aangemerkt staan bij anderen.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifeer uw identiteit om toegang te krijgen tot uw versleutelde berichten en uw identiteit te bewijzen voor anderen.", + "Use another login": "Gebruik andere login", + "Please choose a strong password": "Kies een sterk wachtwoord", + "You can add more later too, including already existing ones.": "U kunt er later nog meer toevoegen, inclusief al bestaande gesprekken.", + "Let's create a room for each of them.": "Laten we voor elk een los gesprek maken.", + "What are some things you want to discuss in %(spaceName)s?": "Wat wilt u allemaal bespreken in %(spaceName)s?", + "Verification requested": "Verificatieverzocht", + "Avatar": "Avatar", + "Verify other login": "Verifieer andere login", + "You most likely do not want to reset your event index store": "U wilt waarschijnlijk niet uw gebeurtenisopslag-index resetten", + "Reset event store?": "Gebeurtenisopslag resetten?", + "Reset event store": "Gebeurtenisopslag resetten", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Als u dit wilt, let op uw berichten worden niet verwijderd, zal het zoeken tijdelijk minder goed werken terwijl we uw index opnieuw opbouwen", + "Consult first": "Eerst overleggen", + "Invited people will be able to read old messages.": "Uitgenodigde personen kunnen de oude berichten lezen.", + "We couldn't create your DM.": "We konden uw DM niet aanmaken.", + "Adding...": "Toevoegen...", + "Add existing rooms": "Bestaande gesprekken toevoegen", + "%(count)s people you know have already joined|one": "%(count)s persoon die u kent is al geregistreerd", + "%(count)s people you know have already joined|other": "%(count)s personen die u kent hebben zijn al geregistreerd", + "Accept on your other login…": "Accepteer op uw andere login…", + "Stop & send recording": "Stop & verstuur opname", + "Record a voice message": "Audiobericht opnemen", + "Invite messages are hidden by default. Click to show the message.": "Uitnodigingen zijn standaard verborgen. Klik om de uitnodigingen weer te geven.", + "Quick actions": "Snelle acties", + "Invite to just this room": "Uitnodigen voor alleen dit gesprek", + "Warn before quitting": "Waarschuwen voordat u afsluit", + "Message search initilisation failed": "Zoeken in berichten opstarten is mislukt", + "Manage & explore rooms": "Beheer & ontdek gesprekken", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Overleggen met %(transferTarget)s. Verstuur naar %(transferee)s", + "unknown person": "onbekend persoon", + "Share decryption keys for room history when inviting users": "Deel ontsleutelsleutels voor de gespreksgeschiedenis wanneer u personen uitnodigd", + "Send and receive voice messages (in development)": "Verstuur en ontvang audioberichten (in ontwikkeling)", + "%(deviceId)s from %(ip)s": "%(deviceId)s van %(ip)s", + "Review to ensure your account is safe": "Controleer om u te verzekeren dat uw account veilig is", + "Sends the given message as a spoiler": "Verstuurt het bericht als een spoiler" } From a5767a9b8dd8bfd236f314f0db29000f3a799ac5 Mon Sep 17 00:00:00 2001 From: Nikita Epifanov Date: Wed, 7 Apr 2021 12:09:31 +0000 Subject: [PATCH 022/330] Translated using Weblate (Russian) Currently translated at 99.4% (2863 of 2880 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 43 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 27a418c5c2..7db3758fd8 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -3169,5 +3169,46 @@ "Decrypted event source": "Расшифрованный исходный код", "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s комната и %(numSpaces)s пространств", "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s комнат и %(numSpaces)s пространств", - "If you can't find the room you're looking for, ask for an invite or create a new room.": "Если вы не можете найти комнату, попросите приглашение или создайте новую комнату." + "If you can't find the room you're looking for, ask for an invite or create a new room.": "Если вы не можете найти комнату, попросите приглашение или создайте новую комнату.", + "Values at explicit levels in this room:": "Значения уровня чувствительности в этой комнате:", + "Values at explicit levels:": "Значения уровня чувствительности:", + "Values at explicit levels in this room": "Значения уровня чувствительности в этой комнате", + "Values at explicit levels": "Значения уровня чувствительности", + "We'll create rooms for each of them. You can add more later too, including already existing ones.": "Мы создадим комнаты для каждого из них. Вы можете добавить ещё больше позже, включая уже существующие.", + "What projects are you working on?": "Над какими проектами вы работаете?", + "Invite by username": "Пригласить по имени пользователя", + "Make sure the right people have access. You can invite more later.": "Убедитесь, что правильные люди имеют доступ. Вы можете пригласить больше людей позже.", + "Invite your teammates": "Пригласите своих товарищей по команде", + "Inviting...": "Приглашение…", + "Failed to invite the following users to your space: %(csvUsers)s": "Не удалось пригласить следующих пользователей в ваше пространство: %(csvUsers)s", + "Me and my teammates": "Я и мои товарищи по команде", + "A private space for you and your teammates": "Приватное пространство для вас и ваших товарищей по команде", + "A private space to organise your rooms": "Приватное пространство для организации ваших комнат", + "Just me": "Только я", + "Make sure the right people have access to %(name)s": "Убедитесь, что правильные люди имеют доступ к %(name)s", + "Who are you working with?": "С кем ты работаешь?", + "Go to my first room": "Перейти в мою первую комнату", + "It's just you at the moment, it will be even better with others.": "Сейчас здесь только ты, с другими будет ещё лучше.", + "Share %(name)s": "Поделиться %(name)s", + "Creating rooms...": "Создание комнат…", + "Skip for now": "Пропустить сейчас", + "Failed to create initial space rooms": "Не удалось создать первоначальные комнаты пространства", + "Room name": "Название комнаты", + "Support": "Поддержка", + "Random": "Случайный", + "Welcome to ": "Добро пожаловать в ", + "Your server does not support showing space hierarchies.": "Ваш сервер не поддерживает отображение пространственных иерархий.", + "Add existing rooms & spaces": "Добавить существующие комнаты и пространства", + "Private space": "Приватное пространство", + "Public space": "Публичное пространство", + " invites you": " пригласил(а) тебя", + "Search names and description": "Искать имена и описание", + "You may want to try a different search or check for typos.": "Вы можете попробовать другой поиск или проверить опечатки.", + "No results found": "Результаты не найдены", + "Mark as suggested": "Отметить как рекомендуется", + "Mark as not suggested": "Отметить как не рекомендуется", + "Removing...": "Удаление…", + "Failed to remove some rooms. Try again later": "Не удалось удалить несколько комнат. Попробуйте позже", + "%(count)s rooms and 1 space|one": "%(count)s комната и одно пространство", + "%(count)s rooms and 1 space|other": "%(count)s комнат и одно пространство" } From 4136a02279be829d717f346ecf9b8c5585bd40c8 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 7 Apr 2021 15:42:31 +0000 Subject: [PATCH 023/330] Translated using Weblate (Albanian) Currently translated at 99.6% (2907 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 58d23e9395..ad768b59cb 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -874,7 +874,7 @@ "Incompatible Database": "Bazë të dhënash e Papërputhshme", "Continue With Encryption Disabled": "Vazhdo Me Fshehtëzimin të Çaktivizuar", "Unable to load! Check your network connectivity and try again.": "S’arrihet të ngarkohet! Kontrolloni lidhjen tuaj në rrjet dhe riprovoni.", - "Forces the current outbound group session in an encrypted room to be discarded": "", + "Forces the current outbound group session in an encrypted room to be discarded": "E detyron të hidhet tej sesionin e tanishëm outbound grupi në një dhomë të fshehtëzuar", "Delete Backup": "Fshije Kopjeruajtjen", "Unable to load key backup status": "S’arrihet të ngarkohet gjendje kopjeruajtjeje kyçesh", "Backup version: ": "Version kopjeruajtjeje: ", @@ -3240,5 +3240,37 @@ "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Nga %(deviceName)s (%(deviceId)s) te %(ip)s", "Check your devices": "Kontrolloni pajisjet tuaja", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Në llogarinë tuaj po hyhet nga një palë kredenciale të reja: %(name)s (%(deviceID)s) te %(ip)s", - "You have unverified logins": "Keni kredenciale të erifikuar" + "You have unverified logins": "Keni kredenciale të erifikuar", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Pa e verifikuar, s’do të mund të hyni te krejt mesazhet tuaja dhe mund të dukeni jo i besueshëm për të tjerët.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifikoni identitetin tuaj që të hyhet në mesazhe të fshehtëzuar dhe t’u provoni të tjerëve identitetin tuaj.", + "Use another login": "Përdorni të tjera kredenciale hyrjesh", + "Please choose a strong password": "Ju lutemi, zgjidhni një fjalëkalim të fuqishëm", + "You can add more later too, including already existing ones.": "Mund të shtoni edhe të tjera më vonë, përfshi ato ekzistueset tashmë.", + "Let's create a room for each of them.": "Le të krijojmë një dhomë për secilën prej tyre.", + "What are some things you want to discuss in %(spaceName)s?": "Cilat janë disa nga gjërat që doni të diskutoni në %(spaceName)s?", + "Verification requested": "U kërkua verifikim", + "Avatar": "Avatar", + "Verify other login": "Verifikoni kredencialet e tjera për hyrje", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Nëse e bëni, ju lutemi, kini parasysh se s’do të fshihet asnjë prej mesazheve tuaja, por puna me kërkimet mund të bjerë, për ca çaste, teksa rikrijohet treguesi", + "Consult first": "Konsultohu së pari", + "Invited people will be able to read old messages.": "Personat e ftuar do të jenë në gjendje të lexojnë mesazhe të vjetër.", + "We couldn't create your DM.": "S’e krijuam dot DM-në tuaj.", + "Adding...": "Po shtohet…", + "Add existing rooms": "Shtoni dhoma ekzistuese", + "%(count)s people you know have already joined|one": "%(count)s person që e njihni është bërë pjesë tashmë", + "%(count)s people you know have already joined|other": "%(count)s persona që i njihni janë bërë pjesë tashmë", + "Stop & send recording": "Ndale & dërgo incizimin", + "Record a voice message": "Incizoni një mesazh zanor", + "Invite messages are hidden by default. Click to show the message.": "Mesazhet e ftesave, si parazgjedhje, janë të fshehur. Klikoni që të shfaqet mesazhi.", + "Quick actions": "Veprime të shpejta", + "Invite to just this room": "Ftoje thjesht te kjo dhomë", + "Warn before quitting": "Sinjalizo përpara daljes", + "Message search initilisation failed": "Dështoi gatitje kërkimi mesazhesh", + "Manage & explore rooms": "Administroni & eksploroni dhoma", + "unknown person": "person i panjohur", + "Sends the given message as a spoiler": "E dërgon mesazhin e dhënë si spoiler", + "Share decryption keys for room history when inviting users": "Ndani me përdorues kyçe shfshehtëzimi, kur ftohen përdorues", + "Send and receive voice messages (in development)": "Dërgoni dhe merrni mesazhe zanorë (në zhvillim)", + "%(deviceId)s from %(ip)s": "%(deviceId)s prej %(ip)s", + "Review to ensure your account is safe": "Shqyrtojeni për t’u siguruar se llogaria është e parrezik" } From 0b878e9d72398354dabb5efcb60c8c2ddea56e7e Mon Sep 17 00:00:00 2001 From: Magnus Date: Wed, 7 Apr 2021 16:44:36 +0000 Subject: [PATCH 024/330] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 48.3% (1411 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nb_NO/ --- src/i18n/strings/nb_NO.json | 48 ++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index ee116fa5bd..a2f5444289 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -1507,5 +1507,51 @@ "This will end the conference for everyone. Continue?": "Dette vil avslutte konferansen for alle. Fortsett?", "End conference": "Avslutt konferanse", "You're already in a call with this person.": "Du er allerede i en samtale med denne personen.", - "Already in call": "Allerede i en samtale" + "Already in call": "Allerede i en samtale", + "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Er du sikkert på at du vil fjerne '%(roomName)s' fra %(groupId)s?", + "Burundi": "Burundi", + "Burkina Faso": "Burkina Faso", + "Bulgaria": "Bulgaria", + "Brunei": "Brunei", + "Brazil": "Brazil", + "Botswana": "Botswana", + "Bolivia": "Bolivia", + "Bhutan": "Bhutan", + "Bermuda": "Bermuda", + "Benin": "Benin", + "Belize": "Belize", + "Belarus": "Hviterussland", + "Barbados": "Barbados", + "Bangladesh": "Bangladesh", + "Bahrain": "Bahrain", + "Bahamas": "Bahamas", + "Azerbaijan": "Azerbaijan", + "Austria": "Østerrike", + "Australia": "Australia", + "Aruba": "Aruba", + "Armenia": "Armenia", + "Argentina": "Argentina", + "Antigua & Barbuda": "Antigua og Barbuda", + "Antarctica": "Antarktis", + "Anguilla": "Anguilla", + "Angola": "Angola", + "Andorra": "Andorra", + "Algeria": "Algeria", + "Albania": "Albania", + "Åland Islands": "Åland", + "Afghanistan": "Afghanistan", + "United Kingdom": "Storbritannia", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Din hjemmeserver kunne ikke nås, og kan derfor ikke logge deg inn. Vennligst prøv igjen. Hvis dette fortsetter, kontakt administratoren til din hjemmeserver", + "Only continue if you trust the owner of the server.": "Fortsett kun om du stoler på eieren av serveren.", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Denne handlingen krever tilgang til standard identitetsserver for å kunne validere en epostaddresse eller telefonnummer, men serveren har ikke bruksvilkår.", + "Too Many Calls": "For mange samtaler", + "Call failed because webcam or microphone could not be accessed. Check that:": "Samtalen mislyktes fordi du fikk ikke tilgang til webkamera eller mikrofon. Sørg for at:", + "Unable to access webcam / microphone": "Ingen tilgang til webkamera / mikrofon", + "The call was answered on another device.": "Samtalen ble besvart på en annen enhet.", + "The call could not be established": "Samtalen kunne ikke etableres.", + "The other party declined the call.": "Den andre parten avviste samtalen.", + "Call Declined": "Samtale avvist", + "Click the button below to confirm adding this phone number.": "Klikk knappen nedenfor for å bekrefte dette telefonnummeret.", + "Single Sign On": "Single Sign On", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bekreft dette telefonnummeret ved å bruke Single Sign On for å bevise din identitet." } From eaeefa8c61f3fd8608b712a446796343472c819c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20L=C3=B8vbr=C3=B8tte=20Olsen?= Date: Wed, 7 Apr 2021 16:39:01 +0000 Subject: [PATCH 025/330] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 48.3% (1411 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nb_NO/ --- src/i18n/strings/nb_NO.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index a2f5444289..9fe5efddbb 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -1553,5 +1553,6 @@ "Call Declined": "Samtale avvist", "Click the button below to confirm adding this phone number.": "Klikk knappen nedenfor for å bekrefte dette telefonnummeret.", "Single Sign On": "Single Sign On", - "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bekreft dette telefonnummeret ved å bruke Single Sign On for å bevise din identitet." + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bekreft dette telefonnummeret ved å bruke Single Sign On for å bevise din identitet.", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Befrekt denne e-postadressen ved å bruke Single Sign On for å bevise din identitet." } From 0981b0ce6f0c605924c65ec9ae599ae4a883a704 Mon Sep 17 00:00:00 2001 From: Dan-Philipp Krenn Date: Thu, 8 Apr 2021 13:54:48 +0000 Subject: [PATCH 026/330] Translated using Weblate (German) Currently translated at 98.3% (2868 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index d22b9ebfb7..b5b08f43f4 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2621,7 +2621,7 @@ "Call Paused": "Anruf pausiert", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern, um sie in Suchergebnissen finden zu können. Es werden %(size)s benötigt, um die Nachrichten von %(rooms)s Räumen zu speichern.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern, um sie in Suchergebnissen finden zu können. Es werden %(size)s benötigt, um die Nachrichten vom Raum %(rooms)s zu speichern.", - "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Nur ihr zwei seid in dieser Konversation, außer ihr lädt jemanden neues ein.", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Nur ihr zwei seid in dieser Konversation, außer ihr ladet jemanden Neues ein.", "This is the beginning of your direct message history with .": "Dies ist der Beginn deiner Direktnachrichten mit .", "Topic: %(topic)s (edit)": "Thema: %(topic)s (ändern)", "Topic: %(topic)s ": "Thema: %(topic)s ", From 3c4eb3514c0ea3fc0155767993a5c1fa226bace4 Mon Sep 17 00:00:00 2001 From: libexus Date: Thu, 8 Apr 2021 11:28:27 +0000 Subject: [PATCH 027/330] Translated using Weblate (German) Currently translated at 98.3% (2868 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index b5b08f43f4..bb7ad2d0f9 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -126,12 +126,12 @@ "Jan": "Jan", "Feb": "Feb", "Mar": "Mrz", - "Apr": "April", + "Apr": "Apr", "May": "Mai", "Jun": "Jun", "Jul": "Jul", "Aug": "Aug", - "Sep": "Sep", + "Sep": "Sept", "Oct": "Okt", "Nov": "Nov", "Dec": "Dez", @@ -582,7 +582,7 @@ "Notify the whole room": "Alle im Raum benachrichtigen", "Room Notification": "Raum-Benachrichtigung", "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "Diese Räume werden Community-Mitgliedern auf der Community-Seite angezeigt. Community-Mitglieder können diesen Räumen beitreten, indem sie diese anklicken.", - "Show these rooms to non-members on the community page and room list?": "Sollen diese Räume öffentlich sichtbar auf der Community-Seite und in der Raum-Liste angezeigt werden?", + "Show these rooms to non-members on the community page and room list?": "Sollen diese Räume öffentlich auf der Community-Seite und in der Raum-Liste angezeigt werden?", "

HTML for your community's page

\n

\n Use the long description to introduce new members to the community, or distribute\n some important links\n

\n

\n You can even use 'img' tags\n

\n": "

HTML für deine Community-Seite

\n

\n Nutze die ausführliche Beschreibung, um neuen Mitgliedern diese Community vorzustellen\n oder um wichtige Links bereitzustellen.\n

\n

\n Du kannst sogar 'img'-Tags (HTML) verwenden\n

\n", "Your community hasn't got a Long Description, a HTML page to show to community members.
Click here to open settings and give it one!": "Deine Community hat noch keine ausführliche Beschreibung, d. h. eine HTML-Seite, die Community-Mitgliedern angezeigt wird.
Hier klicken, um die Einstellungen zu öffnen und eine Beschreibung zu erstellen!", "Enable inline URL previews by default": "URL-Vorschau standardmäßig aktivieren", @@ -632,7 +632,7 @@ "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der letze Nutzer mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.", "Community IDs cannot be empty.": "Community-IDs können nicht leer sein.", "Learn more about how we use analytics.": "Lerne mehr darüber, wie wir die Analysedaten nutzen.", - "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Wenn diese Seite identifizierbare Informationen wie Raum-, Nutzer- oder Gruppen-ID enthält, werden diese Daten entfernt bevor sie an den Server gesendet werden.", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Wenn diese Seite identifizierbare Informationen wie Raum-, Nutzer- oder Gruppen-ID enthält, werden diese Daten entfernt, bevor sie an den Server gesendet werden.", "Which officially provided instance you are using, if any": "Welche offiziell angebotene Instanz du nutzt, wenn überhaupt eine", "In reply to ": "Als Antwort auf ", "This room is not public. You will not be able to rejoin without an invite.": "Dies ist kein öffentlicher Raum. Du wirst diesen nicht ohne Einladung wieder beitreten können.", @@ -1240,7 +1240,7 @@ "The server does not support the room version specified.": "Der Server unterstützt die angegebene Raumversion nicht.", "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.": "Achtung: Ein Raum-Upgrade wird die Mitglieder des Raumes nicht automatisch auf die neue Version migrieren. Wir werden in der alten Raumversion einen Link zum neuen Raum posten - Raum-Mitglieder müssen dann auf diesen Link klicken um dem neuen Raum beizutreten.", "Replying With Files": "Mit Dateien antworten", - "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Momentan ist es nicht möglich mit einer Datei zu antworten. Möchtest Du die Datei hochladen ohne zu antworten?", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Momentan ist es nicht möglich mit einer Datei zu antworten. Möchtest Du die Datei hochladen, ohne zu antworten?", "The file '%(fileName)s' failed to upload.": "Die Datei \"%(fileName)s\" konnte nicht hochgeladen werden.", "Changes your avatar in this current room only": "Ändert deinen Avatar für diesen Raum", "Unbans user with given ID": "Entbannt den Benutzer mit der angegebenen ID", @@ -1328,7 +1328,7 @@ "Find a room…": "Einen Raum suchen…", "Find a room… (e.g. %(exampleRoom)s)": "Einen Raum suchen… (z.B. %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Wenn du den gesuchten Raum nicht finden kannst, frage nach einer Einladung für den Raum oder Erstelle einen neuen Raum.", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ kannst du versuchen, den öffentlichen Server unter turn.matrix.org zu verwenden. Allerdings wird dieser nicht so zuverlässig sein und du teilst deine IP-Adresse mit diesem Server. Du kannst dies auch in den Einstellungen konfigurieren.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ kannst du versuchen, den öffentlichen Server unter turn.matrix.org zu verwenden. Allerdings wird dieser nicht so zuverlässig sein und du teilst deine IP-Adresse mit dem Server. Du kannst dies auch in den Einstellungen konfigurieren.", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Diese Handlung erfordert es, auf den Standard-Identitätsserver zuzugreifen, um eine E-Mail Adresse oder Telefonnummer zu validieren, aber der Server hat keine Nutzungsbedingungen.", "Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du den Betreibern des Servers vertraust.", "Trust": "Vertrauen", @@ -1416,8 +1416,8 @@ "View rules": "Regeln öffnen", "You are currently subscribed to:": "Du abonnierst momentan:", "⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.", - "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem das Tasten die primäre Eingabemöglichkeit ist", - "Whether you're using %(brand)s as an installed Progressive Web App": "Ob du %(brand)s als installierte progressive Web-App verwendest", + "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem Touch die primäre Eingabemöglichkeit ist", + "Whether you're using %(brand)s as an installed Progressive Web App": "Ob du %(brand)s als installierte progressive Web-App (PWA) verwendest", "Your user agent": "Dein User-Agent", "If you cancel now, you won't complete verifying the other user.": "Wenn Sie jetzt abbrechen, werden Sie die Verifizierung des anderen Nutzers nicht beenden können.", "If you cancel now, you won't complete verifying your other session.": "Wenn Sie jetzt abbrechen, werden Sie die Verifizierung der anderen Sitzung nicht beenden können.", @@ -2540,7 +2540,7 @@ "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Alle Server sind von der Teilnahme ausgeschlossen! Dieser Raum kann nicht mehr genutzt werden.", "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s hat die Server-ACLs für diesen Raum geändert.", "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s hat die Server-ACLs für diesen Raum gesetzt.", - "The call was answered on another device.": "Der Anruf wurde an einem anderen Gerät angenommen.", + "The call was answered on another device.": "Der Anruf wurde auf einem anderen Gerät angenommen.", "Answered Elsewhere": "Anderswo beantwortet", "The call could not be established": "Der Anruf kann nicht getätigt werden", "The other party declined the call.": "Die andere Seite hat den Anruf abgelehnt.", @@ -2649,7 +2649,7 @@ "Decline All": "Alles ablehnen", "Go to Home View": "Zur Startseite gehen", "Filter rooms and people": "Räume und Personen filtern", - "%(creator)s created this DM.": "%(creator)s hat diese DM erstellt.", + "%(creator)s created this DM.": "%(creator)s hat diese Direktnachricht erstellt.", "Now, let's help you get started": "Nun, lassen Sie uns Ihnen den Einstieg erleichtern", "Welcome %(name)s": "Willkommen %(name)s", "Add a photo so people know it's you.": "Fügen Sie ein Foto hinzu, damit die Leute wissen, dass Sie es sind.", @@ -3204,5 +3204,6 @@ "Don't want to add an existing room?": "Willst du keinen existierenden Raum hinzufügen?", "Edit devices": "Sitzungen anzeigen", "Your private space ": "Dein privater Space ", - "Your public space ": "Dein öffentlicher Space " + "Your public space ": "Dein öffentlicher Space ", + "Quick actions": "Schnellaktionen" } From a5743489cb0cc244937706a1e697b3db27bed6aa Mon Sep 17 00:00:00 2001 From: "@a2sc:matrix.org" Date: Thu, 8 Apr 2021 11:21:01 +0000 Subject: [PATCH 028/330] Translated using Weblate (German) Currently translated at 98.3% (2868 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index bb7ad2d0f9..675c27c9a3 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -776,7 +776,7 @@ "Failed to change settings": "Einstellungen konnten nicht geändert werden", "View Community": "Community ansehen", "Event sent!": "Event gesendet!", - "View Source": "Quellcode ansehen", + "View Source": "Rohdaten anzeigen", "Event Content": "Event-Inhalt", "Thank you!": "Danke!", "Uploaded on %(date)s by %(user)s": "Hochgeladen: %(date)s von %(user)s", @@ -1461,7 +1461,7 @@ "If your other sessions do not have the key for this message you will not be able to decrypt them.": "Wenn deine anderen Sitzungen nicht über den Schlüssel für diese Nachricht verfügen, kannst du die Nachricht nicht entschlüsseln.", "Re-request encryption keys from your other sessions.": "Fordere die Schlüssel aus deinen anderen Sitzungen erneut an.", "Room %(name)s": "Raum %(name)s", - "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Ein Upgrade dieses Raums schaltet die aktuelle Instanz des Raums ab und erstellt einen aktualisierten Raum mit demselben Namen.", + "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Ein Upgrade dieses Raums deaktiviert die aktuelle Instanz des Raums und erstellt einen aktualisierten Raum mit demselben Namen.", "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) hat sich zu einer neuen Sitzung angemeldet, ohne sie zu verifizieren:", "%(count)s verified sessions|other": "%(count)s verifizierte Sitzungen", "Hide verified sessions": "Verifizierte Sitzungen ausblenden", @@ -2659,7 +2659,7 @@ "Enter email address": "E-Mail-Adresse eingeben", "Open the link in the email to continue registration.": "Öffnen Sie den Link in der E-Mail, um mit der Registrierung fortzufahren.", "A confirmation email has been sent to %(emailAddress)s": "Eine Bestätigungs-E-Mail wurde an %(emailAddress)s gesendet", - "Use the + to make a new room or explore existing ones below": "Benutze das + um einen neuen Raum zu erstellen oder darunter um existierende Räume zu suchen", + "Use the + to make a new room or explore existing ones below": "Benutze das + um einen neuen Raum zu erstellen oder um existierende Räume zu entdecken", "Return to call": "Zurück zum Anruf", "Fill Screen": "Bildschirm ausfüllen", "Voice Call": "Sprachanruf", @@ -3205,5 +3205,25 @@ "Edit devices": "Sitzungen anzeigen", "Your private space ": "Dein privater Space ", "Your public space ": "Dein öffentlicher Space ", - "Quick actions": "Schnellaktionen" + "Quick actions": "Schnellaktionen", + "We couldn't create your DM.": "Wir konnten deine Direktnachricht nicht erstellen.", + "Adding...": "Hinzufügen...", + "Add existing rooms": "Bestehende Räume hinzufügen", + "Space selection": "Matrix-Space-Auswahl", + "%(count)s people you know have already joined|one": "%(count)s Person, die du kennst, ist schon beigetreten", + "%(count)s people you know have already joined|other": "%(count)s Leute, die du kennst, sind bereits beigetreten", + "Accept on your other login…": "Akzeptiere in deiner anderen Anmeldung…", + "Stop & send recording": "Stoppen und Aufzeichnung senden", + "Record a voice message": "Eine Sprachnachricht aufnehmen", + "Invite messages are hidden by default. Click to show the message.": "Einladungsnachrichten sind standardmäßig ausgeblendet. Klicken um diese anzuzeigen.", + "Warn before quitting": "Vor Beenden warnen", + "Spell check dictionaries": "Wörterbücher für Rechtschreibprüfung", + "Space options": "Matrix-Space-Optionen", + "Manage & explore rooms": "Räume entdecken und verwalten", + "unknown person": "unbekannte Person", + "Send and receive voice messages (in development)": "Sprachnachrichten senden und empfangen (in der Entwicklung)", + "Check your devices": "Überprüfe dein Gerät", + "%(deviceId)s from %(ip)s": "%(deviceId)s von %(ip)s", + "This homeserver has been blocked by it's administrator.": "Dieser Heimserver wurde von seiner Administration blockiert.", + "You have unverified logins": "Du hast nicht-bestätigte Anmeldungen" } From 00ad860bd0083a21f19cd145cbbd6760871f85d2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 04:58:54 +0000 Subject: [PATCH 029/330] Translated using Weblate (English (United States)) Currently translated at 20.0% (585 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/en_US/ --- src/i18n/strings/en_US.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index a1275fb089..baabce2fe7 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -650,5 +650,7 @@ "Error upgrading room": "Error upgrading room", "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.", "Changes the avatar of the current room": "Changes the avatar of the current room", - "Changes your avatar in all rooms": "Changes your avatar in all rooms" + "Changes your avatar in all rooms": "Changes your avatar in all rooms", + "Favourited": "Favorited", + "Explore rooms": "Explore rooms" } From b9ff02c5a5031f0f6c4d9f590c0cd3cd0dc948fd Mon Sep 17 00:00:00 2001 From: Qt Resynth Date: Sat, 10 Apr 2021 03:39:14 +0000 Subject: [PATCH 030/330] Translated using Weblate (English (United States)) Currently translated at 20.0% (585 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/en_US/ --- src/i18n/strings/en_US.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index baabce2fe7..7be8e8fa9e 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -652,5 +652,9 @@ "Changes the avatar of the current room": "Changes the avatar of the current room", "Changes your avatar in all rooms": "Changes your avatar in all rooms", "Favourited": "Favorited", - "Explore rooms": "Explore rooms" + "Explore rooms": "Explore rooms", + "Click the button below to confirm adding this email address.": "Click the button below to confirm adding this email address.", + "Confirm adding email": "Confirm adding email", + "Single Sign On": "Single Sign On", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirm adding this email address by using Single Sign On to prove your identity." } From bc250bbffbef087384dc8a7aa8355483972ea891 Mon Sep 17 00:00:00 2001 From: jeiannueva Date: Sat, 10 Apr 2021 03:38:54 +0000 Subject: [PATCH 031/330] Translated using Weblate (English (United States)) Currently translated at 20.0% (585 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/en_US/ --- src/i18n/strings/en_US.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 7be8e8fa9e..003da7ef8f 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -656,5 +656,6 @@ "Click the button below to confirm adding this email address.": "Click the button below to confirm adding this email address.", "Confirm adding email": "Confirm adding email", "Single Sign On": "Single Sign On", - "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirm adding this email address by using Single Sign On to prove your identity." + "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirm adding this email address by using Single Sign On to prove your identity.", + "Use Single Sign On to continue": "Use Single Sign On to continue" } From 457139f59097e570eefe4d22f94408a28dfd7d58 Mon Sep 17 00:00:00 2001 From: jelv Date: Wed, 7 Apr 2021 19:53:05 +0000 Subject: [PATCH 032/330] Translated using Weblate (Dutch) Currently translated at 100.0% (2917 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index f1c48cc539..de98a878e8 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -198,7 +198,7 @@ "Join Room": "Gesprek toetreden", "%(targetName)s joined the room.": "%(targetName)s is tot het gesprek toegetreden.", "Jump to first unread message.": "Spring naar het eerste ongelezen bericht.", - "Labs": "Experimenteel", + "Labs": "Labs", "Last seen": "Laatst gezien", "Leave room": "Gesprek verlaten", "%(targetName)s left the room.": "%(targetName)s heeft het gesprek verlaten.", @@ -1758,7 +1758,7 @@ "Cancelling…": "Bezig met annuleren…", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "In %(brand)s ontbreken enige modulen vereist voor het veilig lokaal bewaren van versleutelde berichten. Wilt u deze functie uittesten, compileer dan een aangepaste versie van %(brand)s Desktop die de zoekmodulen bevat.", "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Deze sessie maakt geen back-ups van uw sleutels, maar u beschikt over een reeds bestaande back-up waaruit u kunt herstellen en waaraan u nieuwe sleutels vanaf nu kunt toevoegen.", - "Customise your experience with experimental labs features. Learn more.": "Personaliseer uw ervaring met experimentele functies. Klik hier voor meer informatie.", + "Customise your experience with experimental labs features. Learn more.": "Personaliseer uw ervaring met experimentele labs functies. Lees verder.", "Cross-signing": "Kruiselings ondertekenen", "Your key share request has been sent - please check your other sessions for key share requests.": "Uw sleuteldeelverzoek is verstuurd - controleer de sleuteldeelverzoeken op uw andere sessies.", "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Sleuteldeelverzoeken worden automatisch naar andere sessies verstuurd. Als u op uw andere sessies het sleuteldeelverzoek geweigerd of genegeerd hebt, kunt u hier klikken op de sleutels voor deze sessie opnieuw aan te vragen.", From 32ae074ddff7a7536cdb9d8840d8f9c3d1e102d1 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 8 Apr 2021 06:34:20 +0000 Subject: [PATCH 033/330] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2917 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 39 ++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 33abcfe74e..c9bb9bb2d7 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3251,5 +3251,42 @@ "From %(deviceName)s (%(deviceId)s) at %(ip)s": "從 %(deviceName)s (%(deviceId)s) 於 %(ip)s", "Check your devices": "檢查您的裝置", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "新登入正在存取您的帳號:%(name)s (%(deviceID)s) 於 %(ip)s", - "You have unverified logins": "您有未驗證的登入" + "You have unverified logins": "您有未驗證的登入", + "unknown person": "不明身份的人", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "與 %(transferTarget)s 進行協商。轉讓至 %(transferee)s", + "Message search initilisation failed": "訊息搜尋初始化失敗", + "Invite to just this room": "邀請到此聊天室", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "如果這樣做,請注意,您的任何訊息都不會被刪除,但是在重新建立索引的同時,搜索體驗可能會降低片刻", + "Let's create a room for each of them.": "讓我們為每個主題建立一個聊天室吧。", + "Verify your identity to access encrypted messages and prove your identity to others.": "驗證您的身份來存取已加密的訊息並對其他人證明您的身份。", + "Sends the given message as a spoiler": "將指定訊息以劇透傳送", + "Review to ensure your account is safe": "請審閱以確保您的帳號安全", + "%(deviceId)s from %(ip)s": "從 %(ip)s 而來的 %(deviceId)s", + "Send and receive voice messages (in development)": "傳送與接收語音訊息(開發中)", + "Share decryption keys for room history when inviting users": "邀請使用者時分享聊天室歷史紀錄的解密金鑰", + "Manage & explore rooms": "管理與探索聊天室", + "Warn before quitting": "離開前警告", + "Quick actions": "快速動作", + "Invite messages are hidden by default. Click to show the message.": "邀請訊息預設隱藏。點擊以顯示訊息。", + "Record a voice message": "錄製語音訊息", + "Stop & send recording": "停止並傳送錄音", + "Accept on your other login…": "接受您的其他登入……", + "%(count)s people you know have already joined|other": "%(count)s 個您認識的人已加入", + "%(count)s people you know have already joined|one": "%(count)s 個您認識的人已加入", + "Add existing rooms": "新增既有聊天室", + "Adding...": "正在新增……", + "We couldn't create your DM.": "我們無法建立您的直接訊息。", + "Invited people will be able to read old messages.": "被邀請的人將能閱讀舊訊息。", + "Consult first": "先協商", + "Reset event store?": "重設活動儲存?", + "You most likely do not want to reset your event index store": "您很可能不想重設您的活動索引儲存", + "Reset event store": "重設活動儲存", + "Verify other login": "驗證其他登入", + "Avatar": "大頭貼", + "Verification requested": "已請求驗證", + "What are some things you want to discuss in %(spaceName)s?": "您想在 %(spaceName)s 中討論什麼?", + "You can add more later too, including already existing ones.": "您稍後可以新增更多內容,包含既有的。", + "Please choose a strong password": "請選擇強密碼", + "Use another login": "使用其他登入", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "未經驗證,您將無法存取您的所有訊息,且可能不被其他人信任。" } From 56635ce026bea64cccfb30a2fcc1b64519b2ebc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20L=C3=B8vbr=C3=B8tte=20Olsen?= Date: Wed, 7 Apr 2021 16:46:10 +0000 Subject: [PATCH 034/330] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 48.3% (1411 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nb_NO/ --- src/i18n/strings/nb_NO.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index 9fe5efddbb..9253f74fab 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -1508,7 +1508,7 @@ "End conference": "Avslutt konferanse", "You're already in a call with this person.": "Du er allerede i en samtale med denne personen.", "Already in call": "Allerede i en samtale", - "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Er du sikkert på at du vil fjerne '%(roomName)s' fra %(groupId)s?", + "Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Er du sikke på at du vil fjerne '%(roomName)s' fra %(groupId)s?", "Burundi": "Burundi", "Burkina Faso": "Burkina Faso", "Bulgaria": "Bulgaria", @@ -1541,14 +1541,14 @@ "Åland Islands": "Åland", "Afghanistan": "Afghanistan", "United Kingdom": "Storbritannia", - "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Din hjemmeserver kunne ikke nås, og kan derfor ikke logge deg inn. Vennligst prøv igjen. Hvis dette fortsetter, kontakt administratoren til din hjemmeserver", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Din hjemmeserver kunne ikke nås, og kan derfor ikke logge deg inn. Vennligst prøv igjen. Hvis dette fortsetter, kontakt administratoren til din hjemmeserver.", "Only continue if you trust the owner of the server.": "Fortsett kun om du stoler på eieren av serveren.", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Denne handlingen krever tilgang til standard identitetsserver for å kunne validere en epostaddresse eller telefonnummer, men serveren har ikke bruksvilkår.", "Too Many Calls": "For mange samtaler", "Call failed because webcam or microphone could not be accessed. Check that:": "Samtalen mislyktes fordi du fikk ikke tilgang til webkamera eller mikrofon. Sørg for at:", "Unable to access webcam / microphone": "Ingen tilgang til webkamera / mikrofon", "The call was answered on another device.": "Samtalen ble besvart på en annen enhet.", - "The call could not be established": "Samtalen kunne ikke etableres.", + "The call could not be established": "Samtalen kunne ikke etableres", "The other party declined the call.": "Den andre parten avviste samtalen.", "Call Declined": "Samtale avvist", "Click the button below to confirm adding this phone number.": "Klikk knappen nedenfor for å bekrefte dette telefonnummeret.", From 98cd0c2b4bcfef8f32c414be1950f31c604197e0 Mon Sep 17 00:00:00 2001 From: random Date: Thu, 8 Apr 2021 13:18:54 +0000 Subject: [PATCH 035/330] Translated using Weblate (Italian) Currently translated at 100.0% (2917 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 229b769c18..2d0edb77e6 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3248,5 +3248,42 @@ "Check your devices": "Controlla i tuoi dispositivi", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Una nuova sessione sta accedendo al tuo account: %(name)s (%(deviceID)s) al %(ip)s", "You have unverified logins": "Hai accessi non verificati", - "Open": "Apri" + "Open": "Apri", + "Send and receive voice messages (in development)": "Invia e ricevi messaggi vocali (in sviluppo)", + "unknown person": "persona sconosciuta", + "Sends the given message as a spoiler": "Invia il messaggio come spoiler", + "Review to ensure your account is safe": "Controlla per assicurarti che l'account sia sicuro", + "%(deviceId)s from %(ip)s": "%(deviceId)s da %(ip)s", + "Share decryption keys for room history when inviting users": "Condividi le chiavi di decifrazione della cronologia della stanza quando inviti utenti", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consultazione con %(transferTarget)s. Trasferisci a %(transferee)s", + "Manage & explore rooms": "Gestisci ed esplora le stanze", + "Invite to just this room": "Invita solo in questa stanza", + "%(count)s people you know have already joined|other": "%(count)s persone che conosci sono già entrate", + "%(count)s people you know have already joined|one": "%(count)s persona che conosci è già entrata", + "Message search initilisation failed": "Inizializzazione ricerca messaggi fallita", + "Add existing rooms": "Aggiungi stanze esistenti", + "Warn before quitting": "Avvisa prima di uscire", + "Invited people will be able to read old messages.": "Le persone invitate potranno leggere i vecchi messaggi.", + "You most likely do not want to reset your event index store": "Probabilmente non hai bisogno di reinizializzare il tuo archivio indice degli eventi", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Se lo fai, ricorda che nessuno dei tuoi messaggi verrà eliminato, ma l'esperienza di ricerca potrà peggiorare per qualche momento mentre l'indice viene ricreato", + "Avatar": "Avatar", + "Verification requested": "Verifica richiesta", + "What are some things you want to discuss in %(spaceName)s?": "Quali sono le cose di cui vuoi discutere in %(spaceName)s?", + "Please choose a strong password": "Scegli una password robusta", + "Quick actions": "Azioni rapide", + "Invite messages are hidden by default. Click to show the message.": "I messaggi di invito sono nascosti in modo predefinito. Clicca per mostrare il messaggio.", + "Record a voice message": "Registra un messaggio vocale", + "Stop & send recording": "Ferma e invia la registrazione", + "Accept on your other login…": "Accetta nella tua altra sessione…", + "Adding...": "Aggiunta...", + "We couldn't create your DM.": "Non abbiamo potuto creare il tuo messaggio diretto.", + "Consult first": "Prima consulta", + "Reset event store?": "Reinizializzare l'archivio eventi?", + "Reset event store": "Reinizializza archivio eventi", + "Verify other login": "Verifica l'altra sessione", + "Let's create a room for each of them.": "Creiamo una stanza per ognuno di essi.", + "You can add more later too, including already existing ones.": "Puoi aggiungerne anche altri in seguito, inclusi quelli già esistenti.", + "Use another login": "Usa un altro accesso", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifica la tua identità per accedere ai messaggi cifrati e provare agli altri che sei tu.", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Senza la verifica, non avrai accesso a tutti i tuoi messaggi e potresti apparire agli altri come non fidato." } From 6e4b55ed673c42f97ff72120c239318b4ce61ce0 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Thu, 8 Apr 2021 17:18:21 +0000 Subject: [PATCH 036/330] Translated using Weblate (Czech) Currently translated at 100.0% (2917 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ --- src/i18n/strings/cs.json | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index b03bd6a2b5..7f9ff9341c 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3165,5 +3165,42 @@ "Edit devices": "Upravit zařízení", "Check your devices": "Zkontrolujte svá zařízení", "You have unverified logins": "Máte neověřená přihlášení", - "Open": "Otevřít" + "Open": "Otevřít", + "Share decryption keys for room history when inviting users": "Při pozvání uživatelů sdílet dešifrovací klíče pro historii místnosti", + "Manage & explore rooms": "Spravovat a prozkoumat místnosti", + "Message search initilisation failed": "Inicializace vyhledávání zpráv se nezdařila", + "%(count)s people you know have already joined|one": "%(count)s osoba, kterou znáte, se již připojila", + "Invited people will be able to read old messages.": "Pozvaní lidé budou moci číst staré zprávy.", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Pokud tak učiníte, nezapomeňte, že žádná z vašich zpráv nebude smazána, ale vyhledávání může být na několik okamžiků degradováno, zatímco index bude znovu vytvářen", + "You can add more later too, including already existing ones.": "Později můžete přidat i další, včetně již existujících.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Ověřte svou identitu, abyste získali přístup k šifrovaným zprávám a prokázali svou identitu ostatním.", + "Sends the given message as a spoiler": "Odešle danou zprávu jako spoiler", + "Review to ensure your account is safe": "Zkontrolujte, zda je váš účet v bezpečí", + "%(deviceId)s from %(ip)s": "%(deviceId)s z %(ip)s", + "Send and receive voice messages (in development)": "Odesílat a přijímat hlasové zprávy (ve vývoji)", + "unknown person": "neznámá osoba", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Konzultace s %(transferTarget)s. Převod na %(transferee)s", + "Warn before quitting": "Varovat před ukončením", + "Invite to just this room": "Pozvat jen do této místnosti", + "Quick actions": "Rychlé akce", + "Invite messages are hidden by default. Click to show the message.": "Zprávy s pozvánkou jsou ve výchozím nastavení skryté. Kliknutím zobrazíte zprávu.", + "Record a voice message": "Nahrát hlasovou zprávu", + "Stop & send recording": "Zastavit a odeslat záznam", + "Accept on your other login…": "Přijměte ve svém dalším přihlášení…", + "%(count)s people you know have already joined|other": "%(count)s lidí, které znáte, se již připojili", + "Add existing rooms": "Přidat stávající místnosti", + "Adding...": "Přidávání...", + "We couldn't create your DM.": "Nemohli jsme vytvořit vaši přímou zprávu.", + "Consult first": "Nejprve se poraďte", + "You most likely do not want to reset your event index store": "Pravděpodobně nechcete resetovat úložiště indexů událostí", + "Reset event store": "Resetovat úložiště událostí", + "Reset event store?": "Resetovat úložiště událostí?", + "Verify other login": "Ověřit další přihlášení", + "Avatar": "Avatar", + "Verification requested": "Žádost ověření", + "Please choose a strong password": "Vyberte silné heslo", + "What are some things you want to discuss in %(spaceName)s?": "O kterých tématech chcete diskutovat v %(spaceName)s?", + "Let's create a room for each of them.": "Vytvořme pro každé z nich místnost.", + "Use another login": "Použijte jiné přihlašovací jméno", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Bez ověření nebudete mít přístup ke všem svým zprávám a ostatním se můžete zobrazit jako nedůvěryhodný." } From f34d4b95dcfbe7e13a498b7cabfe8a93af29380d Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 8 Apr 2021 04:37:24 +0000 Subject: [PATCH 037/330] Translated using Weblate (Galician) Currently translated at 100.0% (2917 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 026207030e..f6ebce684f 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3248,5 +3248,42 @@ "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Desde %(deviceName)s%(deviceId)s en %(ip)s", "Check your devices": "Comproba os teus dispositivos", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Hai unha nova conexión á túa conta: %(name)s %(deviceID)s desde %(ip)s", - "You have unverified logins": "Tes conexións sen verificar" + "You have unverified logins": "Tes conexións sen verificar", + "Sends the given message as a spoiler": "Envía a mensaxe dada como un spoiler", + "Review to ensure your account is safe": "Revisa para asegurarte de que a túa conta está protexida", + "Share decryption keys for room history when inviting users": "Comparte chaves de descifrado para o historial da sala ao convidar usuarias", + "Warn before quitting": "Aviso antes de saír", + "Invite to just this room": "Convida só a esta sala", + "Stop & send recording": "Deter e enviar e a gravación", + "We couldn't create your DM.": "Non puidemos crear o teu MD.", + "Invited people will be able to read old messages.": "As persoas convidadas poderán ler as mensaxes antigas.", + "Reset event store?": "Restablecer almacenaxe do evento?", + "You most likely do not want to reset your event index store": "Probablemente non queiras restablecer o índice de almacenaxe do evento", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Se o fas, ten en conta que ningunha das mensaxes será eliminada, pero a experiencia de busca podería degradarse durante o tempo en que o índice volve a crearse", + "Avatar": "Avatar", + "Please choose a strong password": "Escolle un contrasinal forte", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifica a túa identidade para acceder a mensaxes cifradas e acreditar a túa identidade ante outras.", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Sen verificación, non terás acceso a tódalas túas mensaxes e poderías aparecer antes outras como non confiable.", + "%(deviceId)s from %(ip)s": "%(deviceId)s desde %(ip)s", + "Send and receive voice messages (in development)": "Enviar e recibir mensaxes de voz (en desenvolvemento)", + "unknown person": "persoa descoñecida", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consultando con %(transferTarget)s. Transferir a %(transferee)s", + "Manage & explore rooms": "Xestionar e explorar salas", + "Message search initilisation failed": "Fallo a inicialización da busca de mensaxes", + "Quick actions": "Accións rápidas", + "Invite messages are hidden by default. Click to show the message.": "As mensaxes de convite están agochadas por defecto. Preme para amosar a mensaxe.", + "Record a voice message": "Gravar mensaxe de voz", + "Accept on your other login…": "Acepta na túa outra sesión…", + "%(count)s people you know have already joined|other": "%(count)s persoas que coñeces xa se uniron", + "%(count)s people you know have already joined|one": "%(count)s persoa que coñeces xa se uniu", + "Add existing rooms": "Engadir salas existentes", + "Adding...": "Engadindo...", + "Consult first": "Preguntar primeiro", + "Reset event store": "Restablecer almacenaxe de eventos", + "Verify other login": "Verificar outra conexión", + "Verification requested": "Verificación solicitada", + "What are some things you want to discuss in %(spaceName)s?": "Sobre que temas queres conversar en %(spaceName)s?", + "Let's create a room for each of them.": "Crea unha sala para cada un deles.", + "You can add more later too, including already existing ones.": "Podes engadir máis posteriormente, incluíndo os xa existentes.", + "Use another login": "Usar outra conexión" } From 460650ea7cd25fdcb4c4110e96ae83abbed81a32 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 07:31:42 +0000 Subject: [PATCH 038/330] Translated using Weblate (Hebrew) Currently translated at 92.4% (2696 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/he/ --- src/i18n/strings/he.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index dda9902e72..5baa1d7c67 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -52,7 +52,7 @@ "Operation failed": "פעולה נכשלה", "Search": "חפש", "Custom Server Options": "הגדרות שרת מותאמות אישית", - "Dismiss": "שחרר", + "Dismiss": "התעלם", "powered by Matrix": "מופעל ע\"י Matrix", "Error": "שגיאה", "Remove": "הסר", From 3879a7449d33a12f2f5286d1a130cf171c977261 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 02:02:14 +0000 Subject: [PATCH 039/330] Translated using Weblate (Croatian) Currently translated at 0.2% (7 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hr/ --- src/i18n/strings/hr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hr.json b/src/i18n/strings/hr.json index 527b86e0a7..2511771578 100644 --- a/src/i18n/strings/hr.json +++ b/src/i18n/strings/hr.json @@ -4,5 +4,6 @@ "Failed to verify email address: make sure you clicked the link in the email": "Nismo u mogućnosti verificirati Vašu email adresu. Provjerite dali ste kliknuli link u mailu", "The platform you're on": "Platforma na kojoj se nalazite", "The version of %(brand)s": "Verzija %(brand)s", - "Your language of choice": "Izabrani jezik" + "Your language of choice": "Izabrani jezik", + "Dismiss": "Odbaci" } From 3fd32698f61c3c1b805845b06eb507c4552d75b7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 02:31:40 +0000 Subject: [PATCH 040/330] Translated using Weblate (Vietnamese) Currently translated at 9.8% (286 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/vi/ --- src/i18n/strings/vi.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index 744310675c..eebbaef3d0 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -293,5 +293,7 @@ "Enable URL previews by default for participants in this room": "Bật mặc định xem trước nội dung đường link cho mọi người trong phòng", "Room Colour": "Màu phòng chat", "Enable widget screenshots on supported widgets": "Bật widget chụp màn hình cho các widget có hỗ trợ", - "Sign In": "Đăng nhập" + "Sign In": "Đăng nhập", + "Explore rooms": "Khám phá phòng chat", + "Create Account": "Tạo tài khoản" } From a5a91f015ec1aae2977bf4732b9c489b88b67069 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 12:40:56 +0000 Subject: [PATCH 041/330] Translated using Weblate (Arabic) Currently translated at 51.4% (1502 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ar/ --- src/i18n/strings/ar.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ar.json b/src/i18n/strings/ar.json index 67b5426d09..cc63995e0f 100644 --- a/src/i18n/strings/ar.json +++ b/src/i18n/strings/ar.json @@ -1551,5 +1551,6 @@ "You've reached the maximum number of simultaneous calls.": "لقد وصلت للحد الاقصى من المكالمات المتزامنة.", "Too Many Calls": "مكالمات كثيرة جدا", "Call failed because webcam or microphone could not be accessed. Check that:": "فشلت المكالمة لعدم امكانية الوصل للميكروفون او الكاميرا , من فضلك قم بالتأكد.", - "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح." + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح.", + "Explore rooms": "استكشِف الغرف" } From dda0d56b7a57fe9621dfc106bf3907f98a157fb2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 02:15:24 +0000 Subject: [PATCH 042/330] Translated using Weblate (West Flemish) Currently translated at 44.7% (1306 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/vls/ --- src/i18n/strings/vls.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/vls.json b/src/i18n/strings/vls.json index 1172804efa..75ab903ebe 100644 --- a/src/i18n/strings/vls.json +++ b/src/i18n/strings/vls.json @@ -1443,5 +1443,7 @@ "Terms of service not accepted or the identity server is invalid.": "Dienstvoorwoardn nie anveird, of den identiteitsserver is oungeldig.", "Enter a new identity server": "Gift e nieuwen identiteitsserver in", "Remove %(email)s?": "%(email)s verwydern?", - "Remove %(phone)s?": "%(phone)s verwydern?" + "Remove %(phone)s?": "%(phone)s verwydern?", + "Explore rooms": "Gesprekkn ountdekkn", + "Create Account": "Account anmoakn" } From 4f5ee0896c20ab69cf89017a2cc9713a63976ed7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 18:01:48 +0000 Subject: [PATCH 043/330] Translated using Weblate (Polish) Currently translated at 72.3% (2110 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 9fa9c7555e..ab9a478446 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -1277,8 +1277,8 @@ "Enable desktop notifications for this session": "Włącz powiadomienia na pulpicie dla tej sesji", "Enable audible notifications for this session": "Włącz powiadomienia dźwiękowe dla tej sesji", "Direct Messages": "Wiadomości bezpośrednie", - "Create Account": "Utwórz konto", - "Sign In": "Zaloguj się", + "Create Account": "Stwórz konto", + "Sign In": "Zaloguj", "a few seconds ago": "kilka sekund temu", "%(num)s minutes ago": "%(num)s minut temu", "%(num)s hours ago": "%(num)s godzin temu", From cde8fa7ac9e59c0278cd2bebea6b9b74ef7ff35b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 01:00:51 +0000 Subject: [PATCH 044/330] Translated using Weblate (Azerbaijani) Currently translated at 12.2% (357 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/az/ --- src/i18n/strings/az.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/az.json b/src/i18n/strings/az.json index 5a8dec76f0..987cef73b2 100644 --- a/src/i18n/strings/az.json +++ b/src/i18n/strings/az.json @@ -380,5 +380,8 @@ "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "Bu otaqda %(groups)s üçün %(senderDisplayName)s aktiv oldu.", "powered by Matrix": "Matrix tərəfindən təchiz edilmişdir", "Custom Server Options": "Fərdi Server Seçimləri", - "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Bu otaqda %(newGroups)s üçün aktiv və %(oldGroups)s üçün %(senderDisplayName)s deaktiv oldu." + "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Bu otaqda %(newGroups)s üçün aktiv və %(oldGroups)s üçün %(senderDisplayName)s deaktiv oldu.", + "Create Account": "Hesab Aç", + "Explore rooms": "Otaqları kəşf edin", + "Sign In": "Daxil ol" } From 9dd7e0f4bf44024f58da801b16ce8c2dbe0c9d73 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 01:22:19 +0000 Subject: [PATCH 045/330] Translated using Weblate (Tamil) Currently translated at 5.5% (162 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ta/ --- src/i18n/strings/ta.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ta.json b/src/i18n/strings/ta.json index 9cb046ed39..4f87230ef3 100644 --- a/src/i18n/strings/ta.json +++ b/src/i18n/strings/ta.json @@ -179,5 +179,7 @@ "Mar": "மார்ச்", "Apr": "ஏப்ரல்", "May": "மே", - "Jun": "ஜூன்" + "Jun": "ஜூன்", + "Explore rooms": "அறைகளை ஆராயுங்கள்", + "Create Account": "உங்கள் கணக்கை துவங்குங்கள்" } From 8d45a7b463cefe07f8c5ecbf77311ea3d2872b6c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 08:27:00 +0000 Subject: [PATCH 046/330] Translated using Weblate (Malayalam) Currently translated at 3.8% (111 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ml/ --- src/i18n/strings/ml.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ml.json b/src/i18n/strings/ml.json index 23740fefda..6183fe7de2 100644 --- a/src/i18n/strings/ml.json +++ b/src/i18n/strings/ml.json @@ -127,5 +127,8 @@ "Failed to change settings": "സജ്ജീകരണങ്ങള്‍ മാറ്റുന്നവാന്‍ സാധിച്ചില്ല", "View Source": "സോഴ്സ് കാണുക", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "നിങ്ങളുടെ ഇപ്പോളത്തെ ബ്രൌസര്‍ റയട്ട് പ്രവര്‍ത്തിപ്പിക്കാന്‍ പൂര്‍ണമായും പര്യാപത്മല്ല. പല ഫീച്ചറുകളും പ്രവര്‍ത്തിക്കാതെയിരിക്കാം. ഈ ബ്രൌസര്‍ തന്നെ ഉപയോഗിക്കണമെങ്കില്‍ മുന്നോട്ട് പോകാം. പക്ഷേ നിങ്ങള്‍ നേരിടുന്ന പ്രശ്നങ്ങള്‍ നിങ്ങളുടെ ഉത്തരവാദിത്തത്തില്‍ ആയിരിക്കും!", - "Checking for an update...": "അപ്ഡേറ്റ് ഉണ്ടോ എന്ന് തിരയുന്നു..." + "Checking for an update...": "അപ്ഡേറ്റ് ഉണ്ടോ എന്ന് തിരയുന്നു...", + "Explore rooms": "മുറികൾ കണ്ടെത്തുക", + "Sign In": "പ്രവേശിക്കുക", + "Create Account": "അക്കൗണ്ട് സൃഷ്ടിക്കുക" } From 256caed209a097541c31a0ddedb38b5a0d3d962d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 02:56:25 +0000 Subject: [PATCH 047/330] Translated using Weblate (Occitan) Currently translated at 11.1% (325 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/oc/ --- src/i18n/strings/oc.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/oc.json b/src/i18n/strings/oc.json index cd62ff69db..1fcea5a976 100644 --- a/src/i18n/strings/oc.json +++ b/src/i18n/strings/oc.json @@ -62,7 +62,7 @@ "Server error": "Error servidor", "Single Sign On": "Autentificacion unica", "Confirm": "Confirmar", - "Dismiss": "Far desaparéisser", + "Dismiss": "Refusar", "OK": "D’acòrdi", "Continue": "Contunhar", "Go Back": "En arrièr", @@ -118,7 +118,7 @@ "Incoming call": "Sonada entranta", "Accept": "Acceptar", "Start": "Començament", - "Cancelling…": "Anullacion...", + "Cancelling…": "Anullacion…", "Fish": "Pes", "Butterfly": "Parpalhòl", "Tree": "Arborescéncia", @@ -338,5 +338,7 @@ "Esc": "Escap", "Enter": "Entrada", "Space": "Espaci", - "End": "Fin" + "End": "Fin", + "Explore rooms": "Percórrer las salas", + "Create Account": "Crear un compte" } From 36b6e2088cca77a58f6e4ac95a3ba4547f4c989d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 09:45:24 +0000 Subject: [PATCH 048/330] Translated using Weblate (Portuguese (Brazil)) Currently translated at 96.9% (2827 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pt_BR/ --- src/i18n/strings/pt_BR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 0ec835362a..8497ae7164 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -1175,7 +1175,7 @@ "Learn More": "Saiba mais", "Sign In or Create Account": "Faça login ou crie uma conta", "Use your account or create a new one to continue.": "Use sua conta ou crie uma nova para continuar.", - "Create Account": "Criar conta", + "Create Account": "Criar Conta", "Sign In": "Entrar", "Custom (%(level)s)": "Personalizado (%(level)s)", "Messages": "Mensagens", From 269cd79531ed6177b76b94cb1b65a84cc4f8342f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 11:08:48 +0000 Subject: [PATCH 049/330] Translated using Weblate (Thai) Currently translated at 11.7% (342 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/th/ --- src/i18n/strings/th.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/th.json b/src/i18n/strings/th.json index 811d549d54..a5b641921c 100644 --- a/src/i18n/strings/th.json +++ b/src/i18n/strings/th.json @@ -378,5 +378,8 @@ "Unable to fetch notification target list": "ไม่สามารถรับรายชื่ออุปกรณ์แจ้งเตือน", "Quote": "อ้างอิง", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "การแสดงผลของโปรแกรมอาจผิดพลาด ฟังก์ชันบางอย่างหรือทั้งหมดอาจไม่ทำงานในเบราว์เซอร์ปัจจุบันของคุณ หากคุณต้องการลองดำเนินการต่อ คุณต้องรับมือกับปัญหาที่อาจจะเกิดขึ้นด้วยตัวคุณเอง!", - "Checking for an update...": "กำลังตรวจหาอัปเดต..." + "Checking for an update...": "กำลังตรวจหาอัปเดต...", + "Explore rooms": "สำรวจห้อง", + "Sign In": "เข้าสู่ระบบ", + "Create Account": "สร้างบัญชี" } From 417d384e37170e18c9f6012a0132a83ae6b8381e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 02:47:53 +0000 Subject: [PATCH 050/330] Translated using Weblate (Mongolian) Currently translated at 0.1% (4 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/mn/ --- src/i18n/strings/mn.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/mn.json b/src/i18n/strings/mn.json index 0967ef424b..5e44298332 100644 --- a/src/i18n/strings/mn.json +++ b/src/i18n/strings/mn.json @@ -1 +1,6 @@ -{} +{ + "Explore rooms": "Өрөөнүүд үзэх", + "Sign In": "Нэвтрэх", + "Create Account": "Хэрэглэгч үүсгэх", + "Dismiss": "Орхих" +} From a494871727ebdfab561979ea72279c8c31c87a77 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 02:45:26 +0000 Subject: [PATCH 051/330] Translated using Weblate (Welsh) Currently translated at 0.4% (13 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cy/ --- src/i18n/strings/cy.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json index 99c5296be5..b99b834636 100644 --- a/src/i18n/strings/cy.json +++ b/src/i18n/strings/cy.json @@ -8,5 +8,8 @@ "The version of %(brand)s": "Fersiwn %(brand)s", "Whether or not you're logged in (we don't record your username)": "Os ydych wedi mewngofnodi ai peidio (nid ydym yn cofnodi'ch enw defnyddiwr)", "Your language of choice": "Eich iaith o ddewis", - "The version of %(brand)s": "Fersiwn %(brand)s" + "Sign In": "Mewngofnodi", + "Create Account": "Creu Cyfrif", + "Dismiss": "Wfftio", + "Explore rooms": "Archwilio Ystafelloedd" } From 691bfcfeb71c258f2b6ea992f791a28c6bc19654 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 02:45:14 +0000 Subject: [PATCH 052/330] Translated using Weblate (Serbian (latin)) Currently translated at 2.0% (60 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sr_Latn/ --- src/i18n/strings/sr_Latn.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sr_Latn.json b/src/i18n/strings/sr_Latn.json index 19778858d0..96a5d89411 100644 --- a/src/i18n/strings/sr_Latn.json +++ b/src/i18n/strings/sr_Latn.json @@ -58,5 +58,6 @@ "Failed to invite users to the room:": "Nije uspelo pozivanje korisnika u sobu:", "You need to be logged in.": "Morate biti prijavljeni", "You need to be able to invite users to do that.": "Mora vam biti dozvoljeno da pozovete korisnike kako bi to uradili.", - "Failed to send request.": "Slanje zahteva nije uspelo." + "Failed to send request.": "Slanje zahteva nije uspelo.", + "Create Account": "Napravite nalog" } From 893d37cbe1dd76ae3fb367dadef9d1782ebbb7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 7 Apr 2021 17:41:46 +0000 Subject: [PATCH 053/330] Translated using Weblate (Estonian) Currently translated at 100.0% (2917 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 41 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 85f8cbb751..444475deea 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -2517,7 +2517,7 @@ "Join the conference from the room information card on the right": "Liitu konverentsiga selle jututoa infolehelt paremal", "Video conference ended by %(senderName)s": "%(senderName)s lõpetas video rühmakõne", "Video conference updated by %(senderName)s": "%(senderName)s uuendas video rühmakõne", - "Video conference started by %(senderName)s": "%(senderName)s alustas video rühmakõne", + "Video conference started by %(senderName)s": "%(senderName)s alustas video rühmakõnet", "End conference": "Lõpeta videokonverents", "This will end the conference for everyone. Continue?": "Sellega lõpetame kõikide osalejate jaoks videokonverentsi. Nõus?", "Ignored attempt to disable encryption": "Eirasin katset lõpetada krüptimise kasutamine", @@ -3226,5 +3226,42 @@ "Open": "Ava", "Check your devices": "Kontrolli oma seadmeid", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Uus sisselogimissessioon kasutab sinu Matrixi kontot: %(name)s %(deviceID)s aadressil %(ip)s", - "You have unverified logins": "Sul on verifitseerimata sisselogimissessioone" + "You have unverified logins": "Sul on verifitseerimata sisselogimissessioone", + "Manage & explore rooms": "Halda ja uuri jututubasid", + "Warn before quitting": "Hoiata enne rakenduse töö lõpetamist", + "Invite to just this room": "Kutsi vaid siia jututuppa", + "Quick actions": "Kiirtoimingud", + "Adding...": "Lisan...", + "Sends the given message as a spoiler": "Saadab selle sõnumi rõõmurikkujana", + "unknown person": "tundmatu isik", + "Send and receive voice messages (in development)": "Saada ja võta vastu häälsõnumeid (arendusjärgus)", + "%(deviceId)s from %(ip)s": "%(deviceId)s ip-aadressil %(ip)s", + "Review to ensure your account is safe": "Tagamaks, et su konto on sinu kontrolli all, vaata andmed üle", + "Share decryption keys for room history when inviting users": "Kasutajate kutsumisel jaga jututoa ajaloo võtmeid", + "Record a voice message": "Salvesta häälsõnum", + "Stop & send recording": "Lõpeta salvestamine ja saada häälsõnum", + "Add existing rooms": "Lisa olemasolevaid jututubasid", + "%(count)s people you know have already joined|other": "%(count)s sulle tuttavat kasutajat on juba liitunud", + "We couldn't create your DM.": "Otsesuhtluse loomine ei õnnestunud.", + "Invited people will be able to read old messages.": "Kutse saanud kasutajad saavad lugeda vanu sõnumeid.", + "Consult first": "Pea esmalt nõu", + "Reset event store?": "Kas lähtestame sündmuste andmekogu?", + "Reset event store": "Lähtesta sündmuste andmekogu", + "Verify other login": "Verifitseeri muu sisselogimissessioon", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Suhtlen teise osapoolega %(transferTarget)s. Saadan andmeid kasutajale %(transferee)s", + "Message search initilisation failed": "Sõnumite otsingu alustamine ei õnnestunud", + "Invite messages are hidden by default. Click to show the message.": "Kutsed on vaikimisi peidetud. Sõnumi nägemiseks klõpsi.", + "Accept on your other login…": "Nõustu oma teise sisselogimissessiooniga…", + "Avatar": "Tunnuspilt", + "Verification requested": "Verifitseerimistaotlus on saadetud", + "%(count)s people you know have already joined|one": "%(count)s sulle tuttav kasutaja on juba liitunud", + "You most likely do not want to reset your event index store": "Pigem sa siiski ei taha lähtestada sündmuste andmekogu ja selle indeksit", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Kui sa siiski soovid seda teha, siis sinu sõnumeid me ei kustuta, aga seniks kuni sõnumite indeks taustal uuesti luuakse, toimib otsing aeglaselt ja ebatõhusalt", + "You can add more later too, including already existing ones.": "Sa võid ka hiljem siia luua uusi jututubasid või lisada olemasolevaid.", + "What are some things you want to discuss in %(spaceName)s?": "Mida sa sooviksid arutada %(spaceName)s kogukonnakeskuses?", + "Please choose a strong password": "Palun tee üks korralik salasõna", + "Use another login": "Pruugi muud kasutajakontot", + "Verify your identity to access encrypted messages and prove your identity to others.": "Tagamaks ligipääsu oma krüptitud sõnumitele ja tõestamaks oma isikut teistele kasutajatale, verifitseeri end.", + "Let's create a room for each of them.": "Teeme siis iga teema jaoks oma jututoa.", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Ilma verifitseerimiseta sul puudub ligipääs kõikidele oma sõnumitele ning teised ei näe sinu kasutajakontot usaldusväärsena." } From 2a236e020ad52da7523fc87aab19e8f36da674d9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 09:12:42 +0000 Subject: [PATCH 054/330] Translated using Weblate (Portuguese) Currently translated at 16.8% (492 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pt/ --- src/i18n/strings/pt.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index f72edc150d..4047aae760 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -569,5 +569,8 @@ "Try using turn.matrix.org": "Tente utilizar turn.matrix.org", "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Quer esteja a usar o %(brand)s num dispositivo onde o touch é o mecanismo de entrada primário", "Whether you're using %(brand)s as an installed Progressive Web App": "Quer esteja a usar o %(brand)s como uma Progressive Web App (PWA)", - "Your user agent": "O seu user agent" + "Your user agent": "O seu user agent", + "Explore rooms": "Explorar rooms", + "Sign In": "Iniciar sessão", + "Create Account": "Criar conta" } From f4ec4ec8fd6f9601903fe9384cd9ac261b34beef Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 02:07:49 +0000 Subject: [PATCH 055/330] Translated using Weblate (Slovenian) Currently translated at 1.0% (30 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sl/ --- src/i18n/strings/sl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sl.json b/src/i18n/strings/sl.json index 0e9bdb3d3e..aa2019ad45 100644 --- a/src/i18n/strings/sl.json +++ b/src/i18n/strings/sl.json @@ -27,5 +27,7 @@ "Your homeserver's URL": "URL domačega strežnika", "End": "Konec", "Use default": "Uporabi privzeto", - "Change": "Sprememba" + "Change": "Sprememba", + "Explore rooms": "Raziščite sobe", + "Create Account": "Registracija" } From 4450313741ad567ae24949dcdda7b4ccb5047e70 Mon Sep 17 00:00:00 2001 From: iaiz Date: Sat, 10 Apr 2021 22:24:22 +0000 Subject: [PATCH 056/330] Translated using Weblate (Spanish) Currently translated at 99.9% (2916 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 202c6d4d71..785ac28f37 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3211,5 +3211,18 @@ "Send and receive voice messages (in development)": "Enviar y recibir mensajes de voz (en desarrollo)", "%(deviceId)s from %(ip)s": "%(deviceId)s desde %(ip)s", "Review to ensure your account is safe": "Revisa que tu cuenta esté segura", - "Sends the given message as a spoiler": "Envía el mensaje como un spoiler" + "Sends the given message as a spoiler": "Envía el mensaje como un spoiler", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Consultando a %(transferTarget)s. Transferir a %(transferee)s", + "Message search initilisation failed": "Ha fallado la inicialización de la búsqueda de mensajes", + "Reset event store?": "¿Restablecer almacenamiento de eventos?", + "You most likely do not want to reset your event index store": "Lo más probable es que no quieras restablecer tu almacenamiento de índice de ecentos", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Si lo haces, ten en cuenta que no se borrarán tus mensajes, pero la experiencia de búsqueda será peor durante unos momentos mientras se recrea el índice", + "Reset event store": "Restablecer el almacenamiento de eventos", + "What are some things you want to discuss in %(spaceName)s?": "¿De qué quieres hablar en %(spaceName)s?", + "Let's create a room for each of them.": "Crearemos una sala para cada uno.", + "You can add more later too, including already existing ones.": "Puedes añadir más después, incluso si ya existen.", + "Please choose a strong password": "Por favor, elige una contraseña segura", + "Use another login": "Usar otro inicio de sesión", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifica tu identidad para acceder a mensajes cifrados y probar tu identidad a otros.", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Si no verificas no tendrás acceso a todos tus mensajes y puede que aparezcas como no confiable para otros usuarios." } From 986443ba18f312141d60a78bc54a043f102c04da Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 03:46:17 +0000 Subject: [PATCH 057/330] Translated using Weblate (Danish) Currently translated at 19.9% (582 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/da/ --- src/i18n/strings/da.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/da.json b/src/i18n/strings/da.json index 1f4eef4d93..32dba2f063 100644 --- a/src/i18n/strings/da.json +++ b/src/i18n/strings/da.json @@ -54,7 +54,7 @@ "OK": "OK", "Search": "Søg", "Custom Server Options": "Brugerdefinerede serverindstillinger", - "Dismiss": "Afskedige", + "Dismiss": "Afslut", "powered by Matrix": "Drevet af Matrix", "Close": "Luk", "Cancel": "Afbryd", From 33332c1677fb9a1a432f28f5a1fcd9d541d2b1c3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 21:45:36 +0000 Subject: [PATCH 058/330] Translated using Weblate (Catalan) Currently translated at 28.2% (825 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ca/ --- src/i18n/strings/ca.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json index 7db987b8af..9a9e0efaa7 100644 --- a/src/i18n/strings/ca.json +++ b/src/i18n/strings/ca.json @@ -950,5 +950,6 @@ "Confirm": "Confirma", "Click the button below to confirm adding this email address.": "Fes clic al botó de sota per confirmar l'addició d'aquesta adreça de correu electrònic.", "Unable to access webcam / microphone": "No s'ha pogut accedir a la càmera web / micròfon", - "Unable to access microphone": "No s'ha pogut accedir al micròfon" + "Unable to access microphone": "No s'ha pogut accedir al micròfon", + "Explore rooms": "Explora sales" } From 7f8995a5c7a8c65aa492f4b5f8afb606b97c01c7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 02:50:12 +0000 Subject: [PATCH 059/330] Translated using Weblate (Kabyle) Currently translated at 85.2% (2486 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/kab/ --- src/i18n/strings/kab.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json index c4e0cc7099..b6e1b3020f 100644 --- a/src/i18n/strings/kab.json +++ b/src/i18n/strings/kab.json @@ -2,7 +2,7 @@ "Confirm": "Sentem", "Analytics": "Tiselḍin", "Error": "Tuccḍa", - "Dismiss": "Agi", + "Dismiss": "Agwi", "OK": "IH", "Permission Required": "Tasiregt tlaq", "Continue": "Kemmel", From c394f2da44a8028327ca12a4c48af4b170236152 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 01:53:48 +0000 Subject: [PATCH 060/330] Translated using Weblate (Lojban) Currently translated at 16.0% (469 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/jbo/ --- src/i18n/strings/jbo.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/jbo.json b/src/i18n/strings/jbo.json index f2c9dc6e43..b19d4bb95d 100644 --- a/src/i18n/strings/jbo.json +++ b/src/i18n/strings/jbo.json @@ -580,5 +580,8 @@ "%(displayName)s cancelled verification.": ".i la'o zoi. %(displayName)s .zoi co'u co'a lacri", "Decrypt %(text)s": "nu facki le du'u mifra la'o zoi. %(text)s .zoi", "Download %(text)s": "nu kibycpa la'o zoi. %(text)s .zoi", - "Download this file": "nu kibycpa le vreji" + "Download this file": "nu kibycpa le vreji", + "Explore rooms": "nu facki le du'u ve zilbe'i", + "Create Account": "nu pa re'u co'a jaspu", + "Dismiss": "nu mipri" } From 979b7fedb07711d8728cec0c88241f3d818fd59f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 19:05:27 +0000 Subject: [PATCH 061/330] Translated using Weblate (Persian) Currently translated at 9.7% (285 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fa/ --- src/i18n/strings/fa.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 738f48733c..d036a55c23 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -311,5 +311,8 @@ "Your device resolution": "وضوح دستگاه شما", "e.g. ": "برای مثال ", "Every page you use in the app": "هر صفحه‌ی برنامه از که آن استفاده می‌کنید", - "e.g. %(exampleValue)s": "برای مثال %(exampleValue)s" + "e.g. %(exampleValue)s": "برای مثال %(exampleValue)s", + "Explore rooms": "کاوش اتاق", + "Sign In": "ورود", + "Create Account": "ایجاد اکانت" } From deb307e0c58b1c0f30c87268159a20626555372e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 10 Apr 2021 13:13:14 +0000 Subject: [PATCH 062/330] Translated using Weblate (Korean) Currently translated at 51.8% (1512 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 59bb68af94..f817dbc26b 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -1666,5 +1666,6 @@ "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "경고: 키 검증 실패! 제공된 키인 \"%(fingerprint)s\"가 사용자 %(userId)s와 %(deviceId)s 세션의 서명 키인 \"%(fprint)s\"와 일치하지 않습니다. 이는 통신이 탈취되고 있는 중일 수도 있다는 뜻입니다!", "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "사용자 %(userId)s의 세션 %(deviceId)s에서 받은 서명 키와 당신이 제공한 서명 키가 일치합니다. 세션이 검증되었습니다.", "Show more": "더 보기", - "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "비밀번호를 변경한다면 방의 암호화 키를 내보낸 후 다시 가져오지 않는 이상 모든 종단간 암호화 키는 초기화 될 것이고, 암호화된 대화 내역은 읽을 수 없게 될 것입니다. 이 문제는 추후에 개선될 것입니다." + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "비밀번호를 변경한다면 방의 암호화 키를 내보낸 후 다시 가져오지 않는 이상 모든 종단간 암호화 키는 초기화 될 것이고, 암호화된 대화 내역은 읽을 수 없게 될 것입니다. 이 문제는 추후에 개선될 것입니다.", + "Create Account": "계정 만들기" } From f10b6ed882f8d7c91d5c5dde89918b4ad2ebbd06 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 01:24:14 +0000 Subject: [PATCH 063/330] Translated using Weblate (Romanian) Currently translated at 2.4% (72 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ro/ --- src/i18n/strings/ro.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ro.json b/src/i18n/strings/ro.json index aa87d0a912..062a89f2e3 100644 --- a/src/i18n/strings/ro.json +++ b/src/i18n/strings/ro.json @@ -70,5 +70,9 @@ "Add to community": "Adăugați la comunitate", "Failed to invite the following users to %(groupId)s:": "Nu a putut fi invitat următorii utilizatori %(groupId)s", "Failed to invite users to community": "Nu a fost posibilă invitarea utilizatorilor la comunitate", - "Failed to invite users to %(groupId)s": "Nu a fost posibilă invitarea utilizatorilor la %(groupId)s" + "Failed to invite users to %(groupId)s": "Nu a fost posibilă invitarea utilizatorilor la %(groupId)s", + "Explore rooms": "Explorează camerele", + "Sign In": "Autentificare", + "Create Account": "Înregistare", + "Dismiss": "Închide" } From d1a2ca4bbe71d3916f0e483372f3dfbe579c4fe0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Apr 2021 01:15:48 +0000 Subject: [PATCH 064/330] Translated using Weblate (Hindi) Currently translated at 19.1% (558 of 2917 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hi/ --- src/i18n/strings/hi.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hi.json b/src/i18n/strings/hi.json index 75b14cca18..f71c024342 100644 --- a/src/i18n/strings/hi.json +++ b/src/i18n/strings/hi.json @@ -585,5 +585,8 @@ "You cannot modify widgets in this room.": "आप इस रूम में विजेट्स को संशोधित नहीं कर सकते।", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s ने कमरे में शामिल होने के लिए %(targetDisplayName)s के निमंत्रण को रद्द कर दिया।", "User %(userId)s is already in the room": "उपयोगकर्ता %(userId)s पहले से ही रूम में है", - "The user must be unbanned before they can be invited.": "उपयोगकर्ता को आमंत्रित करने से पहले उन्हें प्रतिबंधित किया जाना चाहिए।" + "The user must be unbanned before they can be invited.": "उपयोगकर्ता को आमंत्रित करने से पहले उन्हें प्रतिबंधित किया जाना चाहिए।", + "Explore rooms": "रूम का अन्वेषण करें", + "Sign In": "साइन करना", + "Create Account": "खाता बनाएं" } From fd54fa51195046f29699bb99d568eca3b757eab9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 13 Apr 2021 10:33:32 +0100 Subject: [PATCH 065/330] Fix unknown slash command error exploding --- src/SlashCommands.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 3b6a202cf6..0fe7cf7bda 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1222,4 +1222,5 @@ export function getCommand(input: string) { args, }; } + return {}; } From 14b8b0f8da14493b685582b6108e8029a6c282ca Mon Sep 17 00:00:00 2001 From: "Sam A. Horvath-Hunt" Date: Tue, 13 Apr 2021 17:22:07 +0100 Subject: [PATCH 066/330] Render ignored users setting regardless of if there are any Signed-off-by: Sam A. Horvath-Hunt --- .../views/settings/tabs/user/SecurityUserSettingsTab.js | 7 +++---- src/i18n/strings/en_EN.json | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 8a70811399..da41f2f0dc 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -255,10 +255,9 @@ export default class SecurityUserSettingsTab extends React.Component { _renderIgnoredUsers() { const {waitingUnignored, ignoredUserIds} = this.state; - if (!ignoredUserIds || ignoredUserIds.length === 0) return null; - - const userIds = ignoredUserIds - .map((u) => Date: Wed, 14 Apr 2021 14:28:41 +0100 Subject: [PATCH 067/330] add missing spaces --- src/components/views/dialogs/SeshatResetDialog.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/SeshatResetDialog.tsx b/src/components/views/dialogs/SeshatResetDialog.tsx index 135f5d8197..63654ca949 100644 --- a/src/components/views/dialogs/SeshatResetDialog.tsx +++ b/src/components/views/dialogs/SeshatResetDialog.tsx @@ -36,7 +36,7 @@ export default class SeshatResetDialog extends React.PureComponent {_t("You most likely do not want to reset your event index store")}
{_t("If you do, please note that none of your messages will be deleted, " + - "but the search experience might be degraded for a few moments" + + "but the search experience might be degraded for a few moments " + "whilst the index is recreated", )}

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 02236f9997..a51e67c54a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2308,7 +2308,7 @@ "About homeservers": "About homeservers", "Reset event store?": "Reset event store?", "You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store", - "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated", "Reset event store": "Reset event store", "Sign out and remove encryption keys?": "Sign out and remove encryption keys?", "Clear Storage and Sign Out": "Clear Storage and Sign Out", From a3bcb730479e07c1ca4ed9adf023d607f770f637 Mon Sep 17 00:00:00 2001 From: Sebastian Lithgow Date: Wed, 14 Apr 2021 18:57:17 +0000 Subject: [PATCH 068/330] Translated using Weblate (Danish) Currently translated at 21.2% (621 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/da/ --- src/i18n/strings/da.json | 42 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/da.json b/src/i18n/strings/da.json index 32dba2f063..ca28295c4f 100644 --- a/src/i18n/strings/da.json +++ b/src/i18n/strings/da.json @@ -618,5 +618,45 @@ "Unable to access microphone": "Kan ikke tilgå mikrofonen", "The call could not be established": "Opkaldet kunne ikke etableres", "Call Declined": "Opkald afvist", - "Folder": "Mappe" + "Folder": "Mappe", + "We couldn't log you in": "Vi kunne ikke logge dig ind", + "Try again": "Prøv igen", + "Already in call": "", + "You're already in a call with this person.": "Du har allerede i et opkald med denne person.", + "Chile": "Chile", + "Call failed because webcam or microphone could not be accessed. Check that:": "Opkald fejlede på grund af kamera og mikrofon ikke kunne nås. Tjek dette:", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Opkald fejlede på grund af mikrofon ikke kunne nås. Tjek at din mikrofon er tilsluttet og sat op rigtigt.", + "India": "Indien", + "Iceland": "Island", + "Hong Kong": "Hong Kong", + "Greenland": "Grønland", + "Greece": "Grækenland", + "Ghana": "Ghana", + "Germany": "Tyskland", + "Faroe Islands": "Færøerne", + "Estonia": "Estonien", + "Ecuador": "Ecuador", + "Czech Republic": "Tjekkiet", + "Colombia": "Colombien", + "Chad": "Chad", + "Bulgaria": "Bulgarien", + "Brazil": "Brazilien", + "Bosnia": "Bosnien", + "Bolivia": "Bolivien", + "Belarus": "Hviderusland", + "Austria": "Østrig", + "Australia": "Australien", + "Armenia": "Armenien", + "Argentina": "Argentina", + "Antarctica": "Antarktis", + "Angola": "Angola", + "Albania": "Albanien", + "Afghanistan": "Afghanistan", + "United States": "Amerikas Forenede Stater", + "United Kingdom": "Storbritanien", + "This will end the conference for everyone. Continue?": "Dette vil afbryde opkaldet for alle. Fortsæt?", + "No other application is using the webcam": "Ingen anden application bruger kameraet", + "A microphone and webcam are plugged in and set up correctly": "En mikrofon og kamera er tilsluttet og sat op rigtigt", + "Croatia": "Kroatien", + "Answered Elsewhere": "Svaret andet sted" } From 2ce36a210ce30b7cbbbc01d20ecefc7e006858ac Mon Sep 17 00:00:00 2001 From: "@a2sc:matrix.org" Date: Mon, 12 Apr 2021 10:29:24 +0000 Subject: [PATCH 069/330] Translated using Weblate (German) Currently translated at 98.4% (2870 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 675c27c9a3..c90b5f1c33 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3225,5 +3225,6 @@ "Check your devices": "Überprüfe dein Gerät", "%(deviceId)s from %(ip)s": "%(deviceId)s von %(ip)s", "This homeserver has been blocked by it's administrator.": "Dieser Heimserver wurde von seiner Administration blockiert.", - "You have unverified logins": "Du hast nicht-bestätigte Anmeldungen" + "You have unverified logins": "Du hast nicht-bestätigte Anmeldungen", + "Review to ensure your account is safe": "Überprüfen, um sicher zu sein, dass dein Account sicher ist" } From 208152d10f188d96910576de57ebf418314400c5 Mon Sep 17 00:00:00 2001 From: Sven Grewe Date: Wed, 14 Apr 2021 21:52:25 +0000 Subject: [PATCH 070/330] Translated using Weblate (German) Currently translated at 98.1% (2862 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 132 ++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index c90b5f1c33..6ab638b6c1 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -115,7 +115,7 @@ "You are already in a call.": "Du bist bereits in einem Gespräch.", "You cannot place a call with yourself.": "Du kannst keinen Anruf mit dir selbst starten.", "You cannot place VoIP calls in this browser.": "VoIP-Gespräche werden von diesem Browser nicht unterstützt.", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Homeserver verbunden zu sein.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Deine E-Mail-Adresse scheint nicht mit einer Matrix-ID auf diesem Heimserver verbunden zu sein.", "Sun": "So", "Mon": "Mo", "Tue": "Di", @@ -155,7 +155,7 @@ "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s von %(fromPowerLevel)s zu %(toPowerLevel)s", "%(senderName)s invited %(targetName)s.": "%(senderName)s hat %(targetName)s eingeladen.", "%(targetName)s joined the room.": "%(targetName)s hat den Raum betreten.", - "%(senderName)s kicked %(targetName)s.": "%(senderName)s hat %(targetName)s gekickt.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s hat %(targetName)s rausgeworfen.", "%(targetName)s left the room.": "%(targetName)s hat den Raum verlassen.", "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s hat den Chatverlauf für alle Raummitglieder ab ihrer Einladung sichtbar gemacht.", "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s hat den Chatverlauf für alle Raummitglieder ab ihrem Beitreten sichtbar gemacht.", @@ -238,7 +238,7 @@ "%(items)s and %(lastItem)s": "%(items)s und %(lastItem)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s %(time)s", "Access Token:": "Zugangs-Token:", - "Always show message timestamps": "Nachrichten-Zeitstempel immer anzeigen", + "Always show message timestamps": "Nachrichtenzeitstempel immer anzeigen", "Authentication": "Authentifizierung", "An error has occurred.": "Ein Fehler ist aufgetreten.", "Confirm password": "Passwort bestätigen", @@ -380,7 +380,7 @@ "(could not connect media)": "(Medienverbindung konnte nicht hergestellt werden)", "(no answer)": "(keine Antwort)", "(unknown failure: %(reason)s)": "(Unbekannter Fehler: %(reason)s)", - "Your browser does not support the required cryptography extensions": "Dein Browser unterstützt die benötigten Verschlüsselungs-Erweiterungen nicht", + "Your browser does not support the required cryptography extensions": "Dein Browser unterstützt die benötigten Verschlüsselungserweiterungen nicht", "Not a valid %(brand)s keyfile": "Keine gültige %(brand)s-Schlüsseldatei", "Authentication check failed: incorrect password?": "Authentifizierung fehlgeschlagen: Falsches Passwort?", "Do you want to set an email address?": "Möchtest du eine E-Mail-Adresse setzen?", @@ -392,7 +392,7 @@ "Delete widget": "Widget entfernen", "Define the power level of a user": "Berechtigungsstufe einers Benutzers setzen", "Edit": "Bearbeiten", - "Enable automatic language detection for syntax highlighting": "Automatische Spracherkennung für die Syntax-Hervorhebung", + "Enable automatic language detection for syntax highlighting": "Automatische Spracherkennung für die Syntaxhervorhebung", "To get started, please pick a username!": "Um zu starten, wähle bitte einen Nutzernamen!", "Unable to create widget.": "Widget kann nicht erstellt werden.", "You are not in this room.": "Du bist nicht in diesem Raum.", @@ -451,7 +451,7 @@ "Pinned Messages": "Angeheftete Nachrichten", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s hat die angehefteten Nachrichten für diesen Raum geändert.", "Jump to read receipt": "Zur Lesebestätigung springen", - "Message Pinning": "Nachrichten-Anheftung", + "Message Pinning": "Nachrichtenanheftung", "Long Description (HTML)": "Lange Beschreibung (HTML)", "Jump to message": "Zur Nachricht springen", "No pinned messages.": "Keine angehefteten Nachrichten vorhanden.", @@ -627,7 +627,7 @@ "The version of %(brand)s": "Die %(brand)s-Version", "Your language of choice": "Deine ausgewählte Sprache", "Whether or not you're using the Richtext mode of the Rich Text Editor": "Ob du den Richtext-Modus des Editors benutzt oder nicht", - "Your homeserver's URL": "Deine Homeserver-URL", + "Your homeserver's URL": "Deine Heimserver-URL", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der letze Nutzer mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.", "Community IDs cannot be empty.": "Community-IDs können nicht leer sein.", @@ -795,7 +795,7 @@ "We encountered an error trying to restore your previous session.": "Wir haben ein Problem beim Wiederherstellen deiner vorherigen Sitzung festgestellt.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Den Browser-Speicher zu löschen kann das Problem lösen, wird dich aber abmelden und verschlüsselte Chats unlesbar machen.", "Collapse Reply Thread": "Antwort-Thread zusammenklappen", - "Enable widget screenshots on supported widgets": "Widget-Screenshots bei unterstützten Widgets aktivieren", + "Enable widget screenshots on supported widgets": "Widgetbildschirmfotos bei unterstützten Widgets aktivieren", "Send analytics data": "Analysedaten senden", "e.g. %(exampleValue)s": "z.B. %(exampleValue)s", "Muted Users": "Stummgeschaltete Benutzer", @@ -840,8 +840,8 @@ "System Alerts": "Systembenachrichtigung", "Only room administrators will see this warning": "Nur Raum-Administratoren werden diese Nachricht sehen", "Please contact your service administrator to continue using the service.": "Bitte kontaktiere deinen Systemadministrator, um diesen Dienst weiter zu nutzen.", - "This homeserver has hit its Monthly Active User limit.": "Dieser Heimserver hat sein Limit an monatlich aktiven Nutzern erreicht.", - "This homeserver has exceeded one of its resource limits.": "Dieser Heimserver hat einen seiner Ressourcen-Limits überschritten.", + "This homeserver has hit its Monthly Active User limit.": "Dieser Heimserver hat seinen Grenzwert an monatlich aktiven Nutzern erreicht.", + "This homeserver has exceeded one of its resource limits.": "Dieser Heimserver hat einen seiner Ressourcengrenzwert überschritten.", "Upgrade Room Version": "Raum-Version aufrüsten", "Create a new room with the same name, description and avatar": "Einen neuen Raum mit demselben Namen, Beschreibung und Profilbild erstellen", "Update any local room aliases to point to the new room": "Alle lokalen Raum-Aliase aktualisieren, damit sie auf den neuen Raum zeigen", @@ -850,8 +850,8 @@ "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Deine Nachricht wurde nicht gesendet, weil dieser Heimserver sein Limit an monatlich aktiven Benutzern erreicht hat. Bitte kontaktiere deinen Systemadministrator um diesen Dienst weiter zu nutzen.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Deine Nachricht wurde nicht gesendet, weil dieser Heimserver ein Ressourcen-Limit erreicht hat. Bitte kontaktiere deinen Systemadministrator um diesen Dienst weiter zu nutzen.", "Please contact your service administrator to continue using this service.": "Bitte kontaktiere deinen Systemadministrator um diesen Dienst weiter zu nutzen.", - "Sorry, your homeserver is too old to participate in this room.": "Sorry, dein Homeserver ist zu alt, um an diesem Raum teilzunehmen.", - "Please contact your homeserver administrator.": "Bitte setze dich mit der Administration deines Homeservers in Verbindung.", + "Sorry, your homeserver is too old to participate in this room.": "Tschuldige, dein Heimserver ist zu alt, um an diesem Raum teilzunehmen.", + "Please contact your homeserver administrator.": "Bitte setze dich mit der Administration deines Heimservers in Verbindung.", "Legal": "Rechtliches", "This room has been replaced and is no longer active.": "Dieser Raum wurde ersetzt und ist nicht länger aktiv.", "The conversation continues here.": "Die Konversation wird hier fortgesetzt.", @@ -860,7 +860,7 @@ "Failed to upgrade room": "Konnte Raum nicht aufrüsten", "The room upgrade could not be completed": "Die Raum-Aufrüstung konnte nicht fertiggestellt werden", "Upgrade this room to version %(version)s": "Diesen Raum zur Version %(version)s aufrüsten", - "Forces the current outbound group session in an encrypted room to be discarded": "Erzwingt, dass die aktuell ausgehende Gruppen-Sitzung in einem verschlüsseltem Raum verworfen wird", + "Forces the current outbound group session in an encrypted room to be discarded": "Erzwingt, dass die aktuell ausgehende Gruppensitzung in einem verschlüsseltem Raum verworfen wird", "Unable to connect to Homeserver. Retrying...": "Verbindung mit Heimserver nicht möglich. Versuche erneut...", "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s hat die Hauptadresse zu diesem Raum auf %(address)s gesetzt.", "%(senderName)s removed the main address for this room.": "%(senderName)s hat die Hauptadresse von diesem Raum entfernt.", @@ -933,7 +933,7 @@ "Encrypted messages in group chats": "Verschlüsselte Gruppenchats", "Use a longer keyboard pattern with more turns": "Nutze ein längeres Tastaturmuster mit mehr Abwechslung", "Straight rows of keys are easy to guess": "Gerade Reihen von Tasten sind einfach zu erraten", - "Custom user status messages": "Angepasste Nutzerstatus-Nachrichten", + "Custom user status messages": "Angepasste Nutzerstatusnachrichten", "Unable to load key backup status": "Konnte Status der Schlüsselsicherung nicht laden", "Don't ask again": "Nicht erneut fragen", "Set up": "Einrichten", @@ -976,16 +976,16 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s und %(count)s andere tippen…", "%(names)s and %(count)s others are typing …|one": "%(names)s und eine weitere Person tippen…", "%(names)s and %(lastPerson)s are typing …": "%(names)s und %(lastPerson)s tippen…", - "Render simple counters in room header": "Einfache Zähler in Raum-Kopfzeile anzeigen", - "Enable Emoji suggestions while typing": "Emoji-Vorschläge während Eingabe", + "Render simple counters in room header": "Einfache Zähler in Raumkopfzeile anzeigen", + "Enable Emoji suggestions while typing": "Emojivorschläge während Eingabe", "Show a placeholder for removed messages": "Zeigt einen Platzhalter für gelöschte Nachrichten an", - "Show join/leave messages (invites/kicks/bans unaffected)": "Betreten oder Verlassen von Benutzern (ausgen. Kicks/Bans)", - "Show avatar changes": "Avatar-Änderungen anzeigen", + "Show join/leave messages (invites/kicks/bans unaffected)": "Betreten oder Verlassen von Benutzern (ausgen. Einladungen/Rauswürfe/Banne)", + "Show avatar changes": "Avataränderungen anzeigen", "Show display name changes": "Änderungen von Anzeigenamen", "Send typing notifications": "Tippbenachrichtigungen senden", "Show avatars in user and room mentions": "Avatare in Benutzer- und Raumerwähnungen", "Enable big emoji in chat": "Große Emojis im Chat anzeigen", - "Enable Community Filter Panel": "Community-Filter-Panel", + "Enable Community Filter Panel": "Community-Filtertafel", "Messages containing my username": "Nachrichten mit meinem Benutzernamen", "The other party cancelled the verification.": "Die Gegenstelle hat die Überprüfung abgebrochen.", "Verified!": "Verifiziert!", @@ -1028,7 +1028,7 @@ "Deactivating your account is a permanent action - be careful!": "Die Deaktivierung deines Kontos ist unwiderruflich - sei vorsichtig!", "Preferences": "Chats", "Room list": "Raumliste", - "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Die Datei '%(fileName)s' überschreitet die maximale Uploadgröße deines Homeservers", + "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "Die Datei '%(fileName)s' überschreitet die maximale Uploadgröße deines Heimservers", "This room has no topic.": "Dieser Raum hat kein Thema.", "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s hat den Raum für jeden, der den Link kennt, öffentlich gemacht.", "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s hat den Raum auf eingeladene Benutzer beschränkt.", @@ -1036,7 +1036,7 @@ "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s erlaubte Gäste diesem Raum beizutreten.", "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s hat Gästen verboten, diesem Raum beizutreten.", "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s änderte den Gastzugriff auf '%(rule)s'", - "Group & filter rooms by custom tags (refresh to apply changes)": "Gruppiere & filtere Räume nach eigenen Tags (neu laden um Änderungen zu übernehmen)", + "Group & filter rooms by custom tags (refresh to apply changes)": "Gruppiere und filtere Räume nach eigenen Tags (neu laden um Änderungen zu übernehmen)", "Unable to find a supported verification method.": "Konnte keine unterstützte Verifikationsmethode finden.", "Dog": "Hund", "Cat": "Katze", @@ -1245,21 +1245,21 @@ "Changes your avatar in this current room only": "Ändert deinen Avatar für diesen Raum", "Unbans user with given ID": "Entbannt den Benutzer mit der angegebenen ID", "Sends the given message coloured as a rainbow": "Sendet die Nachricht in Regenbogenfarben", - "Adds a custom widget by URL to the room": "Fügt ein Benutzer-Widget über eine URL zum Raum hinzu", + "Adds a custom widget by URL to the room": "Fügt ein Benutzerwidget über eine URL zum Raum hinzu", "Please supply a https:// or http:// widget URL": "Bitte gib eine mit https:// oder http:// beginnende Widget-URL an", "Sends the given emote coloured as a rainbow": "Zeigt Aktionen in Regenbogenfarben", "%(senderName)s made no change.": "%(senderName)s hat keine Änderung vorgenommen.", "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s hat die Einladung zum Raumbeitritt für %(targetDisplayName)s zurückgezogen.", "Cannot reach homeserver": "Der Heimserver ist nicht erreichbar", - "Ensure you have a stable internet connection, or get in touch with the server admin": "Stelle sicher, dass du eine stabile Internetverbindung hast oder wende dich an deinen Server-Administrator", + "Ensure you have a stable internet connection, or get in touch with the server admin": "Stelle sicher, dass du eine stabile Internetverbindung hast oder wende dich an deinen Serveradministrator", "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.": "Wende dich an deinen %(brand)s-Admin um deine Konfiguration auf ungültige oder doppelte Einträge zu überprüfen.", - "Unexpected error resolving identity server configuration": "Ein unerwarteter Fehler ist beim Laden der Identitätsserver-Konfiguration aufgetreten", + "Unexpected error resolving identity server configuration": "Ein unerwarteter Fehler ist beim Laden der Identitätsserverkonfiguration aufgetreten", "Cannot reach identity server": "Der Identitätsserver ist nicht erreichbar", - "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Du kannst dich registrieren, aber manche Funktionen werden erst wieder verfügbar sein, wenn der Identitätsserver wieder online ist. Wenn diese Warnmeldung weiterhin angezeigt wird, überprüfe deine Konfiguration oder kontaktiere die Server-Administration.", - "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Du kannst dein Passwort zurücksetzen, aber manche Funktionen werden nicht verfügbar sein, bis der Identitätsserver wieder online ist. Wenn du diese Warnmeldung weiterhin siehst, überprüfe deine Konfiguration oder kontaktiere die Server-Administration.", - "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Du kannst dich einloggen, aber manche Funktionen werden nicht verfügbar sein bis der Identitätsserver wieder online ist. Wenn du diese Warnmeldung weiterhin siehst, überprüfe deine Konfiguration oder kontaktiere deinen Server-Administrator.", + "You can register, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Du kannst dich registrieren, aber manche Funktionen werden erst wieder verfügbar sein, wenn der Identitätsserver wieder online ist. Wenn diese Warnmeldung weiterhin angezeigt wird, überprüfe deine Konfiguration oder kontaktiere die Serveradministration.", + "You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Du kannst dein Passwort zurücksetzen, aber manche Funktionen werden nicht verfügbar sein, bis der Identitätsserver wieder online ist. Wenn du diese Warnmeldung weiterhin siehst, überprüfe deine Konfiguration oder kontaktiere die Serveradministrator.", + "You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Du kannst dich anmelden, aber manche Funktionen werden nicht verfügbar sein bis der Identitätsserver wieder erreichbar ist. Wenn du diese Warnmeldung weiterhin siehst, überprüfe deine Konfiguration oder kontaktiere deinen Serveradministrator.", "No homeserver URL provided": "Keine Heimserver-URL angegeben", - "Unexpected error resolving homeserver configuration": "Ein unerwarteter Fehler ist beim Laden der Heimserver-Konfiguration aufgetreten", + "Unexpected error resolving homeserver configuration": "Ein unerwarteter Fehler ist beim Laden der Heimserverkonfiguration aufgetreten", "The user's homeserver does not support the version of the room.": "Die Raumversion wird vom Heimserver des Benutzers nicht unterstützt.", "Show hidden events in timeline": "Zeige versteckte Ereignisse in der Chronik", "Low bandwidth mode": "Modus für niedrige Bandbreite", @@ -1298,7 +1298,7 @@ "Call failed due to misconfigured server": "Anruf aufgrund eines falsch konfigurierten Servers fehlgeschlagen", "Try using turn.matrix.org": "Versuche es mit turn.matrix.org", "You do not have the required permissions to use this command.": "Du hast nicht die erforderlichen Berechtigungen, diesen Befehl zu verwenden.", - "Multiple integration managers": "Mehrere Integrationsmanager", + "Multiple integration managers": "Mehrere Integrationsverwalter", "Public Name": "Öffentlicher Name", "Identity Server URL must be HTTPS": "Die Identity-Server-URL über HTTPS erreichbar sein", "Could not connect to Identity Server": "Verbindung zum Identitätsserver konnte nicht hergestellt werden", @@ -1307,7 +1307,7 @@ "Disconnect": "Trennen", "Identity Server": "Identitätsserver", "Use an identity server": "Benutze einen Identitätsserver", - "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Benutze einen Identitätsserver, um andere mittels E-Mail einzuladen. Klicke auf fortfahren, um den Standard-Identitätsserver (%(defaultIdentityServerName)s) zu benutzen oder ändere ihn in den Einstellungen.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Benutze einen Identitätsserver, um andere mittels E-Mail einzuladen. Klicke auf fortfahren, um den Standardidentitätsserver (%(defaultIdentityServerName)s) zu benutzen oder ändere ihn in den Einstellungen.", "ID": "ID", "Not a valid Identity Server (status code %(code)s)": "Ungültiger Identitätsserver (Fehlercode %(code)s)", "Terms of service not accepted or the identity server is invalid.": "Die Nutzungsbedingungen wurden nicht akzeptiert oder der Identitätsserver ist ungültig.", @@ -1329,7 +1329,7 @@ "Find a room… (e.g. %(exampleRoom)s)": "Einen Raum suchen… (z.B. %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Wenn du den gesuchten Raum nicht finden kannst, frage nach einer Einladung für den Raum oder Erstelle einen neuen Raum.", "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternativ kannst du versuchen, den öffentlichen Server unter turn.matrix.org zu verwenden. Allerdings wird dieser nicht so zuverlässig sein und du teilst deine IP-Adresse mit dem Server. Du kannst dies auch in den Einstellungen konfigurieren.", - "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Diese Handlung erfordert es, auf den Standard-Identitätsserver zuzugreifen, um eine E-Mail Adresse oder Telefonnummer zu validieren, aber der Server hat keine Nutzungsbedingungen.", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "Diese Handlung erfordert es, auf den Standardidentitätsserver zuzugreifen, um eine E-Mail-Adresse oder Telefonnummer zu validieren, aber der Server hat keine Nutzungsbedingungen.", "Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du den Betreibern des Servers vertraust.", "Trust": "Vertrauen", "Custom (%(level)s)": "Benutzerdefinierte (%(level)s)", @@ -1368,7 +1368,7 @@ "%(num)s hours from now": "in %(num)s Stunden", "about a day from now": "in etwa einem Tag", "%(num)s days from now": "in %(num)s Tagen", - "Show info about bridges in room settings": "Information über Bridges in den Raumeinstellungen anzeigen", + "Show info about bridges in room settings": "Information über Brücken in den Raumeinstellungen anzeigen", "Enable message search in encrypted rooms": "Nachrichtensuche in verschlüsselten Räumen aktivieren", "Lock": "Schloss", "Later": "Später", @@ -1416,22 +1416,22 @@ "View rules": "Regeln öffnen", "You are currently subscribed to:": "Du abonnierst momentan:", "⚠ These settings are meant for advanced users.": "⚠ Diese Einstellungen sind für fortgeschrittene Nutzer gedacht.", - "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem Touch die primäre Eingabemöglichkeit ist", + "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Ob du %(brand)s auf einem Gerät verwendest, bei dem die Berührung die primäre Eingabemöglichkeit ist", "Whether you're using %(brand)s as an installed Progressive Web App": "Ob du %(brand)s als installierte progressive Web-App (PWA) verwendest", - "Your user agent": "Dein User-Agent", + "Your user agent": "Dein Useragent", "If you cancel now, you won't complete verifying the other user.": "Wenn Sie jetzt abbrechen, werden Sie die Verifizierung des anderen Nutzers nicht beenden können.", "If you cancel now, you won't complete verifying your other session.": "Wenn Sie jetzt abbrechen, werden Sie die Verifizierung der anderen Sitzung nicht beenden können.", "Cancel entering passphrase?": "Eingabe der Passphrase abbrechen?", "Setting up keys": "Einrichten der Schlüssel", - "Encryption upgrade available": "Verschlüsselungs-Update verfügbar", - "Verifies a user, session, and pubkey tuple": "Verifiziert einen Benutzer, eine Sitzung und Pubkey-Tupel", + "Encryption upgrade available": "Verschlüsselungsaufstufung verfügbar", + "Verifies a user, session, and pubkey tuple": "Verifiziert einen Benutzer, eine Sitzung und die Endlichkeit eines öffentlichen Schlüssels", "Unknown (user, session) pair:": "Unbekanntes Nutzer-/Sitzungspaar:", "Session already verified!": "Sitzung bereits verifiziert!", "WARNING: Session already verified, but keys do NOT MATCH!": "WARNUNG: Die Sitzung wurde bereits verifiziert, aber die Schlüssel passen NICHT ZUSAMMEN!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ACHTUNG: SCHLÜSSEL-VERIFIZIERUNG FEHLGESCHLAGEN! Der Signierschlüssel für %(userId)s und Sitzung %(deviceId)s ist \"%(fprint)s\", was nicht mit dem bereitgestellten Schlüssel \"%(fingerprint)s\" übereinstimmt. Das könnte bedeuten, dass deine Kommunikation abgehört wird!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "ACHTUNG: SCHLÜSSELVERIFIZIERUNG FEHLGESCHLAGEN! Der Signierschlüssel für %(userId)s und Sitzung %(deviceId)s ist \"%(fprint)s\", was nicht mit dem bereitgestellten Schlüssel \"%(fingerprint)s\" übereinstimmt. Das könnte bedeuten, dass deine Kommunikation abgehört wird!", "Never send encrypted messages to unverified sessions from this session": "Niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen senden", "Never send encrypted messages to unverified sessions in this room from this session": "Niemals verschlüsselte Nachrichten von dieser Sitzung zu unverifizierten Sitzungen in diesem Raum senden", - "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Durch die Änderung des Passworts werden derzeit alle Ende-zu-Ende-Verschlüsselungsschlüssel in allen Sitzungen zurückgesetzt, sodass der verschlüsselte Chat-Verlauf nicht mehr lesbar ist, es sei denn, du exportierst zuerst deine Raumschlüssel und importierst sie anschließend wieder. In Zukunft wird dies verbessert werden.", + "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Durch die Änderung des Passworts werden derzeit alle Ende-zu-Ende-Verschlüsselungsschlüssel in allen Sitzungen zurückgesetzt, sodass der verschlüsselte Chatverlauf nicht mehr lesbar ist, es sei denn, du exportierst zuerst deine Raumschlüssel und importierst sie anschließend wieder. In Zukunft wird dies verbessert werden.", "Delete %(count)s sessions|other": "%(count)s Sitzungen löschen", "Backup is not signed by any of your sessions": "Die Sicherung wurde von keiner deiner Sitzungen bestätigt", "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du erhältst keine Push-Benachrichtigungen zu anderen Sitzungen, bis du dich wieder bei diesen anmeldest", @@ -1633,11 +1633,11 @@ "Sends a message as html, without interpreting it as markdown": "Verschickt eine Nachricht im HTML-Format, ohne sie als Markdown zu darzustellen", "Show rooms with unread notifications first": "Räume mit ungelesenen Benachrichtigungen zuerst zeigen", "Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen", - "Use Single Sign On to continue": "Einmal-Anmeldung zum Fortfahren nutzen", - "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte E-Mail-Adresse mit der Einmal-Anmeldung, um deine Identität nachzuweisen.", - "Single Sign On": "Einmal-Anmeldung", + "Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte E-Mail-Adresse mit der Einmalanmeldung, um deine Identität nachzuweisen.", + "Single Sign On": "Einmalanmeldung", "Confirm adding email": "Hinzugefügte E-Mail-Addresse bestätigen", - "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels der Einmal-Anmeldung nachweist.", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels der Einmalanmeldung nachweist.", "Click the button below to confirm adding this phone number.": "Klicke unten die Schaltfläche, um die hinzugefügte Telefonnummer zu bestätigen.", "If you cancel now, you won't complete your operation.": "Wenn du jetzt abbrichst, wirst du deinen Vorgang nicht fertigstellen.", "%(name)s is requesting verification": "%(name)s fordert eine Verifizierung an", @@ -1676,7 +1676,7 @@ "Forgotten your password?": "Passwort vergessen?", "You're signed out": "Du wurdest abgemeldet", "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Achtung: Deine persönlichen Daten (einschließlich Verschlüsselungsschlüssel) sind noch in dieser Sitzung gespeichert. Lösche diese Daten, wenn du diese Sitzung nicht mehr benötigst, oder dich mit einem anderen Konto anmelden möchtest.", - "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Melde dich mittels Single Sign-On an, um das Löschen der Sitzungen zu bestätigen.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Melde dich mittels Einmalanmeldung an, um das Löschen der Sitzungen zu bestätigen.", "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Melde dich mittels Single Sign-On an, um das Löschen der Sitzung zu bestätigen.", "Confirm deleting these sessions": "Bestätige das Löschen dieser Sitzungen", "Click the button below to confirm deleting these sessions.|other": "Klicke den Knopf, um das Löschen dieser Sitzungen zu bestätigen.", @@ -1724,7 +1724,7 @@ "Upgrade this room to the recommended room version": "Aktualisiere diesen Raum auf die empfohlene Raumversion", "this room": "Dieser Raum", "View older messages in %(roomName)s.": "Zeige alte Nachrichten in %(roomName)s.", - "Send a bug report with logs": "Einen Fehlerbericht mit Logs senden", + "Send a bug report with logs": "Einen Fehlerbericht mit der Protokolldatei senden", "Verify all your sessions to ensure your account & messages are safe": "Verifiziere alle deine Sitzungen, um dein Konto und deine Nachrichten zu schützen", "Verify your other session using one of the options below.": "Verifiziere deine andere Sitzung mit einer der folgenden Optionen.", "You signed in to a new session without verifying it:": "Du hast dich in einer neuen Sitzung angemeldet ohne sie zu verifizieren:", @@ -1732,25 +1732,25 @@ "Upgrade": "Hochstufen", "Verify the new login accessing your account: %(name)s": "Verifiziere die neue Anmeldung an deinem Konto: %(name)s", "From %(deviceName)s (%(deviceId)s)": "Von %(deviceName)s (%(deviceId)s)", - "Your homeserver does not support cross-signing.": "Dein Heimserver unterstützt kein Cross-Signing.", + "Your homeserver does not support cross-signing.": "Dein Heimserver unterstützt keine Quersignierung.", "Cross-signing and secret storage are enabled.": "Cross-signing und der sichere Speicher wurden eingerichtet.", - "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Dein Konto hat eine Cross-Signing-Identität im sicheren Speicher, der von dieser Sitzung jedoch noch nicht vertraut wird.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Dein Konto hat eine Quersignaturidentität im sicheren Speicher, der von dieser Sitzung jedoch noch nicht vertraut wird.", "Cross-signing and secret storage are not yet set up.": "Cross-Signing und der sichere Speicher sind noch nicht eingerichtet.", "Reset cross-signing and secret storage": "Cross-Signing und den sicheren Speicher zurücksetzen", "Bootstrap cross-signing and secret storage": "Richte Cross-Signing und den sicheren Speicher ein", "unexpected type": "unbekannter Typ", - "Cross-signing public keys:": "Öffentliche Cross-Signing-Schlüssel:", + "Cross-signing public keys:": "Öffentlicher Quersignaturschlüssel:", "in memory": "im Speicher", - "Cross-signing private keys:": "Private Cross-Signing-Schlüssel:", + "Cross-signing private keys:": "Private Quersignaturschlüssel:", "in secret storage": "im Schlüsselspeicher", "Self signing private key:": "Selbst signierter privater Schlüssel:", "cached locally": "lokal zwischengespeichert", "not found locally": "lokal nicht gefunden", - "User signing private key:": "Privater Benutzer-Schlüssel:", + "User signing private key:": "Privater Benutzerschlüssel:", "Session backup key:": "Sitzungswiederherstellungsschlüssel:", "Secret storage public key:": "Öffentlicher Schlüssel des sicheren Speichers:", "in account data": "in den Kontodaten", - "Homeserver feature support:": "Unterstützte Funktionen des Homeservers:", + "Homeserver feature support:": "Unterstützte Funktionen des Heimservers:", "exists": "existiert", "Delete sessions|other": "Sitzungen löschen", "Delete sessions|one": "Sitzung löschen", @@ -2185,7 +2185,7 @@ "Click the button below to confirm setting up encryption.": "Klick die Schaltfläche unten um die Einstellungen der Verschlüsselung zu bestätigen.", "Font scaling": "Schriftskalierung", "Font size": "Schriftgröße", - "IRC display name width": "Breite des IRC Anzeigenamens", + "IRC display name width": "Breite des IRC-Anzeigenamens", "Size must be a number": "Schriftgröße muss eine Zahl sein", "Custom font size can only be between %(min)s pt and %(max)s pt": "Eigene Schriftgröße kann nur eine Zahl zwischen %(min)s pt und %(max)s pt sein", "Use between %(min)s pt and %(max)s pt": "Verwende eine Zahl zwischen %(min)s pt und %(max)s pt", @@ -2200,9 +2200,9 @@ "Help us improve %(brand)s": "Hilf uns, %(brand)s zu verbessern", "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Hilf uns, %(brand)s zu verbessern, indem du anonyme Nutzungsdaten schickst. Dies wird ein Cookie verwenden.", "I want to help": "Ich möchte helfen", - "Your homeserver has exceeded its user limit.": "Dein Heimserver hat das Benutzerlimit erreicht.", + "Your homeserver has exceeded its user limit.": "Dein Heimserver hat das Benutzergrenzwert erreicht.", "Your homeserver has exceeded one of its resource limits.": "Dein Heimserver hat eine seiner Ressourcengrenzen erreicht.", - "Contact your server admin.": "Kontaktiere deine Heimserver-Administration.", + "Contact your server admin.": "Kontaktiere deine Heimserveradministration.", "Ok": "Ok", "Set password": "Setze Passwort", "To return to your account in future you need to set a password": "Um dein Konto zukünftig wieder verwenden zu können, setze ein Passwort", @@ -2263,7 +2263,7 @@ "Compact": "Kompakt", "Modern": "Modern", "Use a system font": "Systemschriftart verwenden", - "System font name": "System-Schriftart", + "System font name": "Systemschriftart", "Customise your appearance": "Verändere das Erscheinungsbild", "Appearance Settings only affect this %(brand)s session.": "Einstellungen zum Erscheinungsbild wirken sich nur auf diese Sitzung aus.", "The authenticity of this encrypted message can't be guaranteed on this device.": "Die Echtheit dieser verschlüsselten Nachricht kann auf diesem Gerät nicht garantiert werden.", @@ -2363,7 +2363,7 @@ "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!", "Learn more at element.io/previously-riot": "Erfahre mehr unter element.io/previously-riot", "The person who invited you already left the room.": "Die Person, die dich eingeladen hat, hat den Raum bereits verlassen.", - "The person who invited you already left the room, or their server is offline.": "Die Person, die dich eingeladen hat, hat den Raum bereits verlassen oder ihr Server ist offline.", + "The person who invited you already left the room, or their server is offline.": "Die Person, die dich eingeladen hat, hat den Raum bereits verlassen oder ihr Server ist nicht erreichbar bzw. aus.", "Change notification settings": "Benachrichtigungseinstellungen ändern", "Your server isn't responding to some requests.": "Dein Server antwortet auf einige Anfragen nicht.", "Go to Element": "Zu Element gehen", @@ -2478,8 +2478,8 @@ "Group call modified by %(senderName)s": "Gruppenanruf wurde von %(senderName)s verändert", "Group call started by %(senderName)s": "Gruppenanruf von %(senderName)s gestartet", "Group call ended by %(senderName)s": "Gruppenanruf wurde von %(senderName)s beendet", - "Cross-signing is ready for use.": "Cross-Signing ist bereit zur Anwendung.", - "Cross-signing is not set up.": "Cross-Signing wurde nicht eingerichtet.", + "Cross-signing is ready for use.": "Quersignaturen sind bereits in Anwendung.", + "Cross-signing is not set up.": "Quersignierung wurde nicht eingerichtet.", "Backup version:": "Backup-Version:", "Algorithm:": "Algorithmus:", "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Sichere deine Verschlüsselungsschlüssel mit deinen Kontodaten, falls du den Zugriff auf deine Sitzungen verlierst. Deine Schlüssel werden mit einem eindeutigen Wiederherstellungsschlüssel gesichert.", @@ -2535,7 +2535,7 @@ "Hide Widgets": "Widgets verstecken", "%(senderName)s declined the call.": "%(senderName)s hat den Anruf abgelehnt.", "(an error occurred)": "(ein Fehler ist aufgetreten)", - "(their device couldn't start the camera / microphone)": "(ihr/sein Gerät konnte Kamera oder Mikrophon nicht starten)", + "(their device couldn't start the camera / microphone)": "(Gerät des Gegenübers konnte Kamera oder Mikrophon nicht starten)", "(connection failed)": "(Verbindung fehlgeschlagen)", "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Alle Server sind von der Teilnahme ausgeschlossen! Dieser Raum kann nicht mehr genutzt werden.", "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s hat die Server-ACLs für diesen Raum geändert.", @@ -2616,8 +2616,8 @@ "New version of %(brand)s is available": "Neue Version von %(brand)s verfügbar", "You ended the call": "Du hast den Anruf beendet", "%(senderName)s ended the call": "%(senderName)s hat den Anruf beendet", - "Use Command + Enter to send a message": "Benutze Betriebssystemtaste + Enter um eine Nachricht zu senden", - "Use Ctrl + Enter to send a message": "Nachrichten mit Strg + Enter senden", + "Use Command + Enter to send a message": "Benutze Betriebssystemtaste + Eingabe um eine Nachricht zu senden", + "Use Ctrl + Enter to send a message": "Nachrichten mit Strg + Eingabe senden", "Call Paused": "Anruf pausiert", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern, um sie in Suchergebnissen finden zu können. Es werden %(size)s benötigt, um die Nachrichten von %(rooms)s Räumen zu speichern.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern, um sie in Suchergebnissen finden zu können. Es werden %(size)s benötigt, um die Nachrichten vom Raum %(rooms)s zu speichern.", @@ -3038,10 +3038,10 @@ "Use app": "App verwenden", "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web ist auf mobilen Endgeräten experimentell. Für eine bessere Erfahrung und die neuesten Erweiterungen, nutze unsere freie, native App.", "Use app for a better experience": "Nutze die App für eine bessere Erfahrung", - "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Wir haben deinen Browser gebeten, sich zu merken, bei welchem Homeserver du dich anmeldest, aber dein Browser hat dies leider vergessen. Gehe zur Anmeldeseite und versuche es erneut.", - "Show stickers button": "Sticker-Schaltfläche", - "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Dein Homeserver hat deinen Anmeldeversuch abgelehnt. Vielleicht dauert der Prozess einfach zu lange. Bitte versuche es erneut. Wenn dies öfters passiert, wende dich bitte an deine Homeserver-Administration.", - "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Dein Homeserver war nicht erreichbar und konnte dich nicht anmelden. Bitte versuche es erneut. Wenn dies öfters passiert, wende dich bitte an deine Homeserver-Administration.", + "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "Wir haben deinen Browser gebeten, sich zu merken, bei welchem Heimserver du dich anmeldest, aber dein Browser hat dies leider vergessen. Gehe zur Anmeldeseite und versuche es erneut.", + "Show stickers button": "Stickerschaltfläche", + "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Dein Heimserver hat deinen Anmeldeversuch abgelehnt. Vielleicht dauert der Prozess einfach zu lange. Bitte versuche es erneut. Wenn dies öfters passiert, wende dich bitte an deine Heimserveradministrator.", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Dein Heimserver war nicht erreichbar und konnte dich nicht anmelden. Bitte versuche es erneut. Wenn dies öfters passiert, wende dich bitte an deine Heimserveradministrator.", "We couldn't log you in": "Wir konnten dich nicht anmelden", "Windows": "Fenster", "Screens": "Bildschirme", @@ -3122,7 +3122,7 @@ "Invite members": "Mitglieder einladen", "Add some details to help people recognise it.": "Gib einige Infos über deinen neuen Space an.", "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Mit Matrix-Spaces kannst du Räume und Personen gruppieren. Um einen existierenden Space zu betreten, musst du eingeladen werden.", - "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces Prototyp. Inkompatibel mit Communities, Communities v2 und Custom Tags. Für einige Features wird ein kompatibler Homeserver benötigt.", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces Prototyp. Inkompatibel mit Communities, Communities v2 und benutzerdefinierte Tags. Für einige Funktionen wird ein kompatibler Heimserver benötigt.", "Invite to this space": "In diesen Space enladen", "Verify this login to access your encrypted messages and prove to others that this login is really you.": "Verifiziere diese Anmeldung um deine Identität zu bestätigen und Zugriff auf verschlüsselte Nachrichten zu erhalten.", "What projects are you working on?": "An welchen Projekten arbeitest du gerade?", @@ -3226,5 +3226,5 @@ "%(deviceId)s from %(ip)s": "%(deviceId)s von %(ip)s", "This homeserver has been blocked by it's administrator.": "Dieser Heimserver wurde von seiner Administration blockiert.", "You have unverified logins": "Du hast nicht-bestätigte Anmeldungen", - "Review to ensure your account is safe": "Überprüfen, um sicher zu sein, dass dein Account sicher ist" + "Review to ensure your account is safe": "Überprüfen, um sicher zu sein, dass dein Konto sicher ist" } From 8241b6a730105bdea7be4a8366c54e7d71d433a7 Mon Sep 17 00:00:00 2001 From: Christian Paul Date: Wed, 14 Apr 2021 21:56:50 +0000 Subject: [PATCH 071/330] Translated using Weblate (German) Currently translated at 98.1% (2862 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 6ab638b6c1..0abe885675 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1677,7 +1677,7 @@ "You're signed out": "Du wurdest abgemeldet", "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Achtung: Deine persönlichen Daten (einschließlich Verschlüsselungsschlüssel) sind noch in dieser Sitzung gespeichert. Lösche diese Daten, wenn du diese Sitzung nicht mehr benötigst, oder dich mit einem anderen Konto anmelden möchtest.", "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Melde dich mittels Einmalanmeldung an, um das Löschen der Sitzungen zu bestätigen.", - "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Melde dich mittels Single Sign-On an, um das Löschen der Sitzung zu bestätigen.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Bestätige das Löschen dieser Sitzung indem du dich mittels „Single Sign-On“ anmeldest um deine Identität nachzuweisen.", "Confirm deleting these sessions": "Bestätige das Löschen dieser Sitzungen", "Click the button below to confirm deleting these sessions.|other": "Klicke den Knopf, um das Löschen dieser Sitzungen zu bestätigen.", "Click the button below to confirm deleting these sessions.|one": "Klicke den Knopf, um das Löschen dieser Sitzung zu bestätigen.", From 9c250171b4ff2864f3680f921a864f285375af77 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Wed, 14 Apr 2021 18:27:35 -0500 Subject: [PATCH 072/330] Use new copy Signed-off-by: Aaron Raimist --- src/components/structures/MatrixChat.tsx | 4 +--- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index e8e28088db..dd64dd76f9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1098,7 +1098,7 @@ export default class MatrixChat extends React.PureComponent { warnings.push(( {' '/* Whitespace, otherwise the sentences get smashed together */ } - { _t("You are the only member of this room. This room will become unjoinable if you leave.") } + { _t("You are the only person here. If you leave, no one will be able to join in the future, including you.") } )); @@ -1126,7 +1126,6 @@ export default class MatrixChat extends React.PureComponent { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); - const hasWarnings = warnings.length > 0; const isSpace = roomToLeave?.isSpaceRoom(); Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, { @@ -1140,7 +1139,6 @@ export default class MatrixChat extends React.PureComponent { ), button: _t("Leave"), - danger: hasWarnings, onFinished: (shouldLeave) => { if (shouldLeave) { const d = leaveRoomBehaviour(roomId); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b3afc8bfc8..8971848f73 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2545,7 +2545,7 @@ "Failed to reject invitation": "Failed to reject invitation", "Cannot create rooms in this community": "Cannot create rooms in this community", "You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.", - "You are the only member of this room. This room will become unjoinable if you leave.": "You are the only member of this room. This room will become unjoinable if you leave.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "You are the only person here. If you leave, no one will be able to join in the future, including you.", "This space is not public. You will not be able to rejoin without an invite.": "This space is not public. You will not be able to rejoin without an invite.", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?", From 22219e0e802cc58eb3a6d507a2221ec9fc4ad64f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Apr 2021 21:13:09 -0600 Subject: [PATCH 073/330] Adapt to use an Alignment enum instead --- src/components/views/elements/Field.tsx | 2 +- src/components/views/elements/InfoTooltip.tsx | 6 +-- src/components/views/elements/Tooltip.tsx | 48 ++++++++++++++++--- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index f5754da9ae..59d9a11596 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -262,7 +262,7 @@ export default class Field extends React.PureComponent { tooltipClassName={classNames("mx_Field_tooltip", tooltipClassName)} visible={(this.state.focused && this.props.forceTooltipVisible) || this.state.feedbackVisible} label={tooltipContent || this.state.feedback} - forceOnRight + alignment={Tooltip.Alignment.Right} />; } diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx index 8f7f1ea53f..d49090dbae 100644 --- a/src/components/views/elements/InfoTooltip.tsx +++ b/src/components/views/elements/InfoTooltip.tsx @@ -18,8 +18,8 @@ limitations under the License. import React from 'react'; import classNames from 'classnames'; -import Tooltip from './Tooltip'; -import { _t } from "../../../languageHandler"; +import Tooltip, {Alignment} from './Tooltip'; +import {_t} from "../../../languageHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; interface ITooltipProps { @@ -61,7 +61,7 @@ export default class InfoTooltip extends React.PureComponent :
; return (
diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index b2dd00de18..116b226b8f 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -25,6 +25,14 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; const MIN_TOOLTIP_HEIGHT = 25; +export enum Alignment { + Natural, // Pick left or right + Left, + Right, + Top, // Centered + Bottom, // Centered +} + interface IProps { // Class applied to the element used to position the tooltip className?: string; @@ -36,7 +44,7 @@ interface IProps { visible?: boolean; // the react element to put into the tooltip label: React.ReactNode; - forceOnRight?: boolean; + alignment?: Alignment; // defaults to Natural yOffset?: number; } @@ -46,10 +54,14 @@ export default class Tooltip extends React.Component { private tooltip: void | Element | Component; private parent: Element; + // XXX: This is because some components (Field) are unable to `import` the Tooltip class, + // so we expose the Alignment options off of us statically. + public static readonly Alignment = Alignment; public static readonly defaultProps = { visible: true, yOffset: 0, + alignment: Alignment.Natural, }; // Create a wrapper for the tooltip outside the parent and attach it to the body element @@ -86,11 +98,35 @@ export default class Tooltip extends React.Component { offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT); } - style.top = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset + offset; - if (!this.props.forceOnRight && parentBox.right > window.innerWidth / 2) { - style.right = window.innerWidth - parentBox.right - window.pageXOffset - 16; - } else { - style.left = parentBox.right + window.pageXOffset + 6; + const baseTop = (parentBox.top - 2 + this.props.yOffset) + window.pageYOffset; + const top = baseTop + offset; + const right = window.innerWidth - parentBox.right - window.pageXOffset - 16; + const left = parentBox.right + window.pageXOffset + 6; + const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2); + switch(this.props.alignment) { + case Alignment.Natural: + if (parentBox.right > window.innerWidth / 2) { + style.right = right; + style.top = top; + break; + } + // fall through to Right + case Alignment.Right: + style.left = left; + style.top = top; + break; + case Alignment.Left: + style.right = right; + style.top = top; + break; + case Alignment.Top: + style.top = baseTop - 16; + style.left = horizontalCenter; + break; + case Alignment.Bottom: + style.top = baseTop + parentBox.height; + style.left = horizontalCenter; + break; } return style; From 0677cf866cf694d6dae371bce8d3ae91d0c944e7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Apr 2021 21:15:06 -0600 Subject: [PATCH 074/330] Cap recording length, and warn at 10s remaining See diff for details. Note that this introduces an "Uploading" state which is not currently used. At the moment, if a user hits the maximum time then their recording will be broken. This is expected to be fixed in a future PR. --- src/components/views/rooms/MessageComposer.js | 25 +++++++++- src/i18n/strings/en_EN.json | 1 + src/stores/VoiceRecordingStore.ts | 4 +- src/voice/VoiceRecording.ts | 47 +++++++++++++++++-- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 283b11a437..6c227458aa 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -35,6 +35,8 @@ import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; +import {RecordingState} from "../../../voice/VoiceRecording"; +import Tooltip, {Alignment} from "../elements/Tooltip"; function ComposerAvatar(props) { const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); @@ -191,6 +193,7 @@ export default class MessageComposer extends React.Component { joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room), isComposerEmpty: true, haveRecording: false, + recordingTimeLeftSeconds: null, // when set to a number, shows a toast }; } @@ -331,7 +334,17 @@ export default class MessageComposer extends React.Component { } _onVoiceStoreUpdate = () => { - this.setState({haveRecording: !!VoiceRecordingStore.instance.activeRecording}); + const recording = VoiceRecordingStore.instance.activeRecording; + this.setState({haveRecording: !!recording}); + if (recording) { + // We show a little head's up that the recording is about to automatically end soon. The 3s + // display time is completely arbitrary. Note that we don't need to deregister the listener + // because the recording instance will clean that up for us. + recording.on(RecordingState.EndingSoon, ({secondsLeft}) => { + this.setState({recordingTimeLeftSeconds: secondsLeft}); + setTimeout(() => this.setState({recordingTimeLeftSeconds: null}), 3000); + }); + } }; render() { @@ -412,8 +425,18 @@ export default class MessageComposer extends React.Component { ); } + let recordingTooltip; + const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds); + if (secondsLeft) { + recordingTooltip = ; + } + return (
+ {recordingTooltip}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 02236f9997..e8839369b1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1473,6 +1473,7 @@ "The conversation continues here.": "The conversation continues here.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "You do not have permission to post to this room": "You do not have permission to post to this room", + "%(seconds)ss left": "%(seconds)ss left", "Bold": "Bold", "Italics": "Italics", "Strikethrough": "Strikethrough", diff --git a/src/stores/VoiceRecordingStore.ts b/src/stores/VoiceRecordingStore.ts index e1685de529..cc999f23f8 100644 --- a/src/stores/VoiceRecordingStore.ts +++ b/src/stores/VoiceRecordingStore.ts @@ -73,9 +73,7 @@ export class VoiceRecordingStore extends AsyncStoreWithClient { */ public disposeRecording(): Promise { if (this.state.recording) { - // Stop for good measure, but completely async because we're not concerned with this - // passing or failing. - this.state.recording.stop().catch(e => console.error("Error stopping recording", e)); + this.state.recording.destroy(); // stops internally } return this.updateState({recording: null}); } diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 77c182fc54..41bce9d698 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -20,17 +20,29 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; import CallMediaHandler from "../CallMediaHandler"; import {SimpleObservable} from "matrix-widget-api"; import {clamp} from "../utils/numbers"; +import EventEmitter from "events"; +import {IDestroyable} from "../utils/IDestroyable"; const CHANNELS = 1; // stereo isn't important const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus. +const TARGET_MAX_LENGTH = 120; // 2 minutes in seconds. Somewhat arbitrary, though longer == larger files. +const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary. export interface IRecordingUpdate { waveform: number[]; // floating points between 0 (low) and 1 (high). timeSeconds: number; // float } -export class VoiceRecording { +export enum RecordingState { + Started = "started", + EndingSoon = "ending_soon", // emits an object with a single numerical value: secondsLeft + Ended = "ended", + Uploading = "uploading", + Uploaded = "uploaded", +} + +export class VoiceRecording extends EventEmitter implements IDestroyable { private recorder: Recorder; private recorderContext: AudioContext; private recorderSource: MediaStreamAudioSourceNode; @@ -40,9 +52,12 @@ export class VoiceRecording { private buffer = new Uint8Array(0); private mxc: string; private recording = false; + private stopping = false; + private haveWarned = false; // whether or not EndingSoon has been fired private observable: SimpleObservable; public constructor(private client: MatrixClient) { + super(); } private async makeRecorder() { @@ -124,7 +139,7 @@ export class VoiceRecording { return this.mxc; } - private tryUpdateLiveData = (ev: AudioProcessingEvent) => { + private processAudioUpdate = (ev: AudioProcessingEvent) => { if (!this.recording) return; // The time domain is the input to the FFT, which means we use an array of the same @@ -150,6 +165,17 @@ export class VoiceRecording { waveform: translatedData, timeSeconds: ev.playbackTime, }); + + // Now that we've updated the data/waveform, let's do a time check. We don't want to + // go horribly over the limit. We also emit a warning state if needed. + const secondsLeft = TARGET_MAX_LENGTH - ev.playbackTime; + if (secondsLeft <= 0) { + // noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping + this.stop(); + } else if (secondsLeft <= TARGET_WARN_TIME_LEFT && !this.haveWarned) { + this.emit(RecordingState.EndingSoon, {secondsLeft}); + this.haveWarned = true; + } }; public async start(): Promise { @@ -164,9 +190,10 @@ export class VoiceRecording { } this.observable = new SimpleObservable(); await this.makeRecorder(); - this.recorderProcessor.addEventListener("audioprocess", this.tryUpdateLiveData); + this.recorderProcessor.addEventListener("audioprocess", this.processAudioUpdate); await this.recorder.start(); this.recording = true; + this.emit(RecordingState.Started); } public async stop(): Promise { @@ -174,6 +201,9 @@ export class VoiceRecording { throw new Error("No recording to stop"); } + if (this.stopping) return; + this.stopping = true; + // Disconnect the source early to start shutting down resources this.recorderSource.disconnect(); await this.recorder.stop(); @@ -187,12 +217,19 @@ export class VoiceRecording { // Finally do our post-processing and clean up this.recording = false; - this.recorderProcessor.removeEventListener("audioprocess", this.tryUpdateLiveData); + this.recorderProcessor.removeEventListener("audioprocess", this.processAudioUpdate); await this.recorder.close(); + this.emit(RecordingState.Ended); return this.buffer; } + public destroy() { + // noinspection JSIgnoredPromiseFromCall - not concerned about stop() being called async here + this.stop(); + this.removeAllListeners(); + } + public async upload(): Promise { if (!this.hasRecording) { throw new Error("No recording available to upload"); @@ -200,11 +237,13 @@ export class VoiceRecording { if (this.mxc) return this.mxc; + this.emit(RecordingState.Uploading); this.mxc = await this.client.uploadContent(new Blob([this.buffer], { type: "audio/ogg", }), { onlyContentUri: false, // to stop the warnings in the console }).then(r => r['content_uri']); + this.emit(RecordingState.Uploaded); return this.mxc; } } From 22233a8745ee0f8b043b6955762a5bfbb6e87666 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Apr 2021 21:47:30 -0600 Subject: [PATCH 075/330] Add a concept of a singleflight to avoid repeated calls to stop/ending This makes it easier to keep track of which pieces the client will have already dispatched or been executed, reducing the amount of class members needed. Critically, this makes it so the 'stop' button (which is currently a send button) actually works even after the automatic stop has happened. UI is still pending for stopping recording early. This is not covered by this change. --- src/utils/Singleflight.ts | 101 +++++++++++++++++++++++++++++++ src/voice/VoiceRecording.ts | 51 ++++++++-------- test/Singleflight-test.ts | 115 ++++++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 25 deletions(-) create mode 100644 src/utils/Singleflight.ts create mode 100644 test/Singleflight-test.ts diff --git a/src/utils/Singleflight.ts b/src/utils/Singleflight.ts new file mode 100644 index 0000000000..c8d303bcac --- /dev/null +++ b/src/utils/Singleflight.ts @@ -0,0 +1,101 @@ +/* +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 {EnhancedMap} from "./maps"; + +// Inspired by https://pkg.go.dev/golang.org/x/sync/singleflight + +const keyMap = new EnhancedMap>(); + +/** + * Access class to get a singleflight context. Singleflights execute a + * function exactly once, unless instructed to forget about a result. + */ +export class Singleflight { + private constructor() { + } + + /** + * A void marker to help with returning a value in a singleflight context. + * If your code doesn't return anything, return this instead. + */ + public static Void = Symbol("void"); + + /** + * Acquire a singleflight context. + * @param {Object} instance An instance to associate the context with. Can be any object. + * @param {string} key A string key relevant to that instance to namespace under. + * @returns {SingleflightContext} Returns the context to execute the function. + */ + public static for(instance: Object, key: string): SingleflightContext { + if (!instance || !key) throw new Error("An instance and key must be supplied"); + return new SingleflightContext(instance, key); + } + + /** + * Forgets all results for a given instance. + * @param {Object} instance The instance to forget about. + */ + public static forgetAllFor(instance: Object) { + keyMap.delete(instance); + } + + /** + * Forgets all cached results for all instances. Intended for use by tests. + */ + public static forgetAll() { + for (const k of keyMap.keys()) { + keyMap.remove(k); + } + } +} + +class SingleflightContext { + public constructor(private instance: Object, private key: string) { + } + + /** + * Forget this particular instance and key combination, discarding the result. + */ + public forget() { + const map = keyMap.get(this.instance); + if (!map) return; + map.remove(this.key); + if (!map.size) keyMap.remove(this.instance); + } + + /** + * Execute a function. If a result is already known, that will be returned instead + * of executing the provided function. However, if no result is known then the function + * will be called, with its return value cached. The function must return a value + * other than `undefined` - take a look at Singleflight.Void if you don't have a return + * to make. + * @param {Function} fn The function to execute. + * @returns The recorded value. + */ + public do(fn: () => T): T { + const map = keyMap.getOrCreate(this.instance, new EnhancedMap()); + + // We have to manually getOrCreate() because we need to execute the fn + let val = map.get(this.key); + if (val === undefined) { + val = fn(); + map.set(this.key, val); + } + + return val; + } +} diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 41bce9d698..55775ff786 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -22,6 +22,7 @@ import {SimpleObservable} from "matrix-widget-api"; import {clamp} from "../utils/numbers"; import EventEmitter from "events"; import {IDestroyable} from "../utils/IDestroyable"; +import {Singleflight} from "../utils/Singleflight"; const CHANNELS = 1; // stereo isn't important const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. @@ -52,8 +53,6 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private buffer = new Uint8Array(0); private mxc: string; private recording = false; - private stopping = false; - private haveWarned = false; // whether or not EndingSoon has been fired private observable: SimpleObservable; public constructor(private client: MatrixClient) { @@ -172,9 +171,11 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { if (secondsLeft <= 0) { // noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping this.stop(); - } else if (secondsLeft <= TARGET_WARN_TIME_LEFT && !this.haveWarned) { - this.emit(RecordingState.EndingSoon, {secondsLeft}); - this.haveWarned = true; + } else if (secondsLeft <= TARGET_WARN_TIME_LEFT) { + Singleflight.for(this, "ending_soon").do(() => { + this.emit(RecordingState.EndingSoon, {secondsLeft}); + return Singleflight.Void; + }); } }; @@ -197,37 +198,37 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { } public async stop(): Promise { - if (!this.recording) { - throw new Error("No recording to stop"); - } + return Singleflight.for(this, "stop").do(async () => { + if (!this.recording) { + throw new Error("No recording to stop"); + } - if (this.stopping) return; - this.stopping = true; + // Disconnect the source early to start shutting down resources + this.recorderSource.disconnect(); + await this.recorder.stop(); - // Disconnect the source early to start shutting down resources - this.recorderSource.disconnect(); - await this.recorder.stop(); + // close the context after the recorder so the recorder doesn't try to + // connect anything to the context (this would generate a warning) + await this.recorderContext.close(); - // close the context after the recorder so the recorder doesn't try to - // connect anything to the context (this would generate a warning) - await this.recorderContext.close(); + // Now stop all the media tracks so we can release them back to the user/OS + this.recorderStream.getTracks().forEach(t => t.stop()); - // Now stop all the media tracks so we can release them back to the user/OS - this.recorderStream.getTracks().forEach(t => t.stop()); + // Finally do our post-processing and clean up + this.recording = false; + this.recorderProcessor.removeEventListener("audioprocess", this.processAudioUpdate); + await this.recorder.close(); + this.emit(RecordingState.Ended); - // Finally do our post-processing and clean up - this.recording = false; - this.recorderProcessor.removeEventListener("audioprocess", this.processAudioUpdate); - await this.recorder.close(); - this.emit(RecordingState.Ended); - - return this.buffer; + return this.buffer; + }); } public destroy() { // noinspection JSIgnoredPromiseFromCall - not concerned about stop() being called async here this.stop(); this.removeAllListeners(); + Singleflight.forgetAllFor(this); } public async upload(): Promise { diff --git a/test/Singleflight-test.ts b/test/Singleflight-test.ts new file mode 100644 index 0000000000..4f0c6e0da3 --- /dev/null +++ b/test/Singleflight-test.ts @@ -0,0 +1,115 @@ +/* +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 {Singleflight} from "../src/utils/Singleflight"; + +describe('Singleflight', () => { + afterEach(() => { + Singleflight.forgetAll(); + }); + + it('should throw for bad context variables', () => { + const permutations: [Object, string][] = [ + [null, null], + [{}, null], + [null, "test"], + ]; + for (const p of permutations) { + try { + Singleflight.for(p[0], p[1]); + // noinspection ExceptionCaughtLocallyJS + throw new Error("failed to fail: " + JSON.stringify(p)); + } catch (e) { + expect(e.message).toBe("An instance and key must be supplied"); + } + } + }); + + it('should execute the function once', () => { + const instance = {}; + const key = "test"; + const val = {}; // unique object for reference check + const fn = jest.fn().mockReturnValue(val); + const sf = Singleflight.for(instance, key); + const r1 = sf.do(fn); + expect(r1).toBe(val); + expect(fn.mock.calls.length).toBe(1); + const r2 = sf.do(fn); + expect(r2).toBe(val); + expect(fn.mock.calls.length).toBe(1); + }); + + it('should execute the function once, even with new contexts', () => { + const instance = {}; + const key = "test"; + const val = {}; // unique object for reference check + const fn = jest.fn().mockReturnValue(val); + let sf = Singleflight.for(instance, key); + const r1 = sf.do(fn); + expect(r1).toBe(val); + expect(fn.mock.calls.length).toBe(1); + sf = Singleflight.for(instance, key); // RESET FOR TEST + const r2 = sf.do(fn); + expect(r2).toBe(val); + expect(fn.mock.calls.length).toBe(1); + }); + + it('should execute the function twice if the result was forgotten', () => { + const instance = {}; + const key = "test"; + const val = {}; // unique object for reference check + const fn = jest.fn().mockReturnValue(val); + const sf = Singleflight.for(instance, key); + const r1 = sf.do(fn); + expect(r1).toBe(val); + expect(fn.mock.calls.length).toBe(1); + sf.forget(); + const r2 = sf.do(fn); + expect(r2).toBe(val); + expect(fn.mock.calls.length).toBe(2); + }); + + it('should execute the function twice if the instance was forgotten', () => { + const instance = {}; + const key = "test"; + const val = {}; // unique object for reference check + const fn = jest.fn().mockReturnValue(val); + const sf = Singleflight.for(instance, key); + const r1 = sf.do(fn); + expect(r1).toBe(val); + expect(fn.mock.calls.length).toBe(1); + Singleflight.forgetAllFor(instance); + const r2 = sf.do(fn); + expect(r2).toBe(val); + expect(fn.mock.calls.length).toBe(2); + }); + + it('should execute the function twice if everything was forgotten', () => { + const instance = {}; + const key = "test"; + const val = {}; // unique object for reference check + const fn = jest.fn().mockReturnValue(val); + const sf = Singleflight.for(instance, key); + const r1 = sf.do(fn); + expect(r1).toBe(val); + expect(fn.mock.calls.length).toBe(1); + Singleflight.forgetAll(); + const r2 = sf.do(fn); + expect(r2).toBe(val); + expect(fn.mock.calls.length).toBe(2); + }); +}); + From 1aeb9a5fb2dd120b85062bea5c5c236f63ea0ba5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Apr 2021 22:04:07 -0600 Subject: [PATCH 076/330] Appease the linter --- src/components/views/elements/Tooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 116b226b8f..062d26c852 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -103,7 +103,7 @@ export default class Tooltip extends React.Component { const right = window.innerWidth - parentBox.right - window.pageXOffset - 16; const left = parentBox.right + window.pageXOffset + 6; const horizontalCenter = parentBox.right - window.pageXOffset - (parentBox.width / 2); - switch(this.props.alignment) { + switch (this.props.alignment) { case Alignment.Natural: if (parentBox.right > window.innerWidth / 2) { style.right = right; From 72d8e6ccca4a214231c1a6cbbba8aab7bbc62ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Apr 2021 08:09:14 +0200 Subject: [PATCH 077/330] Decrease ZOOM_COEFFICIENT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index dad62521da..71e63d214d 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -38,7 +38,7 @@ const MAX_ZOOM = 300; // This is used for the buttons const ZOOM_STEP = 10; // This is used for mouse wheel events -const ZOOM_COEFFICIENT = 10; +const ZOOM_COEFFICIENT = 7.5; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; From 2e31355741f7d7a837e5691cbee99db291e369b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Apr 2021 08:10:03 +0200 Subject: [PATCH 078/330] Don't do anything if we didn't press the left button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 71e63d214d..bb69e24855 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -209,6 +209,10 @@ export default class ImageView extends React.Component { ev.stopPropagation(); ev.preventDefault(); + // Don't do anything if we pressed any + // other button than the left one + if (ev.button !== 0) return; + // Zoom in if we are completely zoomed out if (this.state.zoom === MIN_ZOOM) { this.setState({zoom: MAX_ZOOM}); From ff8db76d5a7724b34106d98b07e43d96392f2062 Mon Sep 17 00:00:00 2001 From: Sven Grewe Date: Wed, 14 Apr 2021 22:28:14 +0000 Subject: [PATCH 079/330] Translated using Weblate (German) Currently translated at 98.0% (2860 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 93 +++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 0abe885675..153519a643 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -25,8 +25,8 @@ "Warning!": "Warnung!", "Error": "Fehler", "Advanced": "Erweitert", - "Anyone who knows the room's link, apart from guests": "Alle, die den Raum-Link kennen (ausgenommen Gäste)", - "Anyone who knows the room's link, including guests": "Alle, die den Raum-Link kennen (auch Gäste)", + "Anyone who knows the room's link, apart from guests": "Alle, die den Raumlink kennen (ausgenommen Gäste)", + "Anyone who knows the room's link, including guests": "Alle, die den Raumlink kennen (auch Gäste)", "Are you sure you want to reject the invitation?": "Bist du sicher, dass du die Einladung ablehnen willst?", "Banned users": "Verbannte Benutzer", "Continue": "Fortfahren", @@ -50,7 +50,7 @@ "Homeserver is": "Der Heimserver ist", "Identity Server is": "Der Identitätsserver ist", "I have verified my email address": "Ich habe meine E-Mail-Adresse verifiziert", - "Import E2E room keys": "E2E-Raum-Schlüssel importieren", + "Import E2E room keys": "E2E-Raumschlüssel importieren", "Invalid Email Address": "Ungültige E-Mail-Adresse", "Sign in with": "Anmelden mit", "Leave room": "Raum verlassen", @@ -78,7 +78,7 @@ "Someone": "Jemand", "Success": "Erfolg", "This doesn't appear to be a valid email address": "Dies scheint keine gültige E-Mail-Adresse zu sein", - "This room is not accessible by remote Matrix servers": "Remote-Matrix-Server können auf diesen Raum nicht zugreifen", + "This room is not accessible by remote Matrix servers": "Ferngesteuerte Matrixserver können auf diesen Raum nicht zugreifen", "Admin": "Administrator", "Server may be unavailable, overloaded, or you hit a bug.": "Server ist nicht verfügbar, überlastet oder du bist auf einen Softwarefehler gestoßen.", "Labs": "Labor", @@ -190,7 +190,7 @@ "click to reveal": "anzeigen", "Failed to forget room %(errCode)s": "Das Entfernen des Raums ist fehlgeschlagen %(errCode)s", "and %(count)s others...|other": "und %(count)s weitere...", - "and %(count)s others...|one": "und ein(e) weitere(r)...", + "and %(count)s others...|one": "und ein weiterer...", "Are you sure?": "Bist du sicher?", "Attachment": "Anhang", "Ban": "Bannen", @@ -237,7 +237,7 @@ "Autoplay GIFs and videos": "Videos und GIFs automatisch abspielen", "%(items)s and %(lastItem)s": "%(items)s und %(lastItem)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s %(time)s", - "Access Token:": "Zugangs-Token:", + "Access Token:": "Zugangstoken:", "Always show message timestamps": "Nachrichtenzeitstempel immer anzeigen", "Authentication": "Authentifizierung", "An error has occurred.": "Ein Fehler ist aufgetreten.", @@ -302,7 +302,7 @@ "Idle": "Abwesend", "Ongoing conference call%(supportedText)s.": "Laufendes Konferenzgespräch%(supportedText)s.", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Um dein Konto für die Verwendung von %(integrationsUrl)s zu authentifizieren, wirst du jetzt auf die Website eines Drittanbieters weitergeleitet. Möchtest du fortfahren?", - "Start automatically after system login": "Nach System-Login automatisch starten", + "Start automatically after system login": "Nach Systemanmeldung automatisch starten", "Jump to first unread message.": "Zur ersten ungelesenen Nachricht springen.", "Options": "Optionen", "Invited": "Eingeladen", @@ -386,7 +386,7 @@ "Do you want to set an email address?": "Möchtest du eine E-Mail-Adresse setzen?", "This will allow you to reset your password and receive notifications.": "Dies ermöglicht es dir, dein Passwort zurückzusetzen und Benachrichtigungen zu empfangen.", "Skip": "Überspringen", - "Check for update": "Nach Updates suchen", + "Check for update": "Nach Aktualisierung suchen", "Add a widget": "Widget hinzufügen", "Allow": "Erlauben", "Delete widget": "Widget entfernen", @@ -654,12 +654,12 @@ "Changes made to your community name and avatar might not be seen by other users for up to 30 minutes.": "Änderungen am Namen und Bild deiner Community werden evtl. erst nach 30 Minuten von anderen Nutzern gesehen werden.", "Join this community": "Community beitreten", "Leave this community": "Community verlassen", - "You don't currently have any stickerpacks enabled": "Du hast aktuell keine Stickerpacks aktiviert", + "You don't currently have any stickerpacks enabled": "Du hast aktuell keine Stickerpakete aktiviert", "Hide Stickers": "Sticker ausblenden", "Show Stickers": "Sticker anzeigen", "Who can join this community?": "Wer kann dieser Community beitreten?", "Everyone": "Jeder", - "Stickerpack": "Stickerpack", + "Stickerpack": "Stickerpaket", "Fetching third party location failed": "Das Abrufen des Drittanbieterstandorts ist fehlgeschlagen", "Send Account Data": "Benutzerkonto-Daten senden", "All notifications are currently disabled for all targets.": "Aktuell sind alle Benachrichtigungen für alle Ziele deaktiviert.", @@ -676,7 +676,7 @@ "Changelog": "Änderungsprotokoll", "Waiting for response from server": "Auf Antwort vom Server warten", "Send Custom Event": "Benutzerdefiniertes Event senden", - "Advanced notification settings": "Erweiterte Benachrichtigungs-Einstellungen", + "Advanced notification settings": "Erweiterte Benachrichtigungseinstellungen", "Failed to send logs: ": "Senden von Logs fehlgeschlagen: ", "Forget": "Entfernen", "You cannot delete this image. (%(code)s)": "Das Bild kann nicht gelöscht werden. (%(code)s)", @@ -699,7 +699,7 @@ "Messages sent by bot": "Nachrichten von Bots", "Filter results": "Ergebnisse filtern", "Members": "Mitglieder", - "No update available.": "Kein Update verfügbar.", + "No update available.": "Keine Aktualisierung verfügbar.", "Noisy": "Laut", "Collecting app version information": "App-Versionsinformationen werden abgerufen", "Keywords": "Schlüsselwörter", @@ -738,13 +738,13 @@ "Send logs": "Logdateien übermitteln", "All messages": "Alle Nachrichten", "Call invitation": "Anrufe", - "Downloading update...": "Update wird heruntergeladen...", + "Downloading update...": "Aktualisierung wird heruntergeladen...", "State Key": "Status-Schlüssel", "Failed to send custom event.": "Senden des benutzerdefinierten Events fehlgeschlagen.", "What's new?": "Was ist neu?", "Notify me for anything else": "Über alles andere benachrichtigen", "When I'm invited to a room": "Einladungen", - "Can't update user notification settings": "Benachrichtigungs-Einstellungen des Benutzers konnten nicht aktualisiert werden", + "Can't update user notification settings": "Benachrichtigungseinstellungen des Benutzers konnten nicht aktualisiert werden", "Notify for all other messages/rooms": "Benachrichtigungen für alle anderen Mitteilungen/Räume aktivieren", "Unable to look up room ID from server": "Es ist nicht möglich, die Raum-ID auf dem Server nachzuschlagen", "Couldn't find a matching Matrix room": "Konnte keinen entsprechenden Matrix-Raum finden", @@ -781,7 +781,7 @@ "Thank you!": "Danke!", "Uploaded on %(date)s by %(user)s": "Hochgeladen: %(date)s von %(user)s", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "In deinem aktuell verwendeten Browser können Aussehen und Handhabung der Anwendung unter Umständen noch komplett fehlerhaft sein, so dass einige bzw. im Extremfall alle Funktionen nicht zur Verfügung stehen. Du kannst es trotzdem versuchen und fortfahren, bist dabei aber bezüglich aller auftretenden Probleme auf dich allein gestellt!", - "Checking for an update...": "Nach Updates suchen...", + "Checking for an update...": "Nach Aktualisierung suchen...", "Missing roomId.": "Fehlende Raum-ID.", "Every page you use in the app": "Jede Seite, die du in der App benutzt", "e.g. ": "z. B. ", @@ -821,7 +821,7 @@ "Share Message": "Nachricht teilen", "No Audio Outputs detected": "Keine Audioausgabe erkannt", "Audio Output": "Audioausgabe", - "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In verschlüsselten Räumen wie diesem ist die Link-Vorschau standardmäßig deaktiviert, damit dein Heimserver (der die Vorschau erzeugt) keine Informationen über Links in diesem Raum bekommt.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In verschlüsselten Räumen wie diesem ist die Linkvorschau standardmäßig deaktiviert, damit dein Heimserver (der die Vorschau erzeugt) keine Informationen über Links in diesem Raum bekommt.", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Die URL-Vorschau kann Informationen wie den Titel, die Beschreibung sowie ein Vorschaubild der Website enthalten.", "The email field must not be blank.": "Das E-Mail-Feld darf nicht leer sein.", "The phone number field must not be blank.": "Das Telefonnummern-Feld darf nicht leer sein.", @@ -838,7 +838,7 @@ "Failed to remove widget": "Widget konnte nicht entfernt werden", "An error ocurred whilst trying to remove the widget from the room": "Ein Fehler trat auf während versucht wurde, das Widget aus diesem Raum zu entfernen", "System Alerts": "Systembenachrichtigung", - "Only room administrators will see this warning": "Nur Raum-Administratoren werden diese Nachricht sehen", + "Only room administrators will see this warning": "Nur Raumadministratoren werden diese Nachricht sehen", "Please contact your service administrator to continue using the service.": "Bitte kontaktiere deinen Systemadministrator, um diesen Dienst weiter zu nutzen.", "This homeserver has hit its Monthly Active User limit.": "Dieser Heimserver hat seinen Grenzwert an monatlich aktiven Nutzern erreicht.", "This homeserver has exceeded one of its resource limits.": "Dieser Heimserver hat einen seiner Ressourcengrenzwert überschritten.", @@ -1020,11 +1020,11 @@ "For help with using %(brand)s, click here.": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Um Hilfe zur Benutzung von %(brand)s zu erhalten, klicke hier oder beginne einen Chat mit unserem Bot. Klicke dazu auf den unteren Knopf.", "Chat with %(brand)s Bot": "Chatte mit dem %(brand)s-Bot", - "Help & About": "Hilfe & Über", + "Help & About": "Hilfe und Über", "Bug reporting": "Fehler melden", "FAQ": "Häufige Fragen", "Versions": "Versionen", - "Room Addresses": "Raum-Adressen", + "Room Addresses": "Raumadressen", "Deactivating your account is a permanent action - be careful!": "Die Deaktivierung deines Kontos ist unwiderruflich - sei vorsichtig!", "Preferences": "Chats", "Room list": "Raumliste", @@ -1102,7 +1102,7 @@ "Pin": "Anheften", "Timeline": "Chatverlauf", "Autocomplete delay (ms)": "Verzögerung zur Autovervollständigung (ms)", - "Roles & Permissions": "Rollen & Berechtigungen", + "Roles & Permissions": "Rollen und Berechtigungen", "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Änderungen an der Sichtbarkeit des Chatverlaufs gelten nur für zukünftige Nachrichten. Die Sichtbarkeit des existierenden Verlaufs bleibt unverändert.", "Security & Privacy": "Sicherheit", "Encryption": "Verschlüsselung", @@ -1207,11 +1207,11 @@ "Change permissions": "Ändere Berechtigungen", "Change topic": "Ändere das Thema", "Modify widgets": "Widgets bearbeiten", - "Default role": "Standard Rolle", + "Default role": "Standard-Rolle", "Send messages": "Nachrichten senden", "Invite users": "Benutzer einladen", "Change settings": "Einstellungen ändern", - "Kick users": "Benutzer kicken", + "Kick users": "Benutzer rauswerfen", "Ban users": "Benutzer verbannen", "Remove messages": "Nachrichten löschen", "Notify everyone": "Jeden benachrichtigen", @@ -1221,7 +1221,7 @@ "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Sobald aktiviert, kann die Verschlüsselung für einen Raum nicht mehr deaktiviert werden. Nachrichten in einem verschlüsselten Raum können nur noch von Teilnehmern aber nicht mehr vom Server gelesen werden. Einige Bots und Brücken werden vielleicht nicht mehr funktionieren. Erfahre mehr über Verschlüsselung.", "Error updating main address": "Fehler beim Aktualisieren der Hauptadresse", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Es gab ein Problem beim Aktualisieren der Raum-Hauptadresse. Es kann sein, dass es vom Server verboten ist oder ein temporäres Problem auftrat.", - "Error updating flair": "Abzeichen-Aktualisierung fehlgeschlagen", + "Error updating flair": "Abzeichenaktualisierung fehlgeschlagen", "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "Es gab ein Problem beim Aktualisieren des Abzeichens für diesen Raum. Es kann sein, dass der Server es nicht erlaubt oder ein temporäres Problem auftrat.", "Power level": "Berechtigungsstufe", "Room Settings - %(roomName)s": "Raumeinstellungen - %(roomName)s", @@ -1238,7 +1238,7 @@ "You cannot modify widgets in this room.": "Du darfst in diesem Raum keine Widgets verändern.", "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Ob du die \"Breadcrumbs\"-Funktion nutzt oder nicht (Avatare oberhalb der Raumliste)", "The server does not support the room version specified.": "Der Server unterstützt die angegebene Raumversion nicht.", - "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.": "Achtung: Ein Raum-Upgrade wird die Mitglieder des Raumes nicht automatisch auf die neue Version migrieren. Wir werden in der alten Raumversion einen Link zum neuen Raum posten - Raum-Mitglieder müssen dann auf diesen Link klicken um dem neuen Raum beizutreten.", + "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.": "Achtung: Ein Raum-Upgrade wird die Mitglieder des Raumes nicht automatisch auf die neue Version migrieren. Wir werden in der alten Raumversion einen Link zum neuen Raum posten - Raummitglieder müssen dann auf diesen Link klicken um dem neuen Raum beizutreten.", "Replying With Files": "Mit Dateien antworten", "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Momentan ist es nicht möglich mit einer Datei zu antworten. Möchtest Du die Datei hochladen, ohne zu antworten?", "The file '%(fileName)s' failed to upload.": "Die Datei \"%(fileName)s\" konnte nicht hochgeladen werden.", @@ -1300,7 +1300,7 @@ "You do not have the required permissions to use this command.": "Du hast nicht die erforderlichen Berechtigungen, diesen Befehl zu verwenden.", "Multiple integration managers": "Mehrere Integrationsverwalter", "Public Name": "Öffentlicher Name", - "Identity Server URL must be HTTPS": "Die Identity-Server-URL über HTTPS erreichbar sein", + "Identity Server URL must be HTTPS": "Identitätsserver-URL muss HTTPS sein", "Could not connect to Identity Server": "Verbindung zum Identitätsserver konnte nicht hergestellt werden", "Checking server": "Server wird überprüft", "Identity server has no terms of service": "Der Identitätsserver hat keine Nutzungsbedingungen", @@ -1380,9 +1380,9 @@ "Manage": "Verwalten", "Securely cache encrypted messages locally for them to appear in search results.": "Speichere verschlüsselte Nachrichten lokal, sodass sie deinen Suchergebnissen erscheinen können.", "Enable": "Aktivieren", - "Connecting to integration manager...": "Verbinde mit Integrationsmanager...", - "Cannot connect to integration manager": "Verbindung zum Integrationsmanager fehlgeschlagen", - "The integration manager is offline or it cannot reach your homeserver.": "Der Integrationsmanager ist offline oder er kann den Heimserver nicht erreichen.", + "Connecting to integration manager...": "Verbinde mit Integrationsverwalter...", + "Cannot connect to integration manager": "Verbindung zum Integrationsverwalter fehlgeschlagen", + "The integration manager is offline or it cannot reach your homeserver.": "Der Integrationsverwalter ist offline oder er kann den Heimserver nicht erreichen.", "not stored": "nicht gespeichert", "Backup has a signature from unknown user with ID %(deviceId)s": "Die Sicherung hat eine Signatur von unbekanntem Nutzer mit ID %(deviceId)s", "Backup key stored: ": "Backup Schlüssel gespeichert: ", @@ -1394,10 +1394,10 @@ "wait and try again later": "warte und versuche es später erneut", "Disconnect anyway": "Verbindung trotzdem trennen", "You are still sharing your personal data on the identity server .": "Du teilst deine persönlichen Daten immer noch auf dem Identitätsserver .", - "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Wir empfehlen, dass du deine Email Adressen und Telefonnummern vom Identitätsserver löschst, bevor du die Verbindung trennst.", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Wir empfehlen, dass du deine E-Mail-Adressen und Telefonnummern vom Identitätsserver löschst, bevor du die Verbindung trennst.", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "Zur Zeit benutzt du keinen Identitätsserver. Trage unten einen Server ein, um Kontakte finden und von anderen gefunden zu werden.", - "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Nutze einen Integrationsmanager (%(serverName)s), um Bots, Widgets und Stickerpacks zu verwalten.", - "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsmanager, um Bots, Widgets und Sticker Packs zu verwalten.", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Nutze einen Integrationsverwalter (%(serverName)s), um Bots, Widgets und Stickerpakete zu verwalten.", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsverwalter, um Bots, Widgets und Stickerpakete zu verwalten.", "Manage integrations": "Integrationen verwalten", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Stimme den Nutzungsbedingungen des Identitätsservers %(serverName)s zu, um dich per E-Mail-Adresse und Telefonnummer auffindbar zu machen.", "Clear cache and reload": "Zwischenspeicher löschen und neu laden", @@ -1434,7 +1434,7 @@ "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Durch die Änderung des Passworts werden derzeit alle Ende-zu-Ende-Verschlüsselungsschlüssel in allen Sitzungen zurückgesetzt, sodass der verschlüsselte Chatverlauf nicht mehr lesbar ist, es sei denn, du exportierst zuerst deine Raumschlüssel und importierst sie anschließend wieder. In Zukunft wird dies verbessert werden.", "Delete %(count)s sessions|other": "%(count)s Sitzungen löschen", "Backup is not signed by any of your sessions": "Die Sicherung wurde von keiner deiner Sitzungen bestätigt", - "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du erhältst keine Push-Benachrichtigungen zu anderen Sitzungen, bis du dich wieder bei diesen anmeldest", + "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Dein Passwort wurde erfolgreich geändert. Du erhältst keine Puschbenachrichtigungen zu anderen Sitzungen, bis du dich wieder bei diesen anmeldest", "Notification sound": "Benachrichtigungston", "Set a new custom sound": "Benutzerdefinierten Ton setzen", "Browse": "Durchsuchen", @@ -1594,7 +1594,7 @@ "This backup is trusted because it has been restored on this session": "Dieser Sicherung wird vertraut, da sie während dieser Sitzung wiederhergestellt wurde", "Enable desktop notifications for this session": "Desktopbenachrichtigungen in dieser Sitzung", "Enable audible notifications for this session": "Benachrichtigungstöne in dieser Sitzung", - "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsmanager erhalten Konfigurationsdaten und können Widgets modifizieren, Raumeinladungen verschicken und in deinem Namen Berechtigungslevel setzen.", + "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsverwalter erhalten Konfigurationsdaten und können Widgets modifizieren, Raumeinladungen verschicken und in deinem Namen Berechtigungslevel setzen.", "Read Marker lifetime (ms)": "Gültigkeitsdauer der Gelesen-Markierung (ms)", "Read Marker off-screen lifetime (ms)": "Gültigkeitsdauer der Gelesen-Markierung außerhalb des Bildschirms (ms)", "Session key:": "Sitzungsschlüssel:", @@ -1677,7 +1677,7 @@ "You're signed out": "Du wurdest abgemeldet", "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Achtung: Deine persönlichen Daten (einschließlich Verschlüsselungsschlüssel) sind noch in dieser Sitzung gespeichert. Lösche diese Daten, wenn du diese Sitzung nicht mehr benötigst, oder dich mit einem anderen Konto anmelden möchtest.", "Confirm deleting these sessions by using Single Sign On to prove your identity.|other": "Melde dich mittels Einmalanmeldung an, um das Löschen der Sitzungen zu bestätigen.", - "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Bestätige das Löschen dieser Sitzung indem du dich mittels „Single Sign-On“ anmeldest um deine Identität nachzuweisen.", + "Confirm deleting these sessions by using Single Sign On to prove your identity.|one": "Bestätige das Löschen dieser Sitzung mittels Einmalanmeldung um deine Identität nachzuweisen.", "Confirm deleting these sessions": "Bestätige das Löschen dieser Sitzungen", "Click the button below to confirm deleting these sessions.|other": "Klicke den Knopf, um das Löschen dieser Sitzungen zu bestätigen.", "Click the button below to confirm deleting these sessions.|one": "Klicke den Knopf, um das Löschen dieser Sitzung zu bestätigen.", @@ -1710,16 +1710,16 @@ "eg: @bot:* or example.org": "z.B. @bot:* oder example.org", "Subscribed lists": "Abonnierte Listen", "Subscribing to a ban list will cause you to join it!": "Eine Verbotsliste abonnieren bedeutet ihr beizutreten!", - "If this isn't what you want, please use a different tool to ignore users.": "Wenn dies nicht das ist, was du willst, verwende ein anderes Tool, um Benutzer zu blockieren.", + "If this isn't what you want, please use a different tool to ignore users.": "Wenn dies nicht das ist, was du willst, verwende ein anderes Werkzeug, um Benutzer zu blockieren.", "Subscribe": "Abonnieren", "Always show the window menu bar": "Fenstermenüleiste immer anzeigen", "Show tray icon and minimize window to it on close": "Beim Schließen des Fensters in die Taskleiste minimieren", "Session ID:": "Sitzungs-ID:", "Message search": "Nachrichtensuche", - "Cross-signing": "Cross-Signing", + "Cross-signing": "Quersignierung", "This room is bridging messages to the following platforms. Learn more.": "Dieser Raum verbindet Nachrichten mit den folgenden Plattformen. Mehr erfahren.", "This room isn’t bridging messages to any platforms. Learn more.": "Dieser Raum verbindet keine Nachrichten mit anderen Plattformen. Mehr erfahren.", - "Bridges": "Bridges", + "Bridges": "Brücken", "Uploaded sound": "Hochgeladener Ton", "Upgrade this room to the recommended room version": "Aktualisiere diesen Raum auf die empfohlene Raumversion", "this room": "Dieser Raum", @@ -1754,7 +1754,7 @@ "exists": "existiert", "Delete sessions|other": "Sitzungen löschen", "Delete sessions|one": "Sitzung löschen", - "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Alle Sitzungen einzeln verifizieren, anstatt auch Sitzungen zu vertrauen, die durch Cross-Signing verifiziert sind.", + "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Alle Sitzungen einzeln verifizieren, anstatt auch Sitzungen zu vertrauen, die durch Quersignierungen verifiziert sind.", "Securely cache encrypted messages locally for them to appear in search results, using ": "Der Zwischenspeicher für die lokale Suche in verschlüsselten Nachrichten benötigt ", " to store messages from ": " um Nachrichten von ", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "Um verschlüsselte Nachrichten lokal zu durchsuchen, benötigt %(brand)s weitere Komponenten. Wenn du diese Funktion testen möchtest, kannst du dir deine eigene Version von %(brand)s Desktop mit der integrierten Suchfunktion kompilieren.", @@ -1783,7 +1783,7 @@ "Share": "Teilen", "You have not verified this user.": "Du hast diesen Nutzer nicht verifiziert.", "Everyone in this room is verified": "Alle in diesem Raum sind verifiziert", - "Mod": "Mod", + "Mod": "Moderator", "Invite only": "Nur auf Einladung", "Scroll to most recent messages": "Zur neusten Nachricht springen", "No recent messages by %(user)s found": "Keine neuen Nachrichten von %(user)s gefunden", @@ -1806,7 +1806,7 @@ "Re-join": "Wieder beitreten", "You were banned from %(roomName)s by %(memberName)s": "Du wurdest von %(memberName)s aus %(roomName)s verbannt", "Something went wrong with your invite to %(roomName)s": "Bei deiner Einladung zu %(roomName)s ist ein Fehler aufgetreten", - "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Während der Verifizierung deiner Einladung ist ein Fehler (%(errcode)s) aufgetreten. Du kannst diese Information einem Raum-Administrator weitergeben.", + "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Während der Verifizierung deiner Einladung ist ein Fehler (%(errcode)s) aufgetreten. Du kannst diese Information einem Raumadministrator weitergeben.", "You can only join it with a working invite.": "Du kannst nur mit einer gültigen Einladung beitreten.", "Try to join anyway": "Dennoch versuchen beizutreten", "You can still join it because this is a public room.": "Du kannst trotzdem beitreten, weil es ein öffentlicher Raum ist.", @@ -2149,7 +2149,7 @@ "Unable to query secret storage status": "Status des sicheren Speichers kann nicht gelesen werden", "We'll store an encrypted copy of your keys on our server. Secure your backup with a recovery passphrase.": "Wir werden eine verschlüsselte Kopie deiner Schlüssel auf unserem Server speichern. Schütze deine Sicherung mit einer Wiederherstellungspassphrase.", "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another session.": "Ohne eine Schlüsselsicherung kann dein verschlüsselter Nachrichtenverlauf nicht wiederhergestellt werden wenn du dich abmeldest oder eine andere Sitzung verwendest.", - "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Es gab einen Fehler beim Ändern des Raum-Aliases. Entweder erlaubt es der Server nicht oder es gab ein temporäres Problem.", + "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Es gab einen Fehler beim Ändern des Raumaliases. Entweder erlaubt es der Server nicht oder es gab ein temporäres Problem.", "Self-verification request": "Selbstverifikationsanfrage", "or another cross-signing capable Matrix client": "oder einen anderen Matrix Client der Cross-signing fähig ist", "%(brand)s is securely caching encrypted messages locally for them to appear in search results:": "%(brand)s verwendet einen sicheren Zwischenspeicher für verschlüsselte Nachrichten, damit sie in den Suchergebnissen angezeigt werden:", @@ -2225,7 +2225,7 @@ "Address (optional)": "Adresse (optional)", "delete the address.": "lösche die Adresse.", "Use a different passphrase?": "Eine andere Passphrase verwenden?", - "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Deine Server-Administration hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.", + "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Deine Serveradministration hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.", "People": "Personen", "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Beim Entfernen dieser Adresse ist ein Fehler aufgetreten. Vielleicht existiert sie nicht mehr oder es kam zu einem temporären Fehler.", "Set a room address to easily share your room with other people.": "Vergebe eine Raum-Adresse, um diesen Raum auf einfache Weise mit anderen Personen teilen zu können.", @@ -2429,7 +2429,7 @@ "Send %(count)s invites|other": "%(count)s Einladungen senden", "There was an error creating your community. The name may be taken or the server is unable to process your request.": "Beim Erstellen deiner Community ist ein Fehler aufgetreten. Entweder ist der Name schon vergeben oder der Server kann die Anfrage nicht verarbeiten.", "Community ID: +:%(domain)s": "Community-ID: +:%(domain)s", - "Explore community rooms": "Entdecke Community Räume", + "Explore community rooms": "Entdecke Communityräume", "You can change this later if needed.": "Falls nötig, kannst du es später noch ändern.", "What's the name of your community or team?": "Welchen Namen hat deine Community oder dein Team?", "Enter name": "Namen eingeben", @@ -2480,7 +2480,7 @@ "Group call ended by %(senderName)s": "Gruppenanruf wurde von %(senderName)s beendet", "Cross-signing is ready for use.": "Quersignaturen sind bereits in Anwendung.", "Cross-signing is not set up.": "Quersignierung wurde nicht eingerichtet.", - "Backup version:": "Backup-Version:", + "Backup version:": "Version der Sicherung:", "Algorithm:": "Algorithmus:", "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Recovery Key.": "Sichere deine Verschlüsselungsschlüssel mit deinen Kontodaten, falls du den Zugriff auf deine Sitzungen verlierst. Deine Schlüssel werden mit einem eindeutigen Wiederherstellungsschlüssel gesichert.", "Backup key stored:": "Sicherungsschlüssel gespeichert:", @@ -2488,7 +2488,7 @@ "Secret storage:": "Sicherer Speicher:", "ready": "bereit", "not ready": "nicht bereit", - "Secure Backup": "Sicheres Backup", + "Secure Backup": "Sichere Aufbewahrungskopie", "End Call": "Anruf beenden", "Remove the group call from the room?": "Konferenzgespräch aus diesem Raum entfernen?", "You don't have permission to remove the call from the room": "Du hast keine Berechtigung um den Konferenzanruf aus dem Raum zu entfernen", @@ -3226,5 +3226,6 @@ "%(deviceId)s from %(ip)s": "%(deviceId)s von %(ip)s", "This homeserver has been blocked by it's administrator.": "Dieser Heimserver wurde von seiner Administration blockiert.", "You have unverified logins": "Du hast nicht-bestätigte Anmeldungen", - "Review to ensure your account is safe": "Überprüfen, um sicher zu sein, dass dein Konto sicher ist" + "Review to ensure your account is safe": "Überprüfen, um sicher zu sein, dass dein Konto sicher ist", + "Message search initilisation failed": "Initialisierung der Nachrichtensuche fehlgeschlagen" } From 7927170f92676f602e6c9024d5641b8e14b0b708 Mon Sep 17 00:00:00 2001 From: libexus Date: Wed, 14 Apr 2021 22:05:42 +0000 Subject: [PATCH 080/330] Translated using Weblate (German) Currently translated at 98.0% (2860 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 153519a643..7166433caf 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1125,7 +1125,7 @@ "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Bist du sicher? Du wirst alle deine verschlüsselten Nachrichten verlieren, wenn deine Schlüssel nicht gut gesichert sind.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Verschlüsselte Nachrichten sind mit Ende-zu-Ende-Verschlüsselung gesichert. Nur du und der/die Empfänger haben die Schlüssel um diese Nachrichten zu lesen.", "Restore from Backup": "Von Sicherung wiederherstellen", - "Back up your keys before signing out to avoid losing them.": "Damit du deine Schlüssel nicht verlierst, sichere sie, bevor du dich abmeldest.", + "Back up your keys before signing out to avoid losing them.": "Um deine Schlüssel nicht zu verlieren, musst du sie vor der Abmeldung sichern.", "Start using Key Backup": "Beginne Schlüsselsicherung zu nutzen", "Credits": "Danksagungen", "Starting backup...": "Starte Sicherung...", From 4cf88fc69f8f9d9dce021167d4d2fbc6f3f1ad04 Mon Sep 17 00:00:00 2001 From: Thai Localization Date: Mon, 12 Apr 2021 08:07:43 +0000 Subject: [PATCH 081/330] Translated using Weblate (Thai) Currently translated at 11.7% (344 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/th/ --- src/i18n/strings/th.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/th.json b/src/i18n/strings/th.json index a5b641921c..16a9e521c2 100644 --- a/src/i18n/strings/th.json +++ b/src/i18n/strings/th.json @@ -26,7 +26,7 @@ "Results from DuckDuckGo": "ผลจาก DuckDuckGo", "%(brand)s version:": "เวอร์ชัน %(brand)s:", "Cancel": "ยกเลิก", - "Dismiss": "ไม่สนใจ", + "Dismiss": "ปิด", "Mute": "เงียบ", "Notifications": "การแจ้งเตือน", "Operation failed": "การดำเนินการล้มเหลว", @@ -380,6 +380,8 @@ "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "การแสดงผลของโปรแกรมอาจผิดพลาด ฟังก์ชันบางอย่างหรือทั้งหมดอาจไม่ทำงานในเบราว์เซอร์ปัจจุบันของคุณ หากคุณต้องการลองดำเนินการต่อ คุณต้องรับมือกับปัญหาที่อาจจะเกิดขึ้นด้วยตัวคุณเอง!", "Checking for an update...": "กำลังตรวจหาอัปเดต...", "Explore rooms": "สำรวจห้อง", - "Sign In": "เข้าสู่ระบบ", - "Create Account": "สร้างบัญชี" + "Sign In": "ลงชื่อเข้า", + "Create Account": "สร้างบัญชี", + "Add Email Address": "เพิ่มที่อยู่อีเมล", + "Confirm": "ยืนยัน" } From 8f5a74779afa7ed64bd7ba71b275d563cacf45cb Mon Sep 17 00:00:00 2001 From: Andrejs Date: Sun, 11 Apr 2021 17:33:37 +0000 Subject: [PATCH 082/330] Translated using Weblate (Latvian) Currently translated at 50.8% (1482 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/lv/ --- src/i18n/strings/lv.json | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index 4b07a93ea6..da167399d4 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -1559,5 +1559,24 @@ "Verify this session": "Verificēt šo sesiju", "You signed in to a new session without verifying it:": "Jūs pierakstījāties jaunā sesijā, neveicot tās verifikāciju:", "You're already in a call with this person.": "Jums jau notiek zvans ar šo personu.", - "Already in call": "Notiek zvans" + "Already in call": "Notiek zvans", + "%(deviceId)s from %(ip)s": "%(deviceId)s no %(ip)s", + "%(count)s people you know have already joined|other": "%(count)s pazīstami cilvēki ir jau pievienojusies", + "%(count)s people you know have already joined|one": "%(count)s pazīstama persona ir jau pievienojusies", + "Saving...": "Saglabā…", + "%(count)s members|one": "%(count)s dalībnieks", + "Save Changes": "Saglabāt izmaiņas", + "%(count)s messages deleted.|other": "%(count)s ziņas ir dzēstas.", + "%(count)s messages deleted.|one": "%(count)s ziņa ir dzēsta.", + "Welcome to ": "Laipni lūdzam uz ", + "Room name": "Istabas nosaukums", + "%(count)s members|other": "%(count)s dalībnieki", + "Room List": "Istabu saraksts", + "Send as message": "Nosūtīt kā ziņu", + "%(brand)s URL": "%(brand)s URL", + "Send a message…": "Nosūtīt ziņu…", + "Send a reply…": "Nosūtīt atbildi…", + "Room version": "Istabas versija", + "Room list": "Istabu saraksts", + "Failed to set topic": "Neizdevās iestatīt tematu" } From 4a6a53a1ca6486452bb6c414c052470a434e4b0c Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Thu, 15 Apr 2021 07:25:30 +0000 Subject: [PATCH 083/330] Translated using Weblate (Swedish) Currently translated at 100.0% (2916 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ --- src/i18n/strings/sv.json | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index a3147634c7..42a7f78268 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3180,5 +3180,41 @@ "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Från %(deviceName)s %(deviceId)s på %(ip)s", "Check your devices": "Kolla dina enheter", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "En ny inloggning kommer åt ditt konto: %(name)s %(deviceID)s på %(ip)s", - "You have unverified logins": "Du har overifierade inloggningar" + "You have unverified logins": "Du har overifierade inloggningar", + "%(count)s people you know have already joined|other": "%(count)s personer du känner har redan gått med", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Om du gör det, observera att inga av dina meddelanden kommer att raderas, men din sökupplevelse kommer att degraderas en stund medans registret byggs upp igen", + "What are some things you want to discuss in %(spaceName)s?": "Vad är några saker du vill diskutera i %(spaceName)s?", + "You can add more later too, including already existing ones.": "Du kan lägga till flera senare också, inklusive redan existerande.", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Tillfrågar %(transferTarget)s. %(transferTarget)sÖverför till %(transferee)s", + "Review to ensure your account is safe": "Granska för att försäkra dig om att ditt konto är säkert", + "%(deviceId)s from %(ip)s": "%(deviceId)s från %(ip)s", + "Send and receive voice messages (in development)": "Skicka och ta emot röstmeddelanden (under utveckling)", + "unknown person": "okänd person", + "Warn before quitting": "Varna innan avslutning", + "Invite to just this room": "Bjud in till bara det här rummet", + "Invite messages are hidden by default. Click to show the message.": "Inbjudningsmeddelanden är dolda som förval. Klicka för att visa meddelandet.", + "Record a voice message": "Spela in ett röstmeddelande", + "Stop & send recording": "Stoppa och skicka inspelning", + "Accept on your other login…": "Acceptera på din andra inloggning…", + "%(count)s people you know have already joined|one": "%(count)s person du känner har redan gått med", + "Quick actions": "Snabbhandlingar", + "Add existing rooms": "Lägg till existerande rum", + "Adding...": "Lägger till…", + "We couldn't create your DM.": "Vi kunde inte skapa ditt DM.", + "Reset event store": "Återställ händelselagring", + "Invited people will be able to read old messages.": "Inbjudna personer kommer att kunna läsa gamla meddelanden.", + "Reset event store?": "Återställ händelselagring?", + "You most likely do not want to reset your event index store": "Du vill troligen inte återställa din händelseregisterlagring", + "Consult first": "Tillfråga först", + "Verify other login": "Verifiera annan inloggning", + "Avatar": "Avatar", + "Let's create a room for each of them.": "Låt oss skapa ett rum för varje.", + "Verification requested": "Verifiering begärd", + "Sends the given message as a spoiler": "Skickar det angivna meddelandet som en spoiler", + "Manage & explore rooms": "Hantera och utforska rum", + "Message search initilisation failed": "Initialisering av meddelandesökning misslyckades", + "Please choose a strong password": "Vänligen välj ett starkt lösenord", + "Use another login": "Använd annan inloggning", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifiera din identitet för att komma åt krypterade meddelanden och bevisa din identitet för andra.", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Om du inte verifierar så kommer du inte ha åtkomst till alla dina meddelanden och kan synas som ej betrodd för andra." } From a59873df0bd813e3b7f3eb7844dbd7d610bf035a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Apr 2021 15:51:00 +0100 Subject: [PATCH 084/330] Set rooms event listeners during the correct life cycle hook --- src/components/views/rooms/RoomList.tsx | 5 +-- src/components/views/rooms/RoomSublist.tsx | 7 ++- src/components/views/rooms/RoomTile.tsx | 51 ++++++++++++---------- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 963e94ebbb..93c72621af 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -289,12 +289,11 @@ export default class RoomList extends React.PureComponent { // shallow-copy from the template as we need to make modifications to it this.tagAesthetics = objectShallowClone(TAG_AESTHETICS); this.updateDmAddRoomAction(); - - this.dispatcherRef = defaultDispatcher.register(this.onAction); - this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); } public componentDidMount(): void { + this.dispatcherRef = defaultDispatcher.register(this.onAction); + this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists); diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 74052e8ba1..410325ef46 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -125,8 +125,6 @@ export default class RoomSublist extends React.Component { }; // Why Object.assign() and not this.state.height? Because TypeScript says no. this.state = Object.assign(this.state, {height: this.calculateInitialHeight()}); - this.dispatcherRef = defaultDispatcher.register(this.onAction); - RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated); } private calculateInitialHeight() { @@ -242,6 +240,11 @@ export default class RoomSublist extends React.Component { return false; } + public componentDidMount() { + this.dispatcherRef = defaultDispatcher.register(this.onAction); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated); + } + public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated); diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index a32fc46a80..ad923138df 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -97,22 +97,6 @@ export default class RoomTile extends React.PureComponent { // generatePreview() will return nothing if the user has previews disabled messagePreview: this.generatePreview(), }; - - ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); - this.dispatcherRef = defaultDispatcher.register(this.onAction); - MessagePreviewStore.instance.on( - MessagePreviewStore.getPreviewChangedEventName(this.props.room), - this.onRoomPreviewChanged, - ); - this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); - this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); - this.roomProps = EchoChamber.forRoom(this.props.room); - this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate); - CommunityPrototypeStore.instance.on( - CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId), - this.onCommunityUpdate, - ); - this.props.room.on("Room.name", this.onRoomNameUpdate); } private onRoomNameUpdate = (room) => { @@ -167,6 +151,22 @@ export default class RoomTile extends React.PureComponent { if (this.state.selected) { this.scrollIntoView(); } + + ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); + this.dispatcherRef = defaultDispatcher.register(this.onAction); + MessagePreviewStore.instance.on( + MessagePreviewStore.getPreviewChangedEventName(this.props.room), + this.onRoomPreviewChanged, + ); + this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); + this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); + this.roomProps = EchoChamber.forRoom(this.props.room); + this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate); + this.roomProps.on("Room.name", this.onRoomNameUpdate); + CommunityPrototypeStore.instance.on( + CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId), + this.onCommunityUpdate, + ); } public componentWillUnmount() { @@ -182,8 +182,15 @@ export default class RoomTile extends React.PureComponent { ); this.props.room.off("Room.name", this.onRoomNameUpdate); } + ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); defaultDispatcher.unregister(this.dispatcherRef); this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); + this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate); + this.roomProps.off("Room.name", this.onRoomNameUpdate); + CommunityPrototypeStore.instance.off( + CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId), + this.onCommunityUpdate, + ); } private onAction = (payload: ActionPayload) => { @@ -371,7 +378,7 @@ export default class RoomTile extends React.PureComponent { return null; } - const state = this.roomProps.notificationVolume; + const state = this.roomProps?.notificationVolume; let contextMenu = null; if (this.state.notificationsMenuPosition) { @@ -547,7 +554,7 @@ export default class RoomTile extends React.PureComponent { />; let badge: React.ReactNode; - if (!this.props.isMinimized) { + if (!this.props.isMinimized && this.notificationState) { // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below badge = ( ) : null } - { spaces.length + rooms.length < 1 ? + { dms.length > 0 ? ( +
+

{ _t("Direct Messages") }

+ { dms.map(space => { + return { + if (checked) { + selectedToAdd.add(space); + } else { + selectedToAdd.delete(space); + } + setSelectedToAdd(new Set(selectedToAdd)); + }} + />; + }) } +
+ ) : null } + + { spaces.length + rooms.length + dms.length < 1 ? { _t("No results") } : undefined } From 64e0626693a2e007157ad0e7caffdb9b17c20b4b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 19 Apr 2021 11:13:08 +0100 Subject: [PATCH 095/330] i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6ac73611f1..067edec4cf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2015,6 +2015,7 @@ "Add existing rooms": "Add existing rooms", "Filter your rooms and spaces": "Filter your rooms and spaces", "Spaces": "Spaces", + "Direct Messages": "Direct Messages", "Don't want to add an existing room?": "Don't want to add an existing room?", "Create a new room": "Create a new room", "Failed to add rooms to space": "Failed to add rooms to space", @@ -2205,7 +2206,6 @@ "Suggestions": "Suggestions", "May include members not in %(communityName)s": "May include members not in %(communityName)s", "Recently Direct Messaged": "Recently Direct Messaged", - "Direct Messages": "Direct Messages", "Start a conversation with someone using their name, email address or username (like ).": "Start a conversation with someone using their name, email address or username (like ).", "Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here", From 71d5f03a25285cb43e024c31fd29c3305402084f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 19 Apr 2021 11:36:40 +0100 Subject: [PATCH 096/330] delint --- src/components/structures/MatrixChat.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index dd64dd76f9..2328481b40 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1098,7 +1098,8 @@ export default class MatrixChat extends React.PureComponent { warnings.push(( {' '/* Whitespace, otherwise the sentences get smashed together */ } - { _t("You are the only person here. If you leave, no one will be able to join in the future, including you.") } + { _t("You are the only person here. " + + "If you leave, no one will be able to join in the future, including you.") } )); From db646d5987ab5dbba957e7a245a2cf74346f6bd5 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 19 Apr 2021 15:07:11 +0100 Subject: [PATCH 097/330] Fix end to end tests for DM creation --- .../src/usecases/create-room.js | 10 +- test/end-to-end-tests/src/usecases/signup.js | 1 + test/end-to-end-tests/yarn.lock | 142 ++++++++++-------- 3 files changed, 81 insertions(+), 72 deletions(-) diff --git a/test/end-to-end-tests/src/usecases/create-room.js b/test/end-to-end-tests/src/usecases/create-room.js index 35b9d5879e..3830e3e0da 100644 --- a/test/end-to-end-tests/src/usecases/create-room.js +++ b/test/end-to-end-tests/src/usecases/create-room.js @@ -21,15 +21,7 @@ async function openRoomDirectory(session) { } async function findSublist(session, name) { - const sublists = await session.queryAll('.mx_RoomSublist'); - for (const sublist of sublists) { - const header = await sublist.$('.mx_RoomSublist_headerText'); - const headerText = await session.innerText(header); - if (headerText.toLowerCase().includes(name.toLowerCase())) { - return sublist; - } - } - throw new Error(`could not find room list section that contains '${name}' in header`); + return await session.query(`.mx_RoomSublist[aria-label="${name}" i]`); } async function createRoom(session, roomName, encrypted=false) { diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index 804cee9599..c0ab82be33 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -32,6 +32,7 @@ module.exports = async function signup(session, username, password, homeserver) await nextButton.click(); } //fill out form + await session.delay(100); const usernameField = await session.query("#mx_RegistrationForm_username"); const passwordField = await session.query("#mx_RegistrationForm_password"); const passwordRepeatField = await session.query("#mx_RegistrationForm_passwordConfirm"); diff --git a/test/end-to-end-tests/yarn.lock b/test/end-to-end-tests/yarn.lock index 2f4d9979fb..7f2cefb92e 100644 --- a/test/end-to-end-tests/yarn.lock +++ b/test/end-to-end-tests/yarn.lock @@ -37,9 +37,9 @@ assert-plus@1.0.0, assert-plus@^1.0.0: integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= async-limiter@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" - integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== asynckit@^0.4.0: version "0.4.0" @@ -57,9 +57,9 @@ aws4@^1.8.0: integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== bcrypt-pbkdf@^1.0.0: version "1.0.2" @@ -81,6 +81,11 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -120,7 +125,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2: +concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -157,7 +162,7 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -debug@2.6.9: +debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -165,18 +170,18 @@ debug@2.6.9: ms "2.0.0" debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" debug@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: - ms "^2.1.1" + ms "2.1.2" delayed-stream@~1.0.0: version "1.0.0" @@ -250,14 +255,14 @@ extend@~3.0.2: integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== extract-zip@^1.6.6: - version "1.6.7" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" - integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== dependencies: - concat-stream "1.6.2" - debug "2.6.9" - mkdirp "0.5.1" - yauzl "2.4.1" + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" extsprintf@1.3.0: version "1.3.0" @@ -279,10 +284,10 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= dependencies: pend "~1.2.0" @@ -313,9 +318,9 @@ getpass@^0.1.1: assert-plus "^1.0.0" glob@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -374,7 +379,12 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= @@ -442,9 +452,9 @@ mime-types@^2.1.12, mime-types@~2.1.19: mime-db "~1.38.0" mime@^2.0.3: - version "2.4.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6" - integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w== + version "2.5.2" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" + integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== minimatch@^3.0.4: version "3.0.4" @@ -453,28 +463,33 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= +mkdirp@^0.5.4: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: - minimist "0.0.8" + minimist "^1.2.5" ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + nth-check@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -517,9 +532,9 @@ performance-now@^2.1.0: integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== progress@^2.0.1: version "2.0.3" @@ -527,9 +542,9 @@ progress@^2.0.1: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== proxy-from-env@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" - integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== psl@^1.1.24, psl@^1.1.28: version "1.1.31" @@ -547,9 +562,9 @@ punycode@^2.1.0, punycode@^2.1.1: integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== puppeteer@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.14.0.tgz#828c1926b307200d5fc8289b99df4e13e962d339" - integrity sha512-SayS2wUX/8LF8Yo2Rkpc5nkAu4Jg3qu+OLTDSOZtisVQMB2Z5vjlY2TdPi/5CgZKiZroYIiyUN3sRX63El9iaw== + version "1.20.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.20.0.tgz#e3d267786f74e1d87cf2d15acc59177f471bbe38" + integrity sha512-bt48RDBy2eIwZPrkgbcwHtb51mj2nKvHOPMaSH2IsWiv7lOG9k9zhaRzpDZafrk05ajMc3cu+lSQYYOfH2DkVQ== dependencies: debug "^4.1.0" extract-zip "^1.6.6" @@ -566,9 +581,9 @@ qs@~6.5.2: integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== readable-stream@^2.2.2: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -630,9 +645,9 @@ request@^2.88.0: uuid "^3.3.2" rimraf@^2.6.1: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== dependencies: glob "^7.1.3" @@ -751,9 +766,10 @@ ws@^6.1.0: dependencies: async-limiter "~1.0.0" -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= dependencies: - fd-slicer "~1.0.1" + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" From 344e4b6c5bfa105bb03046b08dc99e7803d47bba Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 19 Apr 2021 16:15:24 +0100 Subject: [PATCH 098/330] Trigger lazy loading when filtering using spaces so that the filtered DMs are correct --- src/stores/SpaceStore.tsx | 20 ++++++++++++++++++++ src/stores/room-list/SpaceWatcher.ts | 16 +++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 52060f86a5..dcdbc73cb7 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -525,6 +525,26 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.notificationStateMap.set(key, state); return state; } + + // traverse space tree with DFS calling fn on each space including the given root one + public traverseSpace( + spaceId: string, + fn: (roomId: string) => void, + includeRooms = false, + parentPath?: Set, + ) { + if (parentPath && parentPath.has(spaceId)) return; // prevent cycles + + fn(spaceId); + + const newPath = new Set(parentPath).add(spaceId); + const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId)); + + if (includeRooms) { + childRooms.forEach(r => fn(r.roomId)); + } + childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath)); + } } export default class SpaceStore { diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts index d26f563a91..13e1d83901 100644 --- a/src/stores/room-list/SpaceWatcher.ts +++ b/src/stores/room-list/SpaceWatcher.ts @@ -28,12 +28,22 @@ export class SpaceWatcher { private activeSpace: Room = SpaceStore.instance.activeSpace; constructor(private store: RoomListStoreClass) { - this.filter.updateSpace(this.activeSpace); // get the filter into a consistent state + this.updateFilter(); // get the filter into a consistent state store.addFilter(this.filter); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated); } - private onSelectedSpaceUpdated = (activeSpace) => { - this.filter.updateSpace(this.activeSpace = activeSpace); + private onSelectedSpaceUpdated = (activeSpace: Room) => { + this.activeSpace = activeSpace; + this.updateFilter(); + }; + + private updateFilter = () => { + if (this.activeSpace) { + SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => { + this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); + }); + } + this.filter.updateSpace(this.activeSpace); }; } From 20586e52bc1d4e3598dc96660a7b987920124837 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Apr 2021 10:13:22 -0600 Subject: [PATCH 099/330] Update src/utils/Singleflight.ts to support English Co-authored-by: J. Ryan Stinnett --- src/utils/Singleflight.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Singleflight.ts b/src/utils/Singleflight.ts index 1c2af4278d..c2a564ea3e 100644 --- a/src/utils/Singleflight.ts +++ b/src/utils/Singleflight.ts @@ -30,7 +30,7 @@ const keyMap = new EnhancedMap>(); * to disable a button, however it would be capable of returning a Promise * from the first call. * - * The result of the function call are cached indefinitely, just in case a + * The result of the function call is cached indefinitely, just in case a * second call comes through late. There are various functions named "forget" * to have the cache be cleared of a result. * From 4082a037697e559938c8163a5e19d10a96f806eb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 19 Apr 2021 17:32:15 +0100 Subject: [PATCH 100/330] Fix typo in method call in add existing to space dialog --- src/components/views/dialogs/AddExistingToSpaceDialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 7ea6b43194..0f58a624f3 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -59,6 +59,7 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, const existingSubspacesSet = new Set(existingSubspaces); const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId)); + const joinRule = selectedSpace.getJoinRule(); const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => { if (room.getMyMembership() !== "join") return arr; if (!room.name.toLowerCase().includes(lcQuery)) return arr; @@ -67,7 +68,7 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, if (room !== space && room !== selectedSpace && !existingSubspacesSet.has(room)) { arr[0].push(room); } - } else if (!existingRoomsSet.has(room) && selectedSpace.joinRule() !== "public") { + } else if (!existingRoomsSet.has(room) && joinRule !== "public") { // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. arr[DMRoomMap.shared().getUserIdForRoomId(room.roomId) ? 2 : 1].push(room); } From 33eebb84a6c718d2fc14962dc467c01498a278ab Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 19 Apr 2021 17:57:20 +0100 Subject: [PATCH 101/330] Ensure PersistedElement are unmounted on application logout --- src/components/structures/MatrixChat.tsx | 1 + src/components/views/elements/PersistedElement.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index d9ed7d061b..b8e591943b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -586,6 +586,7 @@ export default class MatrixChat extends React.PureComponent { break; case 'logout': dis.dispatch({action: "hangup_all"}); + dis.dispatch({action: "logout"}); Lifecycle.logout(); break; case 'require_registration': diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index f504b3e97f..701c140a19 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -139,6 +139,8 @@ export default class PersistedElement extends React.Component { _onAction(payload) { if (payload.action === 'timeline_resize') { this._repositionChild(); + } else if (payload.action === 'logout') { + PersistedElement.destroyElement(this.props.persistKey); } } From de5ca92e4e03913b1c19d51c47dc36b502a867cd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 19 Apr 2021 18:01:19 +0100 Subject: [PATCH 102/330] add e2e session.delay explainer --- test/end-to-end-tests/src/usecases/signup.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/end-to-end-tests/src/usecases/signup.js b/test/end-to-end-tests/src/usecases/signup.js index c0ab82be33..a3391e99f3 100644 --- a/test/end-to-end-tests/src/usecases/signup.js +++ b/test/end-to-end-tests/src/usecases/signup.js @@ -31,8 +31,10 @@ module.exports = async function signup(session, username, password, homeserver) // accept homeserver await nextButton.click(); } - //fill out form + // Delay required because of local race condition on macOs + // Where the form is not query-able despite being present in the DOM await session.delay(100); + //fill out form const usernameField = await session.query("#mx_RegistrationForm_username"); const passwordField = await session.query("#mx_RegistrationForm_password"); const passwordRepeatField = await session.query("#mx_RegistrationForm_passwordConfirm"); From b52d6e3d976ed0f7464992b744348b1ab508ec78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 19 Apr 2021 20:52:40 +0200 Subject: [PATCH 103/330] A tiny change to make the dialog a little nicer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/dialogs/_AddExistingToSpaceDialog.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index a7cfd7bde6..dd5a6d618b 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -102,6 +102,7 @@ limitations under the License. .mx_SearchBox { margin: 0; + margin-bottom: 24px; flex-grow: 0; } @@ -123,7 +124,9 @@ limitations under the License. } .mx_AddExistingToSpaceDialog_section { - margin-top: 24px; + &:not(:first-child) { + margin-top: 24px; + } > h3 { margin: 0; From 910a3e5f3104ac769e5c1b7bf877362251ba32f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 19 Apr 2021 20:59:54 +0200 Subject: [PATCH 104/330] A little nicer spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/dialogs/_AddExistingToSpaceDialog.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index dd5a6d618b..80ad4d6c0e 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -101,8 +101,8 @@ limitations under the License. } .mx_SearchBox { - margin: 0; - margin-bottom: 24px; + // To match the space around the title + margin: 0 0 15px 0; flex-grow: 0; } From 59c5ab31ded093a5825e5c2a0d2e948ad680644e Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 19 Apr 2021 20:30:51 +0100 Subject: [PATCH 105/330] Support MSC3086 asserted identity --- src/CallHandler.tsx | 58 +++++++++++++++++-- src/VoipUserMapper.ts | 6 +- src/components/views/voip/CallPreview.tsx | 2 + src/components/views/voip/CallView.tsx | 18 +++--- src/components/views/voip/CallViewForRoom.tsx | 2 + src/components/views/voip/IncomingCallBox.tsx | 6 +- src/dispatcher/actions.ts | 3 + 7 files changed, 78 insertions(+), 17 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index be687a4474..886c594c94 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -86,6 +86,9 @@ import { Action } from './dispatcher/actions'; import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring"; +import SdkConfig from './SdkConfig'; +import DMRoomMap from './utils/DMRoomMap'; +import { ensureDMExists, findDMForUser } from './createRoom'; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -167,6 +170,11 @@ export default class CallHandler { private invitedRoomsAreVirtual = new Map(); private invitedRoomCheckInProgress = false; + // Map of the asserted identiy users after we've looked them up using the API. + // We need to be be able to determine the mapped room synchronously, so we + // do the async lookup when we get new information and then store these mappings here + private assertedIdentityNativeUsers = new Map(); + static sharedInstance() { if (!window.mxCallHandler) { window.mxCallHandler = new CallHandler() @@ -179,8 +187,17 @@ export default class CallHandler { * Gets the user-facing room associated with a call (call.roomId may be the call "virtual room" * if a voip_mxid_translate_pattern is set in the config) */ - public static roomIdForCall(call: MatrixCall): string { + public roomIdForCall(call: MatrixCall): string { if (!call) return null; + + if (SdkConfig.get()['voipObeyAssertedIdentity']) { + const nativeUser = this.assertedIdentityNativeUsers[call.callId]; + if (nativeUser) { + const room = findDMForUser(MatrixClientPeg.get(), nativeUser); + if (room) return room.roomId + } + } + return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId; } @@ -379,14 +396,14 @@ export default class CallHandler { // We don't allow placing more than one call per room, but that doesn't mean there // can't be more than one, eg. in a glare situation. This checks that the given call // is the call we consider 'the' call for its room. - const mappedRoomId = CallHandler.roomIdForCall(call); + const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); const callForThisRoom = this.getCallForRoom(mappedRoomId); return callForThisRoom && call.callId === callForThisRoom.callId; } private setCallListeners(call: MatrixCall) { - const mappedRoomId = CallHandler.roomIdForCall(call); + let mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); call.on(CallEvent.Error, (err: CallError) => { if (!this.matchesCallForThisRoom(call)) return; @@ -500,6 +517,37 @@ export default class CallHandler { this.setCallListeners(newCall); this.setCallState(newCall, newCall.state); }); + call.on(CallEvent.AssertedIdentityChanged, async () => { + if (!this.matchesCallForThisRoom(call)) return; + + console.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity()); + + const newAssertedIdentity = call.getRemoteAssertedIdentity().id; + let newNativeAssertedIdentity = newAssertedIdentity; + if (newAssertedIdentity) { + const response = await this.sipNativeLookup(newAssertedIdentity); + if (response.length) newNativeAssertedIdentity = response[0].userid; + } + console.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); + + if (newNativeAssertedIdentity) { + this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity; + + await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity); + + const newMappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); + console.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`); + if (newMappedRoomId !== mappedRoomId) { + this.removeCallForRoom(mappedRoomId); + mappedRoomId = newMappedRoomId; + this.calls.set(mappedRoomId, call); + dis.dispatch({ + action: Action.CallChangeRoom, + call, + }); + } + } + }); } private async logCallStats(call: MatrixCall, mappedRoomId: string) { @@ -551,7 +599,7 @@ export default class CallHandler { } private setCallState(call: MatrixCall, status: CallState) { - const mappedRoomId = CallHandler.roomIdForCall(call); + const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); console.log( `Call state in ${mappedRoomId} changed to ${status}`, @@ -772,7 +820,7 @@ export default class CallHandler { const call = payload.call as MatrixCall; - const mappedRoomId = CallHandler.roomIdForCall(call); + const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); if (this.getCallForRoom(mappedRoomId)) { // ignore multiple incoming calls to the same room return; diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index 4f5613b4a8..e5bed2e812 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -57,7 +57,11 @@ export default class VoipUserMapper { if (!virtualRoom) return null; const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null; - return virtualRoomEvent.getContent()['native_room'] || null; + const nativeRoomID = virtualRoomEvent.getContent()['native_room']; + const nativeRoom = MatrixClientPeg.get().getRoom(nativeRoomID); + if (!nativeRoom || nativeRoom.getMyMembership() !== 'join') return null; + + return nativeRoomID; } public isVirtualRoom(room: Room): boolean { diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 29de068b0c..d31afddec9 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -27,6 +27,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { Action } from '../../../dispatcher/actions'; const SHOW_CALL_IN_STATES = [ CallState.Connected, @@ -142,6 +143,7 @@ export default class CallPreview extends React.Component { switch (payload.action) { // listen for call state changes to prod the render method, which // may hide the global CallView if the call it is tracking is dead + case Action.CallChangeRoom: case 'call_state': { const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls( CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId), diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 8a6ed75fee..6745713845 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -208,7 +208,7 @@ export default class CallView extends React.Component { }; private onExpandClick = () => { - const userFacingRoomId = CallHandler.roomIdForCall(this.props.call); + const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); dis.dispatch({ action: 'view_room', room_id: userFacingRoomId, @@ -337,7 +337,7 @@ export default class CallView extends React.Component { }; private onRoomAvatarClick = () => { - const userFacingRoomId = CallHandler.roomIdForCall(this.props.call); + const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); dis.dispatch({ action: 'view_room', room_id: userFacingRoomId, @@ -345,7 +345,7 @@ export default class CallView extends React.Component { } private onSecondaryRoomAvatarClick = () => { - const userFacingRoomId = CallHandler.roomIdForCall(this.props.secondaryCall); + const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall); dis.dispatch({ action: 'view_room', @@ -354,7 +354,7 @@ export default class CallView extends React.Component { } private onCallResumeClick = () => { - const userFacingRoomId = CallHandler.roomIdForCall(this.props.call); + const userFacingRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); CallHandler.sharedInstance().setActiveCallRoomId(userFacingRoomId); } @@ -365,8 +365,8 @@ export default class CallView extends React.Component { public render() { const client = MatrixClientPeg.get(); - const callRoomId = CallHandler.roomIdForCall(this.props.call); - const secondaryCallRoomId = CallHandler.roomIdForCall(this.props.secondaryCall); + const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); + const secondaryCallRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.secondaryCall); const callRoom = client.getRoom(callRoomId); const secCallRoom = this.props.secondaryCall ? client.getRoom(secondaryCallRoomId) : null; @@ -482,11 +482,13 @@ export default class CallView extends React.Component { const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold; let holdTransferContent; if (transfereeCall) { - const transferTargetRoom = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.props.call)); + const transferTargetRoom = MatrixClientPeg.get().getRoom( + CallHandler.sharedInstance().roomIdForCall(this.props.call), + ); const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person"); const transfereeRoom = MatrixClientPeg.get().getRoom( - CallHandler.roomIdForCall(transfereeCall), + CallHandler.sharedInstance().roomIdForCall(transfereeCall), ); const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person"); diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx index 878b6af20f..7540dbc8d9 100644 --- a/src/components/views/voip/CallViewForRoom.tsx +++ b/src/components/views/voip/CallViewForRoom.tsx @@ -22,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher'; import {Resizable} from "re-resizable"; import ResizeNotifier from "../../../utils/ResizeNotifier"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { Action } from '../../../dispatcher/actions'; interface IProps { // What room we should display the call for @@ -62,6 +63,7 @@ export default class CallViewForRoom extends React.Component { private onAction = (payload) => { switch (payload.action) { + case Action.CallChangeRoom: case 'call_state': { const newCall = this.getCall(); if (newCall !== this.state.call) { diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index 0ca2a196c2..2abdc0641d 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -72,7 +72,7 @@ export default class IncomingCallBox extends React.Component { e.stopPropagation(); dis.dispatch({ action: 'answer', - room_id: CallHandler.roomIdForCall(this.state.incomingCall), + room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), }); }; @@ -80,7 +80,7 @@ export default class IncomingCallBox extends React.Component { e.stopPropagation(); dis.dispatch({ action: 'reject', - room_id: CallHandler.roomIdForCall(this.state.incomingCall), + room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), }); }; @@ -91,7 +91,7 @@ export default class IncomingCallBox extends React.Component { let room = null; if (this.state.incomingCall) { - room = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.state.incomingCall)); + room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall)); } const caller = room ? room.name : _t("Unknown caller"); diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index cd32c3743f..46c962f160 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -114,6 +114,9 @@ export enum Action { */ VirtualRoomSupportUpdated = "virtual_room_support_updated", + // Probably would be better to have a VoIP states in a store and have the store emit changes + CallChangeRoom = "call_change_room", + /** * Fired when an upload has started. Should be used with UploadStartedPayload. */ From 10d056eb4170e917d12e7d7a6334b9b5f8a7a316 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 19 Apr 2021 20:34:48 +0100 Subject: [PATCH 106/330] unused import --- src/CallHandler.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 886c594c94..81172e2b46 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -87,7 +87,6 @@ import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring"; import SdkConfig from './SdkConfig'; -import DMRoomMap from './utils/DMRoomMap'; import { ensureDMExists, findDMForUser } from './createRoom'; export const PROTOCOL_PSTN = 'm.protocol.pstn'; From ee96201e33e15c3fdf2a659ef79944aa83fd60dd Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 19 Apr 2021 21:05:05 +0100 Subject: [PATCH 107/330] Comment room creation insanity --- src/CallHandler.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 81172e2b46..1686a671ed 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -532,6 +532,11 @@ export default class CallHandler { if (newNativeAssertedIdentity) { this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity; + // If we don't already have a room with this user, make one. This will be slightly odd + // if they called us because we'll be inviting them, but there's not much we can do about + // this if we want the actual, native room to exist (which we do). This is why it's + // important to only obey asserted identity in trusted environments, since anyone you're + // on a call with can cause you to send a room invite to someone. await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity); const newMappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); From 6f794cca9b173ddf6d3a3b71088ff15dd93d53c5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Apr 2021 13:05:27 -0600 Subject: [PATCH 108/330] Fill in some metadata for the sent event --- .../views/rooms/VoiceRecordComposerTile.tsx | 23 +++++++++++++++++++ src/voice/VoiceRecording.ts | 15 +++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 1210a44958..f46b7c6311 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -55,7 +55,30 @@ export default class VoiceRecordComposerTile extends React.PureComponent r['content_uri']); From 7d9562137ef2768c34214c122ba99b77cf5a28f4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Apr 2021 21:54:08 -0600 Subject: [PATCH 109/330] Replace deprecated processor with a worklet --- src/@types/global.d.ts | 27 ++++++++++++++++++++++++ src/voice/RecorderWorklet.ts | 37 +++++++++++++++++++++++++++++++++ src/voice/VoiceRecording.ts | 40 ++++++++++++++++++++++-------------- src/voice/consts.ts | 29 ++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 src/voice/RecorderWorklet.ts create mode 100644 src/voice/consts.ts diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index ee0963e537..78dad28566 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -129,4 +129,31 @@ declare global { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/columnNumber columnNumber?: number; } + + // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 + interface AudioWorkletProcessor { + readonly port: MessagePort; + process( + inputs: Float32Array[][], + outputs: Float32Array[][], + parameters: Record + ): boolean; + + } + + // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 + const AudioWorkletProcessor: { + prototype: AudioWorkletProcessor; + new (options?: AudioWorkletNodeOptions): AudioWorkletProcessor; + }; + + // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 + function registerProcessor( + name: string, + processorCtor: (new ( + options?: AudioWorkletNodeOptions + ) => AudioWorkletProcessor) & { + parameterDescriptors?: AudioParamDescriptor[]; + } + ); } diff --git a/src/voice/RecorderWorklet.ts b/src/voice/RecorderWorklet.ts new file mode 100644 index 0000000000..11f24fce4c --- /dev/null +++ b/src/voice/RecorderWorklet.ts @@ -0,0 +1,37 @@ +/* +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 {ITimingPayload, PayloadEvent, WORKLET_NAME} from "./consts"; + +// from AudioWorkletGlobalScope: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletGlobalScope +declare const currentTime: number; +declare const currentFrame: number; +declare const sampleRate: number; + +class MxVoiceWorklet extends AudioWorkletProcessor { + constructor() { + super(); + } + + process(inputs, outputs, parameters) { + this.port.postMessage({ev: PayloadEvent.Timekeep, timeSeconds: currentTime}); + return true; + } +} + +registerProcessor(WORKLET_NAME, MxVoiceWorklet); + +export default null; // to appease module loaders (we never use the export) diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index fc52a38fa9..8e506c235c 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -23,6 +23,7 @@ import {clamp} from "../utils/numbers"; import EventEmitter from "events"; import {IDestroyable} from "../utils/IDestroyable"; import {Singleflight} from "../utils/Singleflight"; +import {PayloadEvent, WORKLET_NAME} from "./consts"; const CHANNELS = 1; // stereo isn't important const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. @@ -49,7 +50,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private recorderSource: MediaStreamAudioSourceNode; private recorderStream: MediaStream; private recorderFFT: AnalyserNode; - private recorderProcessor: ScriptProcessorNode; + private recorderWorklet: AudioWorkletNode; private buffer = new Uint8Array(0); private mxc: string; private recording = false; @@ -93,18 +94,28 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { // it makes the time domain less than helpful. this.recorderFFT.fftSize = 64; - // We use an audio processor to get accurate timing information. - // The size of the audio buffer largely decides how quickly we push timing/waveform data - // out of this class. Smaller buffers mean we update more frequently as we can't hold as - // many bytes. Larger buffers mean slower updates. For scale, 1024 gives us about 30Hz of - // updates and 2048 gives us about 20Hz. We use 1024 to get as close to perceived realtime - // as possible. Must be a power of 2. - this.recorderProcessor = this.recorderContext.createScriptProcessor(1024, CHANNELS, CHANNELS); + // Set up our worklet. We use this for timing information and waveform analysis: the + // web audio API prefers this be done async to avoid holding the main thread with math. + const mxRecorderWorkletPath = document.body.dataset.vectorRecorderWorkletScript; + if (!mxRecorderWorkletPath) { + throw new Error("Unable to create recorder: no worklet script registered"); + } + await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath); + this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME); // Connect our inputs and outputs this.recorderSource.connect(this.recorderFFT); - this.recorderSource.connect(this.recorderProcessor); - this.recorderProcessor.connect(this.recorderContext.destination); + this.recorderSource.connect(this.recorderWorklet); + this.recorderWorklet.connect(this.recorderContext.destination); + + // Dev note: we can't use `addEventListener` for some reason. It just doesn't work. + this.recorderWorklet.port.onmessage = (ev) => { + switch(ev.data['ev']) { + case PayloadEvent.Timekeep: + this.processAudioUpdate(ev.data['timeSeconds']); + break; + } + }; this.recorder = new Recorder({ encoderPath, // magic from webpack @@ -151,7 +162,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { return this.mxc; } - private processAudioUpdate = (ev: AudioProcessingEvent) => { + private processAudioUpdate = (timeSeconds: number) => { if (!this.recording) return; // The time domain is the input to the FFT, which means we use an array of the same @@ -175,12 +186,12 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { this.observable.update({ waveform: translatedData, - timeSeconds: ev.playbackTime, + timeSeconds: timeSeconds, }); // Now that we've updated the data/waveform, let's do a time check. We don't want to // go horribly over the limit. We also emit a warning state if needed. - const secondsLeft = TARGET_MAX_LENGTH - ev.playbackTime; + const secondsLeft = TARGET_MAX_LENGTH - timeSeconds; if (secondsLeft <= 0) { // noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping this.stop(); @@ -204,7 +215,6 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { } this.observable = new SimpleObservable(); await this.makeRecorder(); - this.recorderProcessor.addEventListener("audioprocess", this.processAudioUpdate); await this.recorder.start(); this.recording = true; this.emit(RecordingState.Started); @@ -218,6 +228,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { // Disconnect the source early to start shutting down resources this.recorderSource.disconnect(); + this.recorderWorklet.disconnect(); await this.recorder.stop(); // close the context after the recorder so the recorder doesn't try to @@ -229,7 +240,6 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { // Finally do our post-processing and clean up this.recording = false; - this.recorderProcessor.removeEventListener("audioprocess", this.processAudioUpdate); await this.recorder.close(); this.emit(RecordingState.Ended); diff --git a/src/voice/consts.ts b/src/voice/consts.ts new file mode 100644 index 0000000000..dbd3b574f4 --- /dev/null +++ b/src/voice/consts.ts @@ -0,0 +1,29 @@ +/* +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. +*/ + +export const WORKLET_NAME = "mx-voice-worklet"; + +export enum PayloadEvent { + Timekeep = "timekeep", +} + +export interface IPayload { + ev: PayloadEvent; +} + +export interface ITimingPayload extends IPayload { + timeSeconds: number; +} From 61730f2f881292bfcdf5becd249c0fc4c45edb1e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Apr 2021 23:05:06 -0600 Subject: [PATCH 110/330] Populate waveform data on voice message event --- .../views/rooms/VoiceRecordComposerTile.tsx | 8 ++++- src/utils/arrays.ts | 2 +- src/voice/RecorderWorklet.ts | 36 ++++++++++++++++++- src/voice/VoiceRecording.ts | 12 +++++++ src/voice/consts.ts | 8 +++++ 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index f46b7c6311..05beb3a0ca 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -77,7 +77,13 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)), }, }); await VoiceRecordingStore.instance.disposeRecording(); diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index 52308937f7..8ab66dfb29 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -54,7 +54,7 @@ export function arraySeed(val: T, length: number): T[] { * @param a The array to clone. Must be defined. * @returns A copy of the array. */ -export function arrayFastClone(a: any[]): any[] { +export function arrayFastClone(a: T[]): T[] { return a.slice(0, a.length); } diff --git a/src/voice/RecorderWorklet.ts b/src/voice/RecorderWorklet.ts index 11f24fce4c..8d6f1e9627 100644 --- a/src/voice/RecorderWorklet.ts +++ b/src/voice/RecorderWorklet.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ITimingPayload, PayloadEvent, WORKLET_NAME} from "./consts"; +import {IAmplitudePayload, ITimingPayload, PayloadEvent, WORKLET_NAME} from "./consts"; +import {percentageOf} from "../utils/numbers"; // from AudioWorkletGlobalScope: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletGlobalScope declare const currentTime: number; @@ -22,12 +23,45 @@ declare const currentFrame: number; declare const sampleRate: number; class MxVoiceWorklet extends AudioWorkletProcessor { + private nextAmplitudeSecond = 0; + constructor() { super(); } process(inputs, outputs, parameters) { + // We only fire amplitude updates once a second to avoid flooding the recording instance + // with useless data. Much of the data would end up discarded, so we ratelimit ourselves + // here. + const currentSecond = Math.round(currentTime); + if (currentSecond === this.nextAmplitudeSecond) { + // We're expecting exactly one mono input source, so just grab the very first frame of + // samples for the analysis. + const monoChan = inputs[0][0]; + + // The amplitude of the frame's samples is effectively the loudness of the frame. This + // translates into a bar which can be rendered as part of the whole recording clip's + // waveform. + // + // We translate the amplitude down to 0-1 for sanity's sake. + const minVal = monoChan.reduce((m, v) => Math.min(m, v), Number.MAX_SAFE_INTEGER); + const maxVal = monoChan.reduce((m, v) => Math.max(m, v), Number.MIN_SAFE_INTEGER); + const amplitude = percentageOf(maxVal, -1, 1) - percentageOf(minVal, -1, 1); + + this.port.postMessage({ + ev: PayloadEvent.AmplitudeMark, + amplitude: amplitude, + forSecond: currentSecond, + }); + this.nextAmplitudeSecond++; + } + + // We mostly use this worklet to fire regular clock updates through to components this.port.postMessage({ev: PayloadEvent.Timekeep, timeSeconds: currentTime}); + + // We're supposed to return false when we're "done" with the audio clip, but seeing as + // we are acting as a passive processor we are never truly "done". The browser will clean + // us up when it is done with us. return true; } } diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 8e506c235c..716936f636 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -24,6 +24,7 @@ import EventEmitter from "events"; import {IDestroyable} from "../utils/IDestroyable"; import {Singleflight} from "../utils/Singleflight"; import {PayloadEvent, WORKLET_NAME} from "./consts"; +import {arrayFastClone} from "../utils/arrays"; const CHANNELS = 1; // stereo isn't important const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. @@ -55,11 +56,16 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private mxc: string; private recording = false; private observable: SimpleObservable; + private amplitudes: number[] = []; // at each second mark, generated public constructor(private client: MatrixClient) { super(); } + public get finalWaveform(): number[] { + return arrayFastClone(this.amplitudes); + } + public get contentType(): string { return "audio/ogg"; } @@ -114,6 +120,12 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { case PayloadEvent.Timekeep: this.processAudioUpdate(ev.data['timeSeconds']); break; + case PayloadEvent.AmplitudeMark: + // Sanity check to make sure we're adding about one sample per second + if (ev.data['forSecond'] === this.amplitudes.length) { + this.amplitudes.push(ev.data['amplitude']); + } + break; } }; diff --git a/src/voice/consts.ts b/src/voice/consts.ts index dbd3b574f4..c530c60f0b 100644 --- a/src/voice/consts.ts +++ b/src/voice/consts.ts @@ -18,6 +18,7 @@ export const WORKLET_NAME = "mx-voice-worklet"; export enum PayloadEvent { Timekeep = "timekeep", + AmplitudeMark = "amplitude_mark", } export interface IPayload { @@ -25,5 +26,12 @@ export interface IPayload { } export interface ITimingPayload extends IPayload { + ev: PayloadEvent.Timekeep; timeSeconds: number; } + +export interface IAmplitudePayload extends IPayload { + ev: PayloadEvent.AmplitudeMark; + forSecond: number; + amplitude: number; +} From 4f75e2944cd8fd399c9de461c68505961b2cb7a7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 19 Apr 2021 23:11:41 -0600 Subject: [PATCH 111/330] Appease the linter --- src/components/views/rooms/VoiceRecordComposerTile.tsx | 10 +++++----- src/voice/RecorderWorklet.ts | 4 ++-- src/voice/VoiceRecording.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 05beb3a0ca..9b7f0da472 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -53,11 +53,11 @@ export default class VoiceRecordComposerTile extends React.PureComponent { - switch(ev.data['ev']) { + switch (ev.data['ev']) { case PayloadEvent.Timekeep: this.processAudioUpdate(ev.data['timeSeconds']); break; From 548cd38a5c1af71b666603189bafebf6d499d181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 08:22:43 +0200 Subject: [PATCH 112/330] Remove weird margin from the file panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/structures/_FilePanel.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss index 2aa068b674..7b975110e1 100644 --- a/res/css/structures/_FilePanel.scss +++ b/res/css/structures/_FilePanel.scss @@ -22,7 +22,6 @@ limitations under the License. } .mx_FilePanel .mx_RoomView_messageListWrapper { - margin-right: 20px; flex-direction: row; align-items: center; justify-content: center; From a3617fa3cdc0545e94421d73d0d108e64ab3e007 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 20 Apr 2021 08:51:14 +0100 Subject: [PATCH 113/330] Remove unnecessary logout action --- src/components/structures/MatrixChat.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b8e591943b..d9ed7d061b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -586,7 +586,6 @@ export default class MatrixChat extends React.PureComponent { break; case 'logout': dis.dispatch({action: "hangup_all"}); - dis.dispatch({action: "logout"}); Lifecycle.logout(); break; case 'require_registration': From 97c775c203eae11cd6327c83af74f34d569272a1 Mon Sep 17 00:00:00 2001 From: Hannah Rittich Date: Sun, 18 Apr 2021 09:09:48 +0000 Subject: [PATCH 114/330] Translated using Weblate (German) Currently translated at 98.5% (2875 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 027f1be177..7a73e8d761 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2258,7 +2258,7 @@ "Dark": "Dunkel", "Use the improved room list (will refresh to apply changes)": "Verwende die verbesserte Raumliste (lädt die Anwendung neu)", "Use custom size": "Verwende individuelle Größe", - "Hey you. You're the best!": "Hey du. Du bist der Beste!", + "Hey you. You're the best!": "Hey du. Du bist großartig.", "Message layout": "Nachrichtenlayout", "Compact": "Kompakt", "Modern": "Modern", @@ -3238,5 +3238,13 @@ "Sends the given message as a spoiler": "Die gegebene Nachricht als Spoiler senden", "Values at explicit levels in this room:": "Werte für explizite Stufen in diesem Raum:", "Values at explicit levels:": "Werte für explizite Stufen:", - "Values at explicit levels in this room": "Werte für explizite Stufen in diesem Raum" + "Values at explicit levels in this room": "Werte für explizite Stufen in diesem Raum", + "Confirm abort of host creation": "Bestätige das Abbrechen der Host-Erstellung", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Soll die Host-Erstellung wirklich abgebrochen werden? Dieser Prozess kann nicht wieder fortgesetzt werden.", + "Invite to just this room": "Nur für diesen Raum einladen", + "Consult first": "Konsultiere zuerst", + "Reset event store?": "Ereigniss-Speicher zurück setzen?", + "You most likely do not want to reset your event index store": "Es ist wahrscheinlich, dass du den Ereigniss-Index-Speicher nicht zurück setzen möchtest", + "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few momentswhilst the index is recreated": "Falls du dies tust, werden keine deiner Nachrichten gelöscht. Allerdings wird die Such-Funktion eine Weile lang schlecht funktionieren, bis der Index wieder hergestellt ist", + "Reset event store": "Ereignis-Speicher zurück setzen" } From 0de0a3028393a71fc2c0dea2379fc8659b6863a4 Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 16 Apr 2021 13:29:08 +0000 Subject: [PATCH 115/330] Translated using Weblate (German) Currently translated at 98.5% (2875 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 7a73e8d761..bfb226aa3c 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -11,7 +11,7 @@ "The email address linked to your account must be entered.": "Es muss die mit dem Benutzerkonto verbundene E-Mail-Adresse eingegeben werden.", "Name": "Name", "Session ID": "Sitzungs-ID", - "Displays action": "Zeigt Aktionen an", + "Displays action": "Als Aktionen anzeigen", "Bans user with given id": "Verbannt den Benutzer mit der angegebenen ID", "Deops user with given id": "Setzt das Berechtigungslevel beim Benutzer mit der angegebenen ID zurück", "Invites user with given id to current room": "Lädt den Benutzer mit der angegebenen ID in den aktuellen Raum ein", @@ -302,7 +302,7 @@ "Idle": "Abwesend", "Ongoing conference call%(supportedText)s.": "Laufendes Konferenzgespräch%(supportedText)s.", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Um dein Konto für die Verwendung von %(integrationsUrl)s zu authentifizieren, wirst du jetzt auf die Website eines Drittanbieters weitergeleitet. Möchtest du fortfahren?", - "Start automatically after system login": "Nach Systemanmeldung automatisch starten", + "Start automatically after system login": "Nach Systemstart automatisch starten", "Jump to first unread message.": "Zur ersten ungelesenen Nachricht springen.", "Options": "Optionen", "Invited": "Eingeladen", @@ -795,7 +795,7 @@ "We encountered an error trying to restore your previous session.": "Wir haben ein Problem beim Wiederherstellen deiner vorherigen Sitzung festgestellt.", "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Den Browser-Speicher zu löschen kann das Problem lösen, wird dich aber abmelden und verschlüsselte Chats unlesbar machen.", "Collapse Reply Thread": "Antwort-Thread zusammenklappen", - "Enable widget screenshots on supported widgets": "Widgetbildschirmfotos bei unterstützten Widgets aktivieren", + "Enable widget screenshots on supported widgets": "Bildschirmfotos bei unterstützten Widgets aktivieren", "Send analytics data": "Analysedaten senden", "e.g. %(exampleValue)s": "z.B. %(exampleValue)s", "Muted Users": "Stummgeschaltete Benutzer", @@ -850,7 +850,7 @@ "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Deine Nachricht wurde nicht gesendet, weil dieser Heimserver sein Limit an monatlich aktiven Benutzern erreicht hat. Bitte kontaktiere deinen Systemadministrator um diesen Dienst weiter zu nutzen.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Deine Nachricht wurde nicht gesendet, weil dieser Heimserver ein Ressourcen-Limit erreicht hat. Bitte kontaktiere deinen Systemadministrator um diesen Dienst weiter zu nutzen.", "Please contact your service administrator to continue using this service.": "Bitte kontaktiere deinen Systemadministrator um diesen Dienst weiter zu nutzen.", - "Sorry, your homeserver is too old to participate in this room.": "Tschuldige, dein Heimserver ist zu alt, um an diesem Raum teilzunehmen.", + "Sorry, your homeserver is too old to participate in this room.": "Leider ist dein Heimserver zu alt, um an diesem Raum teilzunehmen.", "Please contact your homeserver administrator.": "Bitte setze dich mit der Administration deines Heimservers in Verbindung.", "Legal": "Rechtliches", "This room has been replaced and is no longer active.": "Dieser Raum wurde ersetzt und ist nicht länger aktiv.", @@ -1149,7 +1149,7 @@ "Report bugs & give feedback": "Melde Fehler & gib Rückmeldungen", "Update status": "Aktualisiere Status", "Set status": "Setze Status", - "Hide": "Verberge", + "Hide": "Verbergen", "This homeserver would like to make sure you are not a robot.": "Dieser Heimserver möchte sicherstellen, dass du kein Roboter bist.", "Server Name": "Servername", "Your Modular server": "Dein Modular-Server", @@ -2535,7 +2535,7 @@ "Hide Widgets": "Widgets verstecken", "%(senderName)s declined the call.": "%(senderName)s hat den Anruf abgelehnt.", "(an error occurred)": "(ein Fehler ist aufgetreten)", - "(their device couldn't start the camera / microphone)": "(Gerät des Gegenübers konnte Kamera oder Mikrophon nicht starten)", + "(their device couldn't start the camera / microphone)": "(Gerät des Gegenübers konnte Kamera oder Mikrofon nicht starten)", "(connection failed)": "(Verbindung fehlgeschlagen)", "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Alle Server sind von der Teilnahme ausgeschlossen! Dieser Raum kann nicht mehr genutzt werden.", "%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s hat die Server-ACLs für diesen Raum geändert.", @@ -2617,7 +2617,7 @@ "You ended the call": "Du hast den Anruf beendet", "%(senderName)s ended the call": "%(senderName)s hat den Anruf beendet", "Use Command + Enter to send a message": "Benutze Betriebssystemtaste + Eingabe um eine Nachricht zu senden", - "Use Ctrl + Enter to send a message": "Nachrichten mit Strg + Eingabe senden", + "Use Ctrl + Enter to send a message": "Nachrichten mit Strg + Enter senden", "Call Paused": "Anruf pausiert", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern, um sie in Suchergebnissen finden zu können. Es werden %(size)s benötigt, um die Nachrichten von %(rooms)s Räumen zu speichern.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Verschlüsselte Nachrichten sicher lokal zwischenspeichern, um sie in Suchergebnissen finden zu können. Es werden %(size)s benötigt, um die Nachrichten vom Raum %(rooms)s zu speichern.", @@ -3229,7 +3229,7 @@ "Review to ensure your account is safe": "Überprüfen, um sicher zu sein, dass dein Konto sicher ist", "Message search initilisation failed": "Initialisierung der Nachrichtensuche fehlgeschlagen", "Support": "Unterstützen", - "This room is suggested as a good one to join": "Dieser Raum wurde als einen guten zum Beitreten vorgeschlagen", + "This room is suggested as a good one to join": "Dieser Raum wurde als gut zum Beitreten vorgeschlagen", "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Deine Nachricht wurde nicht versendet, weil dieser Heimserver von dessen Administrator gesperrt wurde. Bitte kontaktiere deinen Dienstadministrator um den Dienst weiterzunutzen.", "Verification requested": "Verifizierung angefragt", "Avatar": "Avatar", From 4929247a5eb21cf437253c0c8513c7019648c6e9 Mon Sep 17 00:00:00 2001 From: Sven Grewe Date: Fri, 16 Apr 2021 14:19:37 +0000 Subject: [PATCH 116/330] Translated using Weblate (German) Currently translated at 98.5% (2875 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index bfb226aa3c..9551f00e55 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1424,7 +1424,7 @@ "Cancel entering passphrase?": "Eingabe der Passphrase abbrechen?", "Setting up keys": "Einrichten der Schlüssel", "Encryption upgrade available": "Verschlüsselungsaufstufung verfügbar", - "Verifies a user, session, and pubkey tuple": "Verifiziert einen Benutzer, eine Sitzung und die Endlichkeit eines öffentlichen Schlüssels", + "Verifies a user, session, and pubkey tuple": "Verifiziert einen Benutzer, eine Sitzung und die öffentlichen Schlüsselpaare", "Unknown (user, session) pair:": "Unbekanntes Nutzer-/Sitzungspaar:", "Session already verified!": "Sitzung bereits verifiziert!", "WARNING: Session already verified, but keys do NOT MATCH!": "WARNUNG: Die Sitzung wurde bereits verifiziert, aber die Schlüssel passen NICHT ZUSAMMEN!", From 0eee2149662ef62b54cde30161ab73ba1f0bf53b Mon Sep 17 00:00:00 2001 From: MamasLT Date: Sun, 18 Apr 2021 17:04:04 +0000 Subject: [PATCH 117/330] Translated using Weblate (Lithuanian) Currently translated at 65.1% (1901 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/lt/ --- src/i18n/strings/lt.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index d700b5e800..83b59681e7 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -1184,7 +1184,7 @@ "Manage integrations": "Valdyti integracijas", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integracijų Tvarkytuvai gauna konfigūracijos duomenis ir jūsų vardu gali keisti valdiklius, siųsti kambario pakvietimus ir nustatyti galios lygius.", "Invalid theme schema.": "Klaidinga temos schema.", - "Error downloading theme information.": "Klaida parsisiunčiant temos informaciją.", + "Error downloading theme information.": "Klaida atsisiunčiant temos informaciją.", "Theme added!": "Tema pridėta!", "Custom theme URL": "Pasirinktinės temos URL", "Add theme": "Pridėti temą", @@ -2101,5 +2101,6 @@ "Preparing to download logs": "Ruošiamasi parsiųsti žurnalus", "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use Element with an existing Matrix account on a different homeserver.": "Jūs galite naudoti serverio parinktis, norėdami prisijungti prie kitų Matrix serverių, nurodydami kitą serverio URL. Tai leidžia jums naudoti Element su egzistuojančia paskyra kitame serveryje.", "Server Options": "Serverio Parinktys", - "Your homeserver": "Jūsų serveris" + "Your homeserver": "Jūsų serveris", + "Download logs": "Parsisiųsti žurnalus" } From d0df0e9099e41cfa021ae5481809ba435b935bff Mon Sep 17 00:00:00 2001 From: Imre Kristoffer Eilertsen Date: Sun, 18 Apr 2021 10:39:37 +0000 Subject: [PATCH 118/330] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 63.0% (1839 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nb_NO/ --- src/i18n/strings/nb_NO.json | 429 +++++++++++++++++++++++++++++++++++- 1 file changed, 428 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index 9253f74fab..d3be9cd2ea 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -1554,5 +1554,432 @@ "Click the button below to confirm adding this phone number.": "Klikk knappen nedenfor for å bekrefte dette telefonnummeret.", "Single Sign On": "Single Sign On", "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bekreft dette telefonnummeret ved å bruke Single Sign On for å bevise din identitet.", - "Confirm adding this email address by using Single Sign On to prove your identity.": "Befrekt denne e-postadressen ved å bruke Single Sign On for å bevise din identitet." + "Confirm adding this email address by using Single Sign On to prove your identity.": "Befrekt denne e-postadressen ved å bruke Single Sign On for å bevise din identitet.", + "Show stickers button": "Vis klistremerkeknappen", + "Recently visited rooms": "Nylig besøkte rom", + "Windows": "Vinduer", + "Abort": "Avbryt", + "You have unverified logins": "Du har uverifiserte pålogginger", + "Check your devices": "Sjekk enhetene dine", + "Record a voice message": "Send en stemmebeskjed", + "Edit devices": "Rediger enheter", + "Homeserver": "Hjemmetjener", + "Edit Values": "Rediger verdier", + "Add existing room": "Legg til et eksisterende rom", + "Spell check dictionaries": "Stavesjekk-ordbøker", + "Invite to this space": "Inviter til dette området", + "Send message": "Send melding", + "Cookie Policy": "Infokapselretningslinjer", + "Invite to %(roomName)s": "Inviter til %(roomName)s", + "Resume": "Fortsett", + "Avatar": "Profilbilde", + "A confirmation email has been sent to %(emailAddress)s": "En bekreftelses-E-post har blitt sendt til %(emailAddress)s", + "Suggested Rooms": "Foreslåtte rom", + "Welcome %(name)s": "Velkommen, %(name)s", + "Upgrade to %(hostSignupBrand)s": "Oppgrader til %(hostSignupBrand)s", + "Verification requested": "Verifisering ble forespurt", + "%(count)s members|one": "%(count)s medlem", + "Removing...": "Fjerner …", + "No results found": "Ingen resultater ble funnet", + "Public space": "Offentlig område", + "Private space": "Privat område", + "Support": "Support", + "What projects are you working on?": "Hvilke prosjekter jobber du på?", + "Suggested": "Anbefalte", + "%(deviceId)s from %(ip)s": "%(deviceId)s fra %(ip)s", + "Accept on your other login…": "Aksepter på din andre pålogging …", + "Value:": "Verdi:", + "Leave Space": "Forlat området", + "View dev tools": "Vis utviklerverktøy", + "Saving...": "Lagrer …", + "Save Changes": "Lagre endringer", + "Verify other login": "Verifiser en annen pålogging", + "You don't have permission": "Du har ikke tillatelse", + "%(count)s rooms|other": "%(count)s rom", + "%(count)s rooms|one": "%(count)s rom", + "Invite by username": "Inviter etter brukernavn", + "Delete": "Slett", + "Your public space": "Ditt offentlige område", + "Your private space": "Ditt private område", + "Invite to %(spaceName)s": "Inviter til %(spaceName)s", + "%(count)s members|other": "%(count)s medlemmer", + "Random": "Tilfeldig", + "unknown person": "ukjent person", + "Public": "Offentlig", + "Private": "Privat", + "Click to copy": "Klikk for å kopiere", + "Share invite link": "Del invitasjonslenke", + "Leave space": "Forlat området", + "Warn before quitting": "Advar før avslutning", + "Quick actions": "Hurtigvalg", + "Screens": "Skjermer", + "%(count)s people you know have already joined|other": "%(count)s personer du kjenner har allerede blitt med", + "Add existing rooms": "Legg til eksisterende rom", + "Don't want to add an existing room?": "Vil du ikke legge til et eksisterende rom?", + "Create a new room": "Opprett et nytt rom", + "Adding...": "Legger til …", + "Settings Explorer": "Innstillingsutforsker", + "Value": "Verdi", + "Setting:": "Innstilling:", + "Caution:": "Advarsel:", + "Level": "Nivå", + "Privacy Policy": "Personvern", + "You should know": "Du bør vite", + "Room name": "Rommets navn", + "Skip for now": "Hopp over for nå", + "Creating rooms...": "Oppretter rom …", + "Share %(name)s": "Del %(name)s", + "Just me": "Bare meg selv", + "Inviting...": "Inviterer …", + "Please choose a strong password": "Vennligst velg et sterkt passord", + "New? Create account": "Er du ny her? Opprett en konto", + "Use another login": "Bruk en annen pålogging", + "Use Security Key or Phrase": "Bruk sikkerhetsnøkkel eller -frase", + "Use Security Key": "Bruk sikkerhetsnøkkel", + "Upgrade private room": "Oppgrader privat rom", + "Upgrade public room": "Oppgrader offentlig rom", + "Decline All": "Avslå alle", + "Enter Security Key": "Skriv inn sikkerhetsnøkkel", + "Germany": "Tyskland", + "Malta": "Malta", + "Uruguay": "Uruguay", + "Community settings": "Fellesskapsinnstillinger", + "You’re all caught up": "Du har lest deg opp på alt det nye", + "Remember this": "Husk dette", + "Move right": "Gå til høyre", + "Notify the whole room": "Varsle hele rommet", + "Got an account? Sign in": "Har du en konto? Logg på", + "You created this room.": "Du opprettet dette rommet.", + "Security Phrase": "Sikkerhetsfrase", + "Start a Conversation": "Start en samtale", + "Open dial pad": "Åpne nummerpanelet", + "Message deleted on %(date)s": "Meldingen ble slettet den %(date)s", + "Approve": "Godkjenn", + "Create community": "Opprett fellesskap", + "Already have an account? Sign in here": "Har du allerede en konto? Logg på", + "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s eller %(usernamePassword)s", + "That username already exists, please try another.": "Det brukernavnet finnes allerede, vennligst prøv et annet et", + "New here? Create an account": "Er du ny her? Opprett en konto", + "Now, let's help you get started": "Nå, la oss hjelpe deg med å komme i gang", + "Forgot password?": "Glemt passord?", + "Enter email address": "Legg inn e-postadresse", + "Enter phone number": "Skriv inn telefonnummer", + "Please enter the code it contains:": "Vennligst skriv inn koden den inneholder:", + "Token incorrect": "Sjetongen er feil", + "A text message has been sent to %(msisdn)s": "En SMS har blitt sendt til %(msisdn)s", + "Open the link in the email to continue registration.": "Åpne lenken i E-posten for å fortsette registreringen.", + "This room is public": "Dette rommet er offentlig", + "Move left": "Gå til venstre", + "Take a picture": "Ta et bilde", + "Hold": "Hold", + "Enter Security Phrase": "Skriv inn sikkerhetsfrase", + "Security Key": "Sikkerhetsnøkkel", + "Invalid Security Key": "Ugyldig sikkerhetsnøkkel", + "Wrong Security Key": "Feil sikkerhetsnøkkel", + "About homeservers": "Om hjemmetjenere", + "New Recovery Method": "Ny gjenopprettingsmetode", + "Generate a Security Key": "Generer en sikkerhetsnøkkel", + "Confirm your Security Phrase": "Bekreft sikkerhetsfrasen din", + "Your Security Key": "Sikkerhetsnøkkelen din", + "Repeat your Security Phrase...": "Gjenta sikkerhetsfrasen din", + "Set up with a Security Key": "Sett opp med en sikkerhetsnøkkel", + "Use app": "Bruk app", + "Learn more": "Lær mer", + "Use app for a better experience": "Bruk appen for en bedre opplevelse", + "Continue with %(provider)s": "Fortsett med %(provider)s", + "This address is already in use": "Denne adressen er allerede i bruk", + "In reply to ": "Som svar på ", + "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)sendret navnet sitt %(count)s ganger", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)sfikk sin invitasjon trukket tilbake", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)sfikk sine invitasjoner trukket tilbake", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)savslo invitasjonen sin", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sforlot og ble med igjen", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sforlot og ble med igjen", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sble med og forlot igjen", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sble med og forlot igjen", + "Information": "Informasjon", + "Add rooms to this community": "Legg til rom i dette fellesskapet", + "%(name)s cancelled verifying": "%(name)s avbrøt verifiseringen", + "You cancelled verifying %(name)s": "Du avbrøt verifiseringen av %(name)s", + "Invalid file%(extra)s": "Ugyldig fil%(extra)s", + "Failed to ban user": "Mislyktes i å bannlyse brukeren", + "Room settings": "Rominnstillinger", + "Show files": "Vis filer", + "Not encrypted": "Ikke kryptert", + "About": "Om", + "Widgets": "Komponenter", + "Room Info": "Rominfo", + "Favourited": "Favorittmerket", + "Forget Room": "Glem rommet", + "Show previews of messages": "Vis forhåndsvisninger av meldinger", + "Invalid URL": "Ugyldig URL", + "Continuing without email": "Fortsetter uten E-post", + "Are you sure you want to sign out?": "Er du sikker på at du vil logge av?", + "Transfer": "Overfør", + "Invite by email": "Inviter gjennom E-post", + "Waiting for partner to confirm...": "Venter på at partneren skal bekrefte …", + "Report a bug": "Rapporter en feil", + "Comment": "Kommentar", + "Add comment": "Legg til kommentar", + "Active Widgets": "Aktive moduler", + "Create a room in %(communityName)s": "Opprett et rom i %(communityName)s", + "Reason (optional)": "Årsak (valgfritt)", + "Send %(count)s invites|one": "Send %(count)s invitasjon", + "Send %(count)s invites|other": "Send %(count)s invitasjoner", + "Add another email": "Legg til en annen E-postadresse", + "%(count)s results|one": "%(count)s resultat", + "%(count)s results|other": "%(count)s resultater", + "Start a new chat": "Start en ny chat", + "Custom Tag": "Egendefinert merkelapp", + "Explore public rooms": "Utforsk offentlige rom", + "Explore community rooms": "Utforsk samfunnsrom", + "Invite to this community": "Inviter til dette fellesskapet", + "Verify the link in your inbox": "Verifiser lenken i innboksen din", + "Bridges": "Broer", + "Privacy": "Personvern", + "Reject all %(invitedRooms)s invites": "Avslå alle %(invitedRooms)s-invitasjoner", + "Upgrade Room Version": "Oppgrader romversjon", + "You cancelled verification.": "Du avbrøt verifiseringen.", + "Ask %(displayName)s to scan your code:": "Be %(displayName)s om å skanne koden:", + "Role": "Rolle", + "Failed to deactivate user": "Mislyktes i å deaktivere brukeren", + "Accept all %(invitedRooms)s invites": "Aksepter alle %(invitedRooms)s-invitasjoner", + "": "", + "Custom theme URL": "URL-en til et selvvalgt tema", + "not ready": "ikke klar", + "ready": "klar", + "Algorithm:": "Algoritme:", + "Backing up %(sessionsRemaining)s keys...": "Sikkerhetskopierer %(sessionsRemaining)s nøkler …", + "Away": "Borte", + "Start chat": "Start chat", + "Show Widgets": "Vis moduler", + "Hide Widgets": "Skjul moduler", + "Unknown for %(duration)s": "Ukjent i %(duration)s", + "Update %(brand)s": "Oppdater %(brand)s", + "You are currently ignoring:": "Du ignorerer for øyeblikket:", + "Unknown caller": "Ukjent oppringer", + "Dial pad": "Nummerpanel", + "%(name)s on hold": "%(name)s står på vent", + "Fill Screen": "Fyll skjermen", + "Voice Call": "Taleanrop", + "Video Call": "Videoanrop", + "sends confetti": "sender konfetti", + "System font name": "Systemskrifttypenavn", + "Use a system font": "Bruk en systemskrifttype", + "Waiting for answer": "Venter på svar", + "Call in progress": "Anrop pågår", + "Channel: ": "Kanal: ", + "Enable desktop notifications": "Aktiver skrivebordsvarsler", + "Don't miss a reply": "Ikke gå glipp av noen svar", + "Help us improve %(brand)s": "Hjelp oss å forbedre %(brand)s", + "Unknown App": "Ukjent app", + "Short keyboard patterns are easy to guess": "Korte tastatur mønstre er lett å gjette", + "This is similar to a commonly used password": "Dette ligner på et passord som er brukt mye", + "Predictable substitutions like '@' instead of 'a' don't help very much": "Forutsigbar erstatninger som ‘ @‘ istedet for ‘a’ hjelper ikke mye", + "Reversed words aren't much harder to guess": "Ord som er skrevet baklengs er vanskeligere å huske.", + "All-uppercase is almost as easy to guess as all-lowercase": "Bare store bokstaver er nesten like enkelt å gjette som bare små bokstaver", + "Capitalization doesn't help very much": "Store bokstaver er ikke spesielt nyttig", + "Use a longer keyboard pattern with more turns": "Bruke et lengre og mer uventet tastatur mønster", + "No need for symbols, digits, or uppercase letters": "Ikke nødvendig med symboler, sifre eller bokstaver", + "See images posted to this room": "Se bilder som er lagt ut i dette rommet", + "%(senderName)s declined the call.": "%(senderName)s avslo oppringingen.", + "(an error occurred)": "(en feil oppstod)", + "(connection failed)": "(tilkobling mislyktes)", + "Change the topic of this room": "Endre dette rommets tema", + "Effects": "Effekter", + "Zimbabwe": "Zimbabwe", + "Yemen": "Jemen", + "Zambia": "Zambia", + "Western Sahara": "Vest-Sahara", + "Wallis & Futuna": "Wallis og Futuna", + "Venezuela": "Venezuela", + "Vietnam": "Vietnam", + "Vatican City": "Vatikanstaten", + "Vanuatu": "Vanuatu", + "Uzbekistan": "Usbekistan", + "United Arab Emirates": "De forente arabiske emirater", + "Ukraine": "Ukraina", + "U.S. Virgin Islands": "De amerikanske jomfruøyene", + "Uganda": "Uganda", + "Tuvalu": "Tuvalu", + "Turks & Caicos Islands": "Turks- og Caicosøyene", + "Turkmenistan": "Turkmenistan", + "Tunisia": "Tunis", + "Turkey": "Tyrkia", + "Trinidad & Tobago": "Trinidad og Tobago", + "Tonga": "Tonga", + "Tokelau": "Tokelau", + "Togo": "Togo", + "Timor-Leste": "Timor-Leste", + "Thailand": "Thailand", + "Tanzania": "Tanzania", + "Tajikistan": "Tadsjikistan", + "Taiwan": "Taiwan", + "São Tomé & Príncipe": "São Tomé og Príncipe", + "Syria": "Syria", + "Sweden": "Sverige", + "Switzerland": "Sveits", + "Swaziland": "Swaziland", + "Svalbard & Jan Mayen": "Svalbard og Jan Mayen", + "Suriname": "Surinam", + "Sudan": "Sudan", + "St. Vincent & Grenadines": "St. Vincent og Grenadinene", + "St. Kitts & Nevis": "St. Kitts og Nevis", + "St. Helena": "St. Helena", + "Sri Lanka": "Sri Lanka", + "Spain": "Spania", + "South Sudan": "Sør-Sudan", + "South Korea": "Syd-Korea", + "Somalia": "Somalia", + "South Africa": "Sør-Afrika", + "Solomon Islands": "Solomonøyene", + "Slovenia": "Slovenia", + "Slovakia": "Slovakia", + "Sint Maarten": "Sint Maarten", + "Singapore": "Singapore", + "Sierra Leone": "Sierra Leone", + "Seychelles": "Seyschellene", + "Serbia": "Serbia", + "Saudi Arabia": "Saudi-Arabia", + "Senegal": "Senegal", + "San Marino": "San Marino", + "Samoa": "Samoa", + "Réunion": "Réunion", + "Rwanda": "Rwanda", + "Russia": "Russland", + "Qatar": "Qatar", + "Romania": "Romania", + "Puerto Rico": "Puerto Rico", + "Portugal": "Portugal", + "Poland": "Polen", + "Pitcairn Islands": "Pitcairn-øyene", + "Philippines": "Filippinene", + "Peru": "Peru", + "Papua New Guinea": "Papua New Guinea", + "Paraguay": "Paraguay", + "Panama": "Panama", + "Palestine": "Palestina", + "Pakistan": "Pakistan", + "Palau": "Palau", + "Oman": "Oman", + "Norway": "Norge", + "Northern Mariana Islands": "Northern Mariana Islands", + "North Korea": "Nord-Korea", + "Norfolk Island": "Norfolkøyene", + "Niue": "Niue", + "Nigeria": "Nigeria", + "Niger": "Niger", + "New Zealand": "New Zealand", + "Nicaragua": "Nicaragua", + "New Caledonia": "New Caledonia", + "Netherlands": "Nederland", + "Nepal": "Nepal", + "Nauru": "Nauru", + "Namibia": "Namibia", + "Myanmar": "Myanmar", + "Mozambique": "Mosambik", + "Morocco": "Marokko", + "Montenegro": "Montenegro", + "Montserrat": "Montserrat", + "Mongolia": "Mongolia", + "Monaco": "Monaco", + "Moldova": "Moldova", + "Micronesia": "Mikronesia", + "Mexico": "Mexico", + "Mayotte": "Mayotte", + "Mauritius": "Mauritius", + "Mauritania": "Mauretania", + "Martinique": "Martinique", + "Marshall Islands": "Marshall Islands", + "Maldives": "Maldivene", + "Mali": "Mali", + "Malaysia": "Malaysia", + "Malawi": "Malawi", + "Madagascar": "Madagaskar", + "Macedonia": "Nord-Makedonia", + "Macau": "Macau", + "Luxembourg": "Luxemburg", + "Lithuania": "Litauen", + "Liechtenstein": "Liechtenstein", + "Libya": "Libya", + "Liberia": "Liberia", + "Lesotho": "Lesotho", + "Lebanon": "Libanon", + "Latvia": "Latvia", + "Laos": "Laos", + "Kyrgyzstan": "Kirgistan", + "Kuwait": "Kuwait", + "Kosovo": "Kosovo", + "Kiribati": "Kiribati", + "Kazakhstan": "Kasakstan", + "Kenya": "Kenya", + "Jamaica": "Jamaica", + "Isle of Man": "Man", + "Iceland": "Island", + "Hungary": "Ungarn", + "Hong Kong": "Hong Kong", + "Honduras": "Honduras", + "Haiti": "Haiti", + "Guinea-Bissau": "Guinea-Bissau", + "Guyana": "Guyana", + "Guinea": "Guinea", + "Guernsey": "Guernsey", + "Guatemala": "Guatemala", + "Guam": "Guam", + "Guadeloupe": "Guadeloupe", + "Grenada": "Grenada", + "Greece": "Hellas", + "Greenland": "Grønland", + "Gibraltar": "Gibraltar", + "Ghana": "Ghana", + "Georgia": "Georgia", + "Gambia": "Gambia", + "Gabon": "Gabon", + "French Southern Territories": "De franske sørterritoriene", + "French Polynesia": "Fransk polynesia", + "French Guiana": "Fransk Guyana", + "France": "Frankrike", + "Finland": "Finnland", + "Fiji": "Fiji", + "Falkland Islands": "Falklandsøyene", + "Faroe Islands": "Færøyene", + "Ethiopia": "Etiopia", + "Estonia": "Estland", + "Eritrea": "Eritrea", + "Equatorial Guinea": "Ekvatorial-Guinea", + "El Salvador": "El Salvador", + "Egypt": "Egypt", + "Ecuador": "Ecuador", + "Dominican Republic": "Dominikanske republikk", + "Djibouti": "Djibouti", + "Dominica": "Dominica", + "Denmark": "Danmark", + "Côte d’Ivoire": "Elfenbenskysten", + "Czech Republic": "Tsjekkia", + "Cyprus": "Kypros", + "Curaçao": "Curaçao", + "Cuba": "Kuba", + "Colombia": "Colombia", + "Comoros": "Komorene", + "Cocos (Keeling) Islands": "Cocos- (Keeling) øyene", + "Christmas Island": "Juløya", + "China": "Kina", + "Chad": "Tsjad", + "Chile": "Chile", + "Central African Republic": "Sentralafrikanske republikk", + "Cayman Islands": "Caymanøyene", + "Caribbean Netherlands": "Karibisk Nederland", + "Cape Verde": "Kapp Verde", + "Canada": "Canada", + "Cameroon": "Kamerun", + "Cambodia": "Kambodsja", + "British Virgin Islands": "De britiske jomfruøyer", + "British Indian Ocean Territory": "Britiske havområder i det indiske hav", + "Bouvet Island": "Bouvetøya", + "Bosnia": "Bosnia", + "Croatia": "Kroatia", + "Costa Rica": "Costa Rica", + "Cook Islands": "Cook-øyene", + "All keys backed up": "Alle nøkler er sikkerhetskopiert", + "Secret storage:": "Hemmelig lagring:" } From 8e2afcb5c20deec7803bfe09326302c7befcc7cf Mon Sep 17 00:00:00 2001 From: iaiz Date: Fri, 16 Apr 2021 09:56:09 +0000 Subject: [PATCH 119/330] Translated using Weblate (Spanish) Currently translated at 100.0% (2916 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 785ac28f37..d396cc318f 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3224,5 +3224,6 @@ "Please choose a strong password": "Por favor, elige una contraseña segura", "Use another login": "Usar otro inicio de sesión", "Verify your identity to access encrypted messages and prove your identity to others.": "Verifica tu identidad para acceder a mensajes cifrados y probar tu identidad a otros.", - "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Si no verificas no tendrás acceso a todos tus mensajes y puede que aparezcas como no confiable para otros usuarios." + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Si no verificas no tendrás acceso a todos tus mensajes y puede que aparezcas como no confiable para otros usuarios.", + "Invite messages are hidden by default. Click to show the message.": "Los mensajes de invitación no se muestran por defecto. Haz clic para mostrarlo." } From b3168e479e2a21ad4e973ce41b2cf9756e930ceb Mon Sep 17 00:00:00 2001 From: Sagititi Date: Fri, 16 Apr 2021 16:43:26 +0000 Subject: [PATCH 120/330] Translated using Weblate (Occitan) Currently translated at 11.3% (331 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/oc/ --- src/i18n/strings/oc.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/oc.json b/src/i18n/strings/oc.json index 1fcea5a976..d882b04ac9 100644 --- a/src/i18n/strings/oc.json +++ b/src/i18n/strings/oc.json @@ -340,5 +340,11 @@ "Space": "Espaci", "End": "Fin", "Explore rooms": "Percórrer las salas", - "Create Account": "Crear un compte" + "Create Account": "Crear un compte", + "Click the button below to confirm adding this email address.": "Clicatz sus lo boton aicí dejós per confirmar l'adicion de l'adreça e-mail.", + "Confirm adding email": "Confirmar l'adicion de l'adressa e-mail", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirmatz l'adicion d'aquela adreça e-mail en utilizant l'autentificacion unica per provar la vòstra identitat.", + "Use Single Sign On to continue": "Utilizar l'autentificacion unica (SSO) per contunhar", + "This phone number is already in use": "Aquel numèro de telefòn es ja utilizat", + "This email address is already in use": "Aquela adreça e-mail es ja utilizada" } From c30b62ef355d4ce59648821249f47b81c01f8019 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 20 Apr 2021 11:12:47 +0100 Subject: [PATCH 121/330] Fix alignment issue with nested spaces being cut off wrong --- res/css/structures/_SpacePanel.scss | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 202eaf0f4d..59f2ea947c 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -35,7 +35,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpacePanel_spaceTreeWrapper { flex: 1; - overflow-y: scroll; + padding: 8px 8px 16px 0; } .mx_SpacePanel_toggleCollapse { @@ -59,11 +59,10 @@ $activeBorderColor: $secondary-fg-color; margin: 0; list-style: none; padding: 0; - padding-left: 16px; - } - .mx_AutoHideScrollbar { - padding: 8px 0 16px; + > .mx_SpaceItem { + padding-left: 16px; + } } .mx_SpaceButton_toggleCollapse { From 08c0f0a67ef2068b64f0028279be7a53d4d82a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 12:18:46 +0200 Subject: [PATCH 122/330] Remove unnecessary check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 3585c803c1..ce174d9538 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -513,9 +513,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // persist last viewed room from a space - // We don't want to save if the room is a - // space room since it can cause problems - if (room && !room.isSpaceRoom()) { + if (room) { const activeSpaceId = this.activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; window.localStorage.setItem(`${LAST_VIEWED_ROOMS}_${activeSpaceId}`, payload.room_id); } From f9292c364caa2570b351939017f495dc820d43d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 12:25:56 +0200 Subject: [PATCH 123/330] Check if we are joined MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index ce174d9538..615007af20 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -118,7 +118,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const spaceId = space?.roomId || LAST_VIEWED_ROOMS_HOME; const roomId = window.localStorage.getItem(`${LAST_VIEWED_ROOMS}_${spaceId}`); - if (roomId) { + if (roomId && this.matrixClient.getRoom(roomId).getMyMembership() === "join") { defaultDispatcher.dispatch({ action: "view_room", room_id: roomId, From 1934c4a32f5d9cd9828d36e75fd8dd0900caea59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 12:39:11 +0200 Subject: [PATCH 124/330] Add getLastViewedRoomsStorageKey() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 615007af20..a7efbafd1e 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -41,7 +41,7 @@ type SpaceKey = string | symbol; interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; -const LAST_VIEWED_ROOMS = "mx_last_viewed_rooms"; + const LAST_VIEWED_ROOMS_HOME = "home_space"; @@ -54,6 +54,11 @@ export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); const MAX_SUGGESTED_ROOMS = 20; +const getLastViewedRoomsStorageKey = (spaceId?) => { + if (!spaceId) return null; + return `mx_last_viewed_rooms_${spaceId}`; +} + const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { result[room.isSpaceRoom() ? 0 : 1].push(room); @@ -116,7 +121,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // view last selected room from space const spaceId = space?.roomId || LAST_VIEWED_ROOMS_HOME; - const roomId = window.localStorage.getItem(`${LAST_VIEWED_ROOMS}_${spaceId}`); + const roomId = window.localStorage.getItem(getLastViewedRoomsStorageKey(spaceId)); if (roomId && this.matrixClient.getRoom(roomId).getMyMembership() === "join") { defaultDispatcher.dispatch({ @@ -515,7 +520,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (room) { const activeSpaceId = this.activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; - window.localStorage.setItem(`${LAST_VIEWED_ROOMS}_${activeSpaceId}`, payload.room_id); + window.localStorage.setItem(getLastViewedRoomsStorageKey(activeSpaceId), payload.room_id); } if (room?.getMyMembership() === "join") { From d4ca087c2eb6fe342a89cdb7ed7491edb2f7dd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 13:24:23 +0200 Subject: [PATCH 125/330] Make getLastViewedRoomsStorageKey() make sense MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index a7efbafd1e..eeecbbeba1 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -42,9 +42,6 @@ interface IState {} const ACTIVE_SPACE_LS_KEY = "mx_active_space"; - -const LAST_VIEWED_ROOMS_HOME = "home_space"; - export const HOME_SPACE = Symbol("home-space"); export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); @@ -54,9 +51,10 @@ export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); const MAX_SUGGESTED_ROOMS = 20; -const getLastViewedRoomsStorageKey = (spaceId?) => { - if (!spaceId) return null; - return `mx_last_viewed_rooms_${spaceId}`; +const getLastViewedRoomsStorageKey = (space?: Room) => { + const lastViewRooms = "mx_last_viewed_rooms"; + const homeSpace = "home_space"; + return `${lastViewRooms}_${space?.roomId || homeSpace}`; } const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] @@ -120,8 +118,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); // view last selected room from space - const spaceId = space?.roomId || LAST_VIEWED_ROOMS_HOME; - const roomId = window.localStorage.getItem(getLastViewedRoomsStorageKey(spaceId)); + const roomId = window.localStorage.getItem(getLastViewedRoomsStorageKey(this.activeSpace)); if (roomId && this.matrixClient.getRoom(roomId).getMyMembership() === "join") { defaultDispatcher.dispatch({ @@ -517,10 +514,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const room = this.matrixClient?.getRoom(payload.room_id); // persist last viewed room from a space - if (room) { - const activeSpaceId = this.activeSpace?.roomId || LAST_VIEWED_ROOMS_HOME; - window.localStorage.setItem(getLastViewedRoomsStorageKey(activeSpaceId), payload.room_id); + window.localStorage.setItem(getLastViewedRoomsStorageKey(this.activeSpace), payload.room_id); } if (room?.getMyMembership() === "join") { From 4344ff909714d6550f38acd28af253bcccef777f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 13:31:50 +0200 Subject: [PATCH 126/330] Update src/stores/SpaceStore.tsx Co-authored-by: Michael Telatynski <7t3chguy@googlemail.com> --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index eeecbbeba1..290caba9d4 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -120,7 +120,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // view last selected room from space const roomId = window.localStorage.getItem(getLastViewedRoomsStorageKey(this.activeSpace)); - if (roomId && this.matrixClient.getRoom(roomId).getMyMembership() === "join") { + if (roomId && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join") { defaultDispatcher.dispatch({ action: "view_room", room_id: roomId, From ace8d59a2af11c8dcd78d1c0a63a292ee4e9124e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 20 Apr 2021 13:12:28 +0100 Subject: [PATCH 127/330] Fix Spaces NPE when a room with no tags gains its first tag --- src/stores/SpaceStore.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 290caba9d4..63a95d90c7 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -446,11 +446,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; - private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent: MatrixEvent) => { + private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => { if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { // 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 newTags = ev.getContent()?.tags; + const oldTags = lastEvent?.getContent()?.tags || {}; + const newTags = ev.getContent()?.tags || {}; if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) { this.onRoomUpdate(room); } From 3adb2635bac73810ae27eaf8bfbd586ab0ceb725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 15:40:32 +0200 Subject: [PATCH 128/330] Revert "Remove unnecessary check" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 08c0f0a67ef2068b64f0028279be7a53d4d82a50. Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 290caba9d4..e9be7c1e5e 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -514,7 +514,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const room = this.matrixClient?.getRoom(payload.room_id); // persist last viewed room from a space - if (room) { + if (room && !room.isSpaceRoom()) { window.localStorage.setItem(getLastViewedRoomsStorageKey(this.activeSpace), payload.room_id); } From 9b81f5b4a034c3f791f7083bb0607e4ca7b9ab10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Apr 2021 16:11:34 +0200 Subject: [PATCH 129/330] Add a comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index e9be7c1e5e..dc0c691505 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -514,6 +514,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const room = this.matrixClient?.getRoom(payload.room_id); // persist last viewed room from a space + + // Don't save if the room is a space room. This would cause a problem: + // When switching to a space home, we first view that room and + // only after that we switch to that space. This causes us to + // save the space home to be the last viewed room in the home + // space. if (room && !room.isSpaceRoom()) { window.localStorage.setItem(getLastViewedRoomsStorageKey(this.activeSpace), payload.room_id); } From b519d851277c2d87ee8b3cd278f89a21927f47b6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 20 Apr 2021 09:32:12 -0600 Subject: [PATCH 130/330] Update src/voice/RecorderWorklet.ts to use sanity Co-authored-by: Germain --- src/voice/RecorderWorklet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/voice/RecorderWorklet.ts b/src/voice/RecorderWorklet.ts index eab6bc5f21..48387fc06e 100644 --- a/src/voice/RecorderWorklet.ts +++ b/src/voice/RecorderWorklet.ts @@ -44,8 +44,8 @@ class MxVoiceWorklet extends AudioWorkletProcessor { // waveform. // // We translate the amplitude down to 0-1 for sanity's sake. - const minVal = monoChan.reduce((m, v) => Math.min(m, v), Number.MAX_SAFE_INTEGER); - const maxVal = monoChan.reduce((m, v) => Math.max(m, v), Number.MIN_SAFE_INTEGER); + const minVal = Math.min(...monoChan); + const maxVal = Math.max(...monoChan); const amplitude = percentageOf(maxVal, -1, 1) - percentageOf(minVal, -1, 1); this.port.postMessage({ From 1507f64f2bf954260460ee6ac518dc13537b5dc4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 21 Apr 2021 08:52:56 +0100 Subject: [PATCH 131/330] Fix spaces filtering sometimes lagging behind or behaving oddly --- src/stores/room-list/filters/SpaceFilterCondition.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index ad0ab88868..43bdcb3879 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -42,10 +42,16 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi private onStoreUpdate = async (): Promise => { const beforeRoomIds = this.roomIds; - this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space); + // clone the set as it may be mutated by the space store internally + this.roomIds = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(this.space)); if (setHasDiff(beforeRoomIds, this.roomIds)) { this.emit(FILTER_CHANGED); + // XXX: Room List Store has a bug where updates to the pre-filter during a local echo of a + // tags transition seem to be ignored, so refire in the next tick to work around it + setImmediate(() => { + this.emit(FILTER_CHANGED); + }); } }; From ecd9b8d6deb03bd1b9ed2e6863e2bdc5b82f0b4b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 21 Apr 2021 09:01:22 +0100 Subject: [PATCH 132/330] Fix issue with spaces context switching looping and breaking --- src/stores/SpaceStore.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 290caba9d4..6ec60600a0 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -124,11 +124,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient { defaultDispatcher.dispatch({ action: "view_room", room_id: roomId, + context_switch: true, }); } else if (space) { defaultDispatcher.dispatch({ action: "view_room", room_id: space.roomId, + context_switch: true, }); } else { defaultDispatcher.dispatch({ @@ -513,6 +515,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { case "view_room": { const room = this.matrixClient?.getRoom(payload.room_id); + // Don't auto-switch rooms when reacting to a context-switch + // as this is not helpful and can create loops of rooms/space switching + if (payload.context_switch) break; + // persist last viewed room from a space if (room) { window.localStorage.setItem(getLastViewedRoomsStorageKey(this.activeSpace), payload.room_id); From c5a1bb2d2ce91b416e504ceb37a8e2a2a9f4c766 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 21 Apr 2021 10:44:20 +0100 Subject: [PATCH 133/330] fix sticky tags header in room list --- res/css/views/rooms/_RoomSublist.scss | 4 +++- src/components/structures/LeftPanel.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index a0a40c0239..9d52e40819 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -41,7 +41,9 @@ limitations under the License. // The combined height must be set in the LeftPanel component for sticky headers // to work correctly. padding-bottom: 8px; - height: 24px; + // Allow the container to collapse on itself if its children + // are not in the normal document flow + max-height: 24px; color: $roomlist-header-color; .mx_RoomSublist_stickable { diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index cbfc7b476b..e4762e35ad 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -154,7 +154,7 @@ export default class LeftPanel extends React.Component { private doStickyHeaders(list: HTMLDivElement) { const topEdge = list.scrollTop; const bottomEdge = list.offsetHeight + list.scrollTop; - const sublists = list.querySelectorAll(".mx_RoomSublist"); + const sublists = list.querySelectorAll(".mx_RoomSublist:not(.mx_RoomSublist_hidden)"); const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles const headerStickyWidth = list.clientWidth - headerRightMargin; From 8e0b514dc64c5411a015961a4d59a730da727f55 Mon Sep 17 00:00:00 2001 From: Andrejs Date: Tue, 20 Apr 2021 10:54:53 +0000 Subject: [PATCH 134/330] Translated using Weblate (Latvian) Currently translated at 50.9% (1485 of 2916 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/lv/ --- src/i18n/strings/lv.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index da167399d4..ed7da7dc6b 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -300,7 +300,7 @@ "You need to be logged in.": "Tev ir jāpierakstās.", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Jūsu epasta adrese nav piesaistīta nevienam Matrix ID šajā bāzes serverī.", "You seem to be in a call, are you sure you want to quit?": "Izskatās, ka atrodies zvana režīmā. Vai tiešām vēlies iziet?", - "You seem to be uploading files, are you sure you want to quit?": "Izskatās, ka šobrīd augšuplādē failus. Vai tiešām vēlies iziet?", + "You seem to be uploading files, are you sure you want to quit?": "Izskatās, ka šobrīd notiek failu augšupielāde. Vai tiešām vēlaties iziet?", "Sun": "Sv.", "Mon": "P.", "Tue": "O.", @@ -747,7 +747,7 @@ "Unhide Preview": "Rādīt priekšskatījumu", "Unable to join network": "Neizdodas pievienoties tīklam", "Sorry, your browser is not able to run %(brand)s.": "Atvaino, diemžēl tavs tīmekļa pārlūks nespēj darbināt %(brand)s.", - "Uploaded on %(date)s by %(user)s": "Augšuplādēja %(user)s %(date)s", + "Uploaded on %(date)s by %(user)s": "Augšupielādēja %(user)s %(date)s", "Messages in group chats": "Ziņas grupas čatos", "Yesterday": "Vakardien", "Error encountered (%(errorDetail)s).": "Gadījās kļūda (%(errorDetail)s).", @@ -1578,5 +1578,8 @@ "Send a reply…": "Nosūtīt atbildi…", "Room version": "Istabas versija", "Room list": "Istabu saraksts", - "Failed to set topic": "Neizdevās iestatīt tematu" + "Failed to set topic": "Neizdevās iestatīt tematu", + "Upload files": "Failu augšupielāde", + "These files are too large to upload. The file size limit is %(limit)s.": "Šie faili pārsniedz augšupielādes izmēra limitu %(limit)s.", + "Upload files (%(current)s of %(total)s)": "Failu augšupielāde (%(current)s no %(total)s)" } From 40ed8fd3420b814790ba496528ddd666d9c1cb52 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 21 Apr 2021 16:47:29 +0100 Subject: [PATCH 135/330] Upgrade matrix-js-sdk to 10.0.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7c190c68bf..cb9f4e80c5 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "10.0.0-rc.1", "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 66329cfa89..845a7b8a34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5587,9 +5587,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "9.11.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e277de6e3d9bbb98fbfbbedd47d86ee85f6f47e5" +matrix-js-sdk@10.0.0-rc.1: + version "10.0.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-10.0.0-rc.1.tgz#e99ff19fa02ad6526cd62a20767104591b4e0720" + integrity sha512-3dwM9BFFAW1RC55+XHUpSfV4lQmyrx8peLW+3p+uIbZNgtPV/+h2X0ja281SVipdePJ50gYF9Iif+UkLkXXuug== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From b6b321f90cb11eabb144147c45c0e25c596591cd Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 21 Apr 2021 16:53:33 +0100 Subject: [PATCH 136/330] Prepare changelog for v3.19.0-rc.1 --- CHANGELOG.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec73756ff9..0158e305bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,107 @@ +Changes in [3.19.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0-rc.1) (2021-04-21) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0...v3.19.0-rc.1) + + * Upgrade to JS SDK 10.0.0-rc.1 + * Translations update from Weblate + [\#5896](https://github.com/matrix-org/matrix-react-sdk/pull/5896) + * Fix sticky tags header in room list + [\#5895](https://github.com/matrix-org/matrix-react-sdk/pull/5895) + * Fix spaces filtering sometimes lagging behind or behaving oddly + [\#5893](https://github.com/matrix-org/matrix-react-sdk/pull/5893) + * Fix issue with spaces context switching looping and breaking + [\#5894](https://github.com/matrix-org/matrix-react-sdk/pull/5894) + * Improve RoomList render time when filtering + [\#5874](https://github.com/matrix-org/matrix-react-sdk/pull/5874) + * Avoid being stuck in a space + [\#5891](https://github.com/matrix-org/matrix-react-sdk/pull/5891) + * [Spaces] Context switching + [\#5795](https://github.com/matrix-org/matrix-react-sdk/pull/5795) + * Warn when you attempt to leave room that you are the only member of + [\#5415](https://github.com/matrix-org/matrix-react-sdk/pull/5415) + * Ensure PersistedElement are unmounted on application logout + [\#5884](https://github.com/matrix-org/matrix-react-sdk/pull/5884) + * Add missing space in seshat dialog and the corresponding string + [\#5866](https://github.com/matrix-org/matrix-react-sdk/pull/5866) + * A tiny change to make the Add existing rooms dialog a little nicer + [\#5885](https://github.com/matrix-org/matrix-react-sdk/pull/5885) + * Remove weird margin from the file panel + [\#5889](https://github.com/matrix-org/matrix-react-sdk/pull/5889) + * Trigger lazy loading when filtering using spaces + [\#5882](https://github.com/matrix-org/matrix-react-sdk/pull/5882) + * Fix typo in method call in add existing to space dialog + [\#5883](https://github.com/matrix-org/matrix-react-sdk/pull/5883) + * New Image View fixes/improvements + [\#5872](https://github.com/matrix-org/matrix-react-sdk/pull/5872) + * Limit voice recording length + [\#5871](https://github.com/matrix-org/matrix-react-sdk/pull/5871) + * Clean up add existing to space dialog and include DMs in it too + [\#5881](https://github.com/matrix-org/matrix-react-sdk/pull/5881) + * Fix unknown slash command error exploding + [\#5853](https://github.com/matrix-org/matrix-react-sdk/pull/5853) + * Switch to a spec conforming email validation Regexp + [\#5852](https://github.com/matrix-org/matrix-react-sdk/pull/5852) + * Cleanup unused state in MessageComposer + [\#5877](https://github.com/matrix-org/matrix-react-sdk/pull/5877) + * Pulse animation for voice messages recording state + [\#5869](https://github.com/matrix-org/matrix-react-sdk/pull/5869) + * Don't include invisible rooms in notify summary + [\#5875](https://github.com/matrix-org/matrix-react-sdk/pull/5875) + * Properly disable composer access when recording a voice message + [\#5870](https://github.com/matrix-org/matrix-react-sdk/pull/5870) + * Stabilise starting a DM with multiple people flow + [\#5862](https://github.com/matrix-org/matrix-react-sdk/pull/5862) + * Render msgOption only if showReadReceipts is enabled + [\#5864](https://github.com/matrix-org/matrix-react-sdk/pull/5864) + * Labs: Add quick/cheap "do not disturb" flag + [\#5873](https://github.com/matrix-org/matrix-react-sdk/pull/5873) + * Fix ReadReceipts animations + [\#5836](https://github.com/matrix-org/matrix-react-sdk/pull/5836) + * Add tooltips to message previews + [\#5859](https://github.com/matrix-org/matrix-react-sdk/pull/5859) + * IRC Layout fix layout spacing in replies + [\#5855](https://github.com/matrix-org/matrix-react-sdk/pull/5855) + * Move user to welcome_page if continuing with previous session + [\#5849](https://github.com/matrix-org/matrix-react-sdk/pull/5849) + * Improve image view + [\#5521](https://github.com/matrix-org/matrix-react-sdk/pull/5521) + * Add a button to reset personal encryption state during login + [\#5819](https://github.com/matrix-org/matrix-react-sdk/pull/5819) + * Fix js-sdk import in SlashCommands + [\#5850](https://github.com/matrix-org/matrix-react-sdk/pull/5850) + * Fix useRoomPowerLevels hook + [\#5854](https://github.com/matrix-org/matrix-react-sdk/pull/5854) + * Prevent state events being rendered with invalid state keys + [\#5851](https://github.com/matrix-org/matrix-react-sdk/pull/5851) + * Give server ACLs a name in 'roles & permissions' tab + [\#5838](https://github.com/matrix-org/matrix-react-sdk/pull/5838) + * Don't hide notification badge on the home space button as it has no menu + [\#5845](https://github.com/matrix-org/matrix-react-sdk/pull/5845) + * User Info hide disambiguation as we always show MXID anyway + [\#5843](https://github.com/matrix-org/matrix-react-sdk/pull/5843) + * Improve kick state to not show if the target was not joined to begin with + [\#5846](https://github.com/matrix-org/matrix-react-sdk/pull/5846) + * Fix space store wrongly switching to a non-space filter + [\#5844](https://github.com/matrix-org/matrix-react-sdk/pull/5844) + * Tweak appearance of invite reason + [\#5847](https://github.com/matrix-org/matrix-react-sdk/pull/5847) + * Update Inter font to v3.18 + [\#5840](https://github.com/matrix-org/matrix-react-sdk/pull/5840) + * Enable sharing historical keys on invite + [\#5839](https://github.com/matrix-org/matrix-react-sdk/pull/5839) + * Add ability to hide post-login encryption setup with customisation point + [\#5834](https://github.com/matrix-org/matrix-react-sdk/pull/5834) + * Use LaTeX and TeX delimiters by default + [\#5515](https://github.com/matrix-org/matrix-react-sdk/pull/5515) + * Clone author's deps fork for Netlify previews + [\#5837](https://github.com/matrix-org/matrix-react-sdk/pull/5837) + * Show drop file UI only if dragging a file + [\#5827](https://github.com/matrix-org/matrix-react-sdk/pull/5827) + * Ignore punctuation when filtering rooms + [\#5824](https://github.com/matrix-org/matrix-react-sdk/pull/5824) + * Resizable CallView + [\#5710](https://github.com/matrix-org/matrix-react-sdk/pull/5710) + Changes in [3.18.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.18.0) (2021-04-12) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0-rc.1...v3.18.0) From 037b433519c563c9e0da728740b5b982bfcebfc8 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 21 Apr 2021 16:53:34 +0100 Subject: [PATCH 137/330] v3.19.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index cb9f4e80c5..dc9a9057d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.18.0", + "version": "3.19.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -27,7 +27,7 @@ "matrix-gen-i18n": "scripts/gen-i18n.js", "matrix-prune-i18n": "scripts/prune-i18n.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -190,5 +190,6 @@ "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" ] - } + }, + "typings": "./lib/index.d.ts" } From 91b3688feb160e4a309a02f06890645e1f56dda4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Apr 2021 13:49:58 -0600 Subject: [PATCH 138/330] Redesign "failed to send messages" status bar --- res/css/structures/_RoomStatusBar.scss | 97 +++++++++++++- res/img/element-icons/retry.svg | 3 + res/img/element-icons/trashcan.svg | 3 + src/Resend.js | 10 +- src/components/structures/RoomStatusBar.js | 124 +++++++++--------- .../views/rooms/NotificationBadge.tsx | 2 +- src/i18n/strings/en_EN.json | 10 +- src/utils/ErrorUtils.js | 6 - 8 files changed, 177 insertions(+), 78 deletions(-) create mode 100644 res/img/element-icons/retry.svg create mode 100644 res/img/element-icons/trashcan.svg diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index 5bf2aee3ae..db8e5a10ba 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_RoomStatusBar { +.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { margin-left: 65px; min-height: 50px; } @@ -68,6 +68,99 @@ limitations under the License. min-height: 58px; } +.mx_RoomStatusBar_unsentMessages { + > div[role="alert"] { + // cheat some basic alignment + display: flex; + align-items: center; + min-height: 70px; + margin: 12px; + padding-left: 16px; + background-color: $header-panel-bg-color; + border-radius: 4px; + } + + .mx_RoomStatusBar_unsentBadge { + margin-right: 12px; + + .mx_NotificationBadge { + // Override sizing from the default badge + width: 24px !important; + height: 24px !important; + border-radius: 24px !important; + + .mx_NotificationBadge_count { + font-size: $font-16px !important; // override default + } + } + } + + .mx_RoomStatusBar_unsentTitle { + color: $warning-color; + font-size: $font-15px; + } + + .mx_RoomStatusBar_unsentDescription { + font-size: $font-12px; + } + + .mx_RoomStatusBar_unsentButtonBar { + flex-grow: 1; + text-align: right; + margin-right: 22px; + color: $muted-fg-color; + + .mx_AccessibleButton { + padding: 5px 10px; + padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding + display: inline-block; + position: relative; + + &:nth-child(2) { + border-left: 1px solid $input-darker-bg-color; + } + + &::before { + content: ''; + position: absolute; + left: 10px; // inset for regular button padding + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + &.mx_RoomStatusBar_unsentCancelAllBtn::before { + mask-image: url('$(res)/img/element-icons/trashcan.svg'); + width: 12px; + height: 16px; + top: calc(50% - 8px); // text sizes are dynamic + } + + &.mx_RoomStatusBar_unsentResendAllBtn { + padding-left: 34px; // 28px from above, but +6px to account for the wider icon + + &::before { + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + top: calc(50% - 9px); // text sizes are dynamic + } + } + } + + .mx_InlineSpinner { + vertical-align: middle; + margin-right: 5px; + top: 1px; // just to help the vertical alignment be slightly better + + & + span { + margin-right: 10px; // same margin/padding as the rightmost button + } + } + } +} + .mx_RoomStatusBar_connectionLostBar img { padding-left: 10px; padding-right: 10px; @@ -103,7 +196,7 @@ limitations under the License. } .mx_MatrixChat_useCompactLayout { - .mx_RoomStatusBar { + .mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { min-height: 40px; } diff --git a/res/img/element-icons/retry.svg b/res/img/element-icons/retry.svg new file mode 100644 index 0000000000..09448d6458 --- /dev/null +++ b/res/img/element-icons/retry.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/trashcan.svg b/res/img/element-icons/trashcan.svg new file mode 100644 index 0000000000..f8fb8b5c46 --- /dev/null +++ b/res/img/element-icons/trashcan.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Resend.js b/src/Resend.js index bf69e59c1a..f1e5fb38f5 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -21,11 +21,11 @@ import { EventStatus } from 'matrix-js-sdk/src/models/event'; export default class Resend { static resendUnsentEvents(room) { - room.getPendingEvents().filter(function(ev) { + return Promise.all(room.getPendingEvents().filter(function(ev) { return ev.status === EventStatus.NOT_SENT; - }).forEach(function(event) { - Resend.resend(event); - }); + }).map(function(event) { + return Resend.resend(event); + })); } static cancelUnsentEvents(room) { @@ -38,7 +38,7 @@ export default class Resend { static resend(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).then(function(res) { + return MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 54b6fee233..deffd6b95b 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -1,5 +1,5 @@ /* -Copyright 2015-2020 The Matrix.org Foundation C.I.C. +Copyright 2015-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. @@ -20,10 +20,15 @@ import { _t, _td } from '../../languageHandler'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import Resend from '../../Resend'; import dis from '../../dispatcher/dispatcher'; -import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; +import {messageForResourceLimitError} from '../../utils/ErrorUtils'; import {Action} from "../../dispatcher/actions"; import {replaceableComponent} from "../../utils/replaceableComponent"; import {EventStatus} from "matrix-js-sdk/src/models/event"; +import NotificationBadge from "../views/rooms/NotificationBadge"; +import {NotificationColor} from "../../stores/notifications/NotificationColor"; +import {StaticNotificationState} from "../../stores/notifications/StaticNotificationState"; +import AccessibleButton from "../views/elements/AccessibleButton"; +import InlineSpinner from "../views/elements/InlineSpinner"; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; @@ -76,6 +81,7 @@ export default class RoomStatusBar extends React.Component { syncState: MatrixClientPeg.get().getSyncState(), syncStateData: MatrixClientPeg.get().getSyncStateData(), unsentMessages: getUnsentMessages(this.props.room), + isResending: false, }; componentDidMount() { @@ -109,7 +115,10 @@ export default class RoomStatusBar extends React.Component { }; _onResendAllClick = () => { - Resend.resendUnsentEvents(this.props.room); + Resend.resendUnsentEvents(this.props.room).then(() => { + this.setState({isResending: false}); + }); + this.setState({isResending: true}); dis.fire(Action.FocusComposer); }; @@ -120,10 +129,7 @@ export default class RoomStatusBar extends React.Component { _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => { if (room.roomId !== this.props.room.roomId) return; - - this.setState({ - unsentMessages: getUnsentMessages(this.props.room), - }); + this.setState({unsentMessages: getUnsentMessages(this.props.room)}); }; // Check whether current size is greater than 0, if yes call props.onVisible @@ -141,7 +147,7 @@ export default class RoomStatusBar extends React.Component { _getSize() { if (this._shouldShowConnectionError()) { return STATUS_BAR_EXPANDED; - } else if (this.state.unsentMessages.length > 0) { + } else if (this.state.unsentMessages.length > 0 || this.state.isResending) { return STATUS_BAR_EXPANDED_LARGE; } return STATUS_BAR_HIDDEN; @@ -162,7 +168,6 @@ export default class RoomStatusBar extends React.Component { _getUnsentMessageContent() { const unsentMessages = this.state.unsentMessages; - if (!unsentMessages.length) return null; let title; @@ -206,75 +211,76 @@ export default class RoomStatusBar extends React.Component { "Please contact your service administrator to continue using the service.", ), }); - } else if ( - unsentMessages.length === 1 && - unsentMessages[0].error && - unsentMessages[0].error.data && - unsentMessages[0].error.data.error - ) { - title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error; } else { - title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); + title = _t('Some of your messages have not been sent'); } - const content = _t("%(count)s Resend all or cancel all " + - "now. You can also select individual messages to resend or cancel.", - { count: unsentMessages.length }, - { - 'resendText': (sub) => - { sub }, - 'cancelText': (sub) => - { sub }, - }, - ); + let buttonRow = <> + + {_t("Delete all")} + + + {_t("Retry all")} + + ; + if (this.state.isResending) { + buttonRow = <> + + {/* span for css */} + {_t("Sending")} + ; + } - return
- -
-
- { title } -
-
- { content } + return <> +
+
+
+ +
+
+
+ { title } +
+
+ { _t("You can select all or individual messages to retry or delete") } +
+
+
+ {buttonRow} +
-
; + ; } - // return suitable content for the main (text) part of the status bar. - _getContent() { + render() { if (this._shouldShowConnectionError()) { return ( -
- /!\ -
-
- { _t('Connectivity to the server has been lost.') } -
-
- { _t('Sent messages will be stored until your connection has returned.') } +
+
+
+ /!\ +
+
+ {_t('Connectivity to the server has been lost.')} +
+
+ {_t('Sent messages will be stored until your connection has returned.')} +
+
); } - if (this.state.unsentMessages.length > 0) { + if (this.state.unsentMessages.length > 0 || this.state.isResending) { return this._getUnsentMessageContent(); } return null; } - - render() { - const content = this._getContent(); - - return ( -
-
- { content } -
-
- ); - } } diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 36a52e260d..4b843bfc29 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -30,7 +30,7 @@ interface IProps { * If true, the badge will show a count if at all possible. This is typically * used to override the user's preference for things like room sublists. */ - forceCount: boolean; + forceCount?: boolean; /** * The room ID, if any, the badge represents. diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 133d24e3c8..0b525670a8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -658,7 +658,6 @@ "No homeserver URL provided": "No homeserver URL provided", "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", "Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration", - "The message you are trying to send is too large.": "The message you are trying to send is too large.", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has been blocked by its administrator.": "This homeserver has been blocked by its administrator.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", @@ -2611,10 +2610,11 @@ "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has been blocked by it's administrator. Please contact your service administrator to continue using the service.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", - "%(count)s of your messages have not been sent.|other": "Some of your messages have not been sent.", - "%(count)s of your messages have not been sent.|one": "Your message was not sent.", - "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|other": "Resend all or cancel all now. You can also select individual messages to resend or cancel.", - "%(count)s Resend all or cancel all now. You can also select individual messages to resend or cancel.|one": "Resend message or cancel message now.", + "Some of your messages have not been sent": "Some of your messages have not been sent", + "Delete all": "Delete all", + "Retry all": "Retry all", + "Sending": "Sending", + "You can select all or individual messages to retry or delete": "You can select all or individual messages to retry or delete", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", diff --git a/src/utils/ErrorUtils.js b/src/utils/ErrorUtils.js index 2c6acd5503..b5bd5b0af0 100644 --- a/src/utils/ErrorUtils.js +++ b/src/utils/ErrorUtils.js @@ -49,12 +49,6 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e } } -export function messageForSendError(errorData) { - if (errorData.errcode === "M_TOO_LARGE") { - return _t("The message you are trying to send is too large."); - } -} - export function messageForSyncError(err) { if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const limitError = messageForResourceLimitError( From c5dd6b4dfb95eab8f274875556a6e1d38a3a323d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Apr 2021 16:16:05 -0600 Subject: [PATCH 139/330] Update action bar to incorporate sending states This moves most of them out of the context menu. --- res/css/views/messages/_MessageActionBar.scss | 8 ++ res/css/views/rooms/_EventTile.scss | 4 - .../views/context_menus/MessageContextMenu.js | 91 +------------ .../views/messages/EditHistoryMessage.js | 1 - .../views/messages/MessageActionBar.js | 125 ++++++++++++++---- src/components/views/rooms/EventTile.js | 17 ++- src/i18n/strings/en_EN.json | 8 +- 7 files changed, 128 insertions(+), 126 deletions(-) diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 1254b496b5..3ecbef0d1f 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -105,3 +105,11 @@ limitations under the License. .mx_MessageActionBar_optionsButton::after { mask-image: url('$(res)/img/element-icons/context-menu.svg'); } + +.mx_MessageActionBar_resendButton::after { + mask-image: url('$(res)/img/element-icons/retry.svg'); +} + +.mx_MessageActionBar_cancelButton::after { + mask-image: url('$(res)/img/element-icons/trashcan.svg'); +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 2b3e179c54..5d1dd04383 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -214,10 +214,6 @@ $left-gutter: 64px; color: $accent-fg-color; } -.mx_EventTile_notSent { - color: $event-notsent-color; -} - .mx_EventTile_receiptSent, .mx_EventTile_receiptSending { // We don't use `position: relative` on the element because then it won't line diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index f86cd26f32..142b8c80a8 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -1,8 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2018, 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. @@ -34,7 +32,7 @@ import {MenuItem} from "../../structures/ContextMenu"; import {EventType} from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -function canCancel(eventStatus) { +export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } @@ -98,21 +96,6 @@ export default class MessageContextMenu extends React.Component { return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); } - onResendClick = () => { - Resend.resend(this.props.mxEvent); - this.closeMenu(); - }; - - onResendEditClick = () => { - Resend.resend(this.props.mxEvent.replacingEvent()); - this.closeMenu(); - }; - - onResendRedactionClick = () => { - Resend.resend(this.props.mxEvent.localRedactionEvent()); - this.closeMenu(); - }; - onResendReactionsClick = () => { for (const reaction of this._getUnsentReactions()) { Resend.resend(reaction); @@ -170,29 +153,6 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onCancelSendClick = () => { - const mxEvent = this.props.mxEvent; - const editEvent = mxEvent.replacingEvent(); - const redactEvent = mxEvent.localRedactionEvent(); - const pendingReactions = this._getPendingReactions(); - - if (editEvent && canCancel(editEvent.status)) { - Resend.removeFromQueue(editEvent); - } - if (redactEvent && canCancel(redactEvent.status)) { - Resend.removeFromQueue(redactEvent); - } - if (pendingReactions.length) { - for (const reaction of pendingReactions) { - Resend.removeFromQueue(reaction); - } - } - if (canCancel(mxEvent.status)) { - Resend.removeFromQueue(this.props.mxEvent); - } - this.closeMenu(); - }; - onForwardClick = () => { if (this.props.onCloseDialog) this.props.onCloseDialog(); dis.dispatch({ @@ -285,20 +245,9 @@ export default class MessageContextMenu extends React.Component { const me = cli.getUserId(); const mxEvent = this.props.mxEvent; const eventStatus = mxEvent.status; - const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status; - const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status; const unsentReactionsCount = this._getUnsentReactions().length; - const pendingReactionsCount = this._getPendingReactions().length; - const allowCancel = canCancel(mxEvent.status) || - canCancel(editStatus) || - canCancel(redactStatus) || - pendingReactionsCount !== 0; - let resendButton; - let resendEditButton; let resendReactionsButton; - let resendRedactionButton; let redactButton; - let cancelButton; let forwardButton; let pinButton; let unhidePreviewButton; @@ -309,22 +258,6 @@ export default class MessageContextMenu extends React.Component { // status is SENT before remote-echo, null after const isSent = !eventStatus || eventStatus === EventStatus.SENT; if (!mxEvent.isRedacted()) { - if (eventStatus === EventStatus.NOT_SENT) { - resendButton = ( - - { _t('Resend') } - - ); - } - - if (editStatus === EventStatus.NOT_SENT) { - resendEditButton = ( - - { _t('Resend edit') } - - ); - } - if (unsentReactionsCount !== 0) { resendReactionsButton = ( @@ -334,14 +267,6 @@ export default class MessageContextMenu extends React.Component { } } - if (redactStatus === EventStatus.NOT_SENT) { - resendRedactionButton = ( - - { _t('Resend removal') } - - ); - } - if (isSent && this.state.canRedact) { redactButton = ( @@ -350,14 +275,6 @@ export default class MessageContextMenu extends React.Component { ); } - if (allowCancel) { - cancelButton = ( - - { _t('Cancel Sending') } - - ); - } - if (isContentActionable(mxEvent)) { forwardButton = ( @@ -455,12 +372,8 @@ export default class MessageContextMenu extends React.Component { return (
- { resendButton } - { resendEditButton } { resendReactionsButton } - { resendRedactionButton } { redactButton } - { cancelButton } { forwardButton } { pinButton } { viewSourceButton } diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js index e2eda1e12a..dc4e0187d3 100644 --- a/src/components/views/messages/EditHistoryMessage.js +++ b/src/components/views/messages/EditHistoryMessage.js @@ -160,7 +160,6 @@ export default class EditHistoryMessage extends React.PureComponent { "mx_EventTile": true, // Note: we keep the `sending` state class for tests, not for our styles "mx_EventTile_sending": isSending, - "mx_EventTile_notSent": this.state.sendStatus === 'not_sent', }); return (
  • diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 5a6e7d87b7..b2f7f8a692 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -29,6 +29,8 @@ import RoomContext from "../../../contexts/RoomContext"; import Toolbar from "../../../accessibility/Toolbar"; import {RovingAccessibleTooltipButton, useRovingTabIndex} from "../../../accessibility/RovingTabIndex"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {canCancel} from "../context_menus/MessageContextMenu"; +import Resend from "../../../Resend"; const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -169,45 +171,118 @@ export default class MessageActionBar extends React.PureComponent { }); }; - render() { - let reactButton; - let replyButton; - let editButton; + /** + * Runs a given fn on the set of possible events to test. The first event + * that passes the checkFn will have fn executed on it. Both functions take + * a MatrixEvent object. If no particular conditions are needed, checkFn can + * be null/undefined. If no functions pass the checkFn, no action will be + * taken. + * @param {Function} fn The execution function. + * @param {Function} checkFn The test function. + */ + runActionOnFailedEv(fn, checkFn) { + if (!checkFn) checkFn = () => true; - if (isContentActionable(this.props.mxEvent)) { - if (this.context.canReact) { - reactButton = ( - - ); - } - if (this.context.canReply) { - replyButton = ; + const mxEvent = this.props.mxEvent; + const editEvent = mxEvent.replacingEvent(); + const redactEvent = mxEvent.localRedactionEvent(); + const tryOrder = [redactEvent, editEvent, mxEvent]; + for (const ev of tryOrder) { + if (ev && checkFn(ev)) { + fn(ev); + break; } } + } + + onResendClick = (ev) => { + this.runActionOnFailedEv((tarEv) => Resend.resend(tarEv)); + }; + + onCancelClick = (ev) => { + this.runActionOnFailedEv( + (tarEv) => Resend.removeFromQueue(tarEv), + (testEv) => canCancel(testEv.status), + ); + }; + + render() { + const toolbarOpts = []; if (canEditContent(this.props.mxEvent)) { - editButton = ; + key="edit" + />); } - // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive. - return - {reactButton} - {replyButton} - {editButton} - ; + + // We show a different toolbar for failed events, so detect that first. + const mxEvent = this.props.mxEvent; + const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status; + const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status; + const allowCancel = canCancel(mxEvent.status) || canCancel(editStatus) || canCancel(redactStatus); + const isFailed = [mxEvent.status, editStatus, redactStatus].includes("not_sent"); + if (allowCancel && isFailed) { + // The resend button needs to appear ahead of the edit button, so insert to the + // start of the opts + toolbarOpts.splice(0, 0, ); + + // The delete button should appear last, so we can just drop it at the end + toolbarOpts.push(cancelSendingButton); + } else { + if (isContentActionable(this.props.mxEvent)) { + // Like the resend button, the react and reply buttons need to appear before the edit. + // The only catch is we do the reply button first so that we can make sure the react + // button is the very first button without having to do length checks for `splice()`. + if (this.context.canReply) { + toolbarOpts.splice(0, 0, ); + } + if (this.context.canReact) { + toolbarOpts.splice(0, 0, ); + } + } + + if (allowCancel) { + toolbarOpts.push(cancelSendingButton); + } + + // The menu button should be last, so dump it there. + toolbarOpts.push( + key="menu" + />); + } + + // aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive. + return + {toolbarOpts} ; } } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f6fb83c064..05afb742a4 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -40,6 +40,9 @@ import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStor import {objectHasDiff} from "../../../utils/objects"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import Tooltip from "../elements/Tooltip"; +import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; +import {NotificationColor} from "../../../stores/notifications/NotificationColor"; +import NotificationBadge from "./NotificationBadge"; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -838,7 +841,6 @@ export default class EventTile extends React.Component { mx_EventTile_12hr: this.props.isTwelveHour, // Note: we keep the `sending` state class for tests, not for our styles mx_EventTile_sending: !isEditing && isSending, - mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent', mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(), mx_EventTile_selected: this.props.isSelectedEvent, mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation, @@ -1253,11 +1255,19 @@ class SentReceipt extends React.PureComponent; + } + let tooltip = null; if (this.state.hover) { let label = _t("Sending your message..."); @@ -1265,6 +1275,8 @@ class SentReceipt extends React.PureComponent + {nonCssBadge} {tooltip} ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0b525670a8..7af06aa5b1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1452,6 +1452,7 @@ "Sending your message...": "Sending your message...", "Encrypting your message...": "Encrypting your message...", "Your message was sent": "Your message was sent", + "Failed to send": "Failed to send", "Please select the destination room for this message": "Please select the destination room for this message", "Scroll to most recent messages": "Scroll to most recent messages", "Close preview": "Close preview", @@ -1810,8 +1811,9 @@ "The encryption used by this room isn't supported.": "The encryption used by this room isn't supported.", "Error decrypting audio": "Error decrypting audio", "React": "React", - "Reply": "Reply", "Edit": "Edit", + "Retry": "Retry", + "Reply": "Reply", "Message Actions": "Message Actions", "Attachment": "Attachment", "Error decrypting attachment": "Error decrypting attachment", @@ -2390,7 +2392,6 @@ "Confirm encryption setup": "Confirm encryption setup", "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", "Unable to set up keys": "Unable to set up keys", - "Retry": "Retry", "Restoring keys from backup": "Restoring keys from backup", "Fetching keys from server...": "Fetching keys from server...", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", @@ -2419,10 +2420,7 @@ "Reject invitation": "Reject invitation", "Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?", "Unable to reject invite": "Unable to reject invite", - "Resend edit": "Resend edit", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", - "Resend removal": "Resend removal", - "Cancel Sending": "Cancel Sending", "Forward Message": "Forward Message", "Pin Message": "Pin Message", "Unhide Preview": "Unhide Preview", From cc095c85bfb255ecf45d92ca0baff13d4a797aee Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Apr 2021 16:22:27 -0600 Subject: [PATCH 140/330] Dark theme support --- res/css/structures/_RoomStatusBar.scss | 2 +- res/themes/dark/css/_dark.scss | 2 ++ res/themes/legacy-dark/css/_legacy-dark.scss | 2 ++ res/themes/legacy-light/css/_legacy-light.scss | 2 ++ res/themes/light/css/_light.scss | 2 ++ 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index db8e5a10ba..8cc00aba0f 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -117,7 +117,7 @@ limitations under the License. position: relative; &:nth-child(2) { - border-left: 1px solid $input-darker-bg-color; + border-left: 1px solid $resend-button-divider-color; } &::before { diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index bd7057c3e4..11e6b0202a 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -63,6 +63,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: $bg-color; +$resend-button-divider-color: $muted-fg-color; + // scrollbars $scrollbar-thumb-color: rgba(255, 255, 255, 0.2); $scrollbar-track-color: transparent; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 9b2365a621..adab405fa2 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -61,6 +61,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: $bg-color; +$resend-button-divider-color: $muted-fg-color; + // scrollbars $scrollbar-thumb-color: rgba(255, 255, 255, 0.2); $scrollbar-track-color: transparent; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 0956f433b2..0b1f6cdf37 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -97,6 +97,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: #ffffff; +$resend-button-divider-color: $input-darker-bg-color; + $button-bg-color: $accent-color; $button-fg-color: white; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index b307dbaba3..9904d7553e 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -91,6 +91,8 @@ $field-focused-label-bg-color: #ffffff; $button-bg-color: $accent-color; $button-fg-color: white; +$resend-button-divider-color: $input-darker-bg-color; + // apart from login forms, which have stronger border $strong-input-border-color: #c7c7c7; From 9227618b425a743951d7c82df0eda7922832aac2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Apr 2021 16:36:06 -0600 Subject: [PATCH 141/330] Show indicator in Room List for unsent events --- src/components/structures/RoomStatusBar.js | 2 +- src/components/views/rooms/RoomTile.tsx | 56 ++++++++++++++++------ 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index deffd6b95b..42e8a32874 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -34,7 +34,7 @@ const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED_LARGE = 2; -function getUnsentMessages(room) { +export function getUnsentMessages(room) { if (!room) { return []; } return room.getPendingEvents().filter(function(ev) { return ev.status === EventStatus.NOT_SENT; diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index b2a07d7e06..a3207d9d65 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -1,8 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2015-2017, 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. @@ -19,6 +17,7 @@ limitations under the License. import React, { createRef } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import classNames from "classnames"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; @@ -51,7 +50,10 @@ import IconizedContextMenu, { IconizedContextMenuRadio, } from "../context_menus/IconizedContextMenu"; import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { getUnsentMessages } from "../../structures/RoomStatusBar"; +import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; +import { NotificationColor } from "../../../stores/notifications/NotificationColor"; interface IProps { room: Room; @@ -67,6 +69,7 @@ interface IState { notificationsMenuPosition: PartialDOMRect; generalMenuPosition: PartialDOMRect; messagePreview?: string; + hasUnsentEvents: boolean; } const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`; @@ -93,6 +96,7 @@ export default class RoomTile extends React.PureComponent { selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, notificationsMenuPosition: null, generalMenuPosition: null, + hasUnsentEvents: this.countUnsentEvents() > 0, // generatePreview() will return nothing if the user has previews disabled messagePreview: this.generatePreview(), @@ -101,6 +105,10 @@ export default class RoomTile extends React.PureComponent { this.roomProps = EchoChamber.forRoom(this.props.room); } + private countUnsentEvents(): number { + return getUnsentMessages(this.props.room).length; + } + private onRoomNameUpdate = (room) => { this.forceUpdate(); } @@ -109,6 +117,11 @@ export default class RoomTile extends React.PureComponent { this.forceUpdate(); // notification state changed - update }; + private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => { + if (!room?.roomId === this.props.room.roomId) return; + this.setState({hasUnsentEvents: this.countUnsentEvents() > 0}); + }; + private onRoomPropertyUpdate = (property: CachedRoomKey) => { if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate(); // else ignore - not important for this tile @@ -167,6 +180,7 @@ export default class RoomTile extends React.PureComponent { CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId), this.onCommunityUpdate, ); + MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated); } public componentWillUnmount() { @@ -191,6 +205,7 @@ export default class RoomTile extends React.PureComponent { CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId), this.onCommunityUpdate, ); + MatrixClientPeg.get()?.off("Room.localEchoUpdated", this.onLocalEchoUpdated); } private onAction = (payload: ActionPayload) => { @@ -554,17 +569,30 @@ export default class RoomTile extends React.PureComponent { />; let badge: React.ReactNode; - if (!this.props.isMinimized && this.notificationState) { + if (!this.props.isMinimized) { // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below - badge = ( - - ); + if (this.state.hasUnsentEvents) { + // hardcode the badge to a danger state when there's unsent messages + badge = ( + + ); + } else if (this.notificationState) { + badge = ( + + ); + } } let messagePreview = null; From 4be9c51dadd116f6ce53359dfdd2bc85beb34ebe Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 21 Apr 2021 16:43:25 -0600 Subject: [PATCH 142/330] Move all the RED_EXCLAMATION badges to a single definition --- src/components/structures/RoomStatusBar.js | 3 +-- src/components/views/rooms/EventTile.js | 3 +-- src/components/views/rooms/RoomList.tsx | 7 ++----- src/components/views/rooms/RoomTile.tsx | 3 +-- src/stores/notifications/StaticNotificationState.ts | 2 ++ 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 42e8a32874..ab4f524faf 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -25,7 +25,6 @@ import {Action} from "../../dispatcher/actions"; import {replaceableComponent} from "../../utils/replaceableComponent"; import {EventStatus} from "matrix-js-sdk/src/models/event"; import NotificationBadge from "../views/rooms/NotificationBadge"; -import {NotificationColor} from "../../stores/notifications/NotificationColor"; import {StaticNotificationState} from "../../stores/notifications/StaticNotificationState"; import AccessibleButton from "../views/elements/AccessibleButton"; import InlineSpinner from "../views/elements/InlineSpinner"; @@ -236,7 +235,7 @@ export default class RoomStatusBar extends React.Component {
    diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 05afb742a4..fb07d3fdff 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -41,7 +41,6 @@ import {objectHasDiff} from "../../../utils/objects"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import Tooltip from "../elements/Tooltip"; import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; -import {NotificationColor} from "../../../stores/notifications/NotificationColor"; import NotificationBadge from "./NotificationBadge"; const eventTileTypes = { @@ -1264,7 +1263,7 @@ class SentReceipt extends React.PureComponent; } diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 8ac706fc15..5b446d9f13 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -1,7 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017, 2018 Vector Creations Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2015-2018, 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. @@ -37,7 +35,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import GroupAvatar from "../avatars/GroupAvatar"; import ExtraTile from "./ExtraTile"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; -import { NotificationColor } from "../../../stores/notifications/NotificationColor"; import { Action } from "../../../dispatcher/actions"; import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; @@ -492,7 +489,7 @@ export default class RoomList extends React.PureComponent { isSelected={false} displayName={g.name} avatar={avatar} - notificationState={StaticNotificationState.forSymbol("!", NotificationColor.Red)} + notificationState={StaticNotificationState.RED_EXCLAMATION} onClick={openGroup} key={`temporaryGroupTile_${g.groupId}`} /> diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index a3207d9d65..c0b52d9c0f 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -53,7 +53,6 @@ import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/Community import { replaceableComponent } from "../../../utils/replaceableComponent"; import { getUnsentMessages } from "../../structures/RoomStatusBar"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; -import { NotificationColor } from "../../../stores/notifications/NotificationColor"; interface IProps { room: Room; @@ -576,7 +575,7 @@ export default class RoomTile extends React.PureComponent { badge = ( ; }; export default FacePile; diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js index 0bd491768c..a6fc00fc2e 100644 --- a/src/components/views/elements/TextWithTooltip.js +++ b/src/components/views/elements/TextWithTooltip.js @@ -25,6 +25,7 @@ export default class TextWithTooltip extends React.Component { class: PropTypes.string, tooltipClass: PropTypes.string, tooltip: PropTypes.node.isRequired, + tooltipProps: PropTypes.object, }; constructor() { @@ -46,15 +47,17 @@ export default class TextWithTooltip extends React.Component { render() { const Tooltip = sdk.getComponent("elements.Tooltip"); - const {class: className, children, tooltip, tooltipClass, ...props} = this.props; + const {class: className, children, tooltip, tooltipClass, tooltipProps, ...props} = this.props; return ( {children} {this.state.hover && } + className={"mx_TextWithTooltip_tooltip"} + /> } ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 133d24e3c8..f1b700540f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1916,7 +1916,13 @@ "Please create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue on GitHub so that we can investigate this bug.", "collapse": "collapse", "expand": "expand", + "View all %(count)s members|other": "View all %(count)s members", + "View all %(count)s members|one": "View 1 member", + "Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s members including %(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", + "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "Rotate Right": "Rotate Right", "Rotate Left": "Rotate Left", "Zoom out": "Zoom out", From 90cd5d0472d4bb09640d17a2e8c7bb132cc0b17c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 08:18:28 +0100 Subject: [PATCH 145/330] Remove old redundant hover effect --- res/css/structures/_SpaceRoomView.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index cb7006fb86..2dbf0fe0fe 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -224,12 +224,6 @@ $SpaceRoomViewInnerWidth: 428px; .mx_FacePile_faces { cursor: pointer; - - &:hover { - .mx_BaseAvatar { - filter: brightness(0.8); - } - } } } From ee80c27b2b65ecde0bde2db1d88f2c6f9be58237 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 08:22:11 +0100 Subject: [PATCH 146/330] Improve edge cases with spaces context switching --- src/stores/SpaceStore.tsx | 84 +++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index dc0c691505..9650eb5544 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -110,30 +110,32 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._suggestedRooms; } - public async setActiveSpace(space: Room | null) { + public async setActiveSpace(space: Room | null, contextSwitch = true) { if (space === this.activeSpace) return; this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); - // view last selected room from space - const roomId = window.localStorage.getItem(getLastViewedRoomsStorageKey(this.activeSpace)); + if (contextSwitch) { + // view last selected room from space + const roomId = window.localStorage.getItem(getLastViewedRoomsStorageKey(this.activeSpace)); - if (roomId && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join") { - defaultDispatcher.dispatch({ - action: "view_room", - room_id: roomId, - }); - } else if (space) { - defaultDispatcher.dispatch({ - action: "view_room", - room_id: space.roomId, - }); - } else { - defaultDispatcher.dispatch({ - action: "view_home_page", - }); + if (roomId && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join") { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: roomId, + }); + } else if (space) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: space.roomId, + }); + } else { + defaultDispatcher.dispatch({ + action: "view_home_page", + }); + } } // persist space selected @@ -512,36 +514,30 @@ export class SpaceStoreClass extends AsyncStoreWithClient { switch (payload.action) { case "view_room": { const room = this.matrixClient?.getRoom(payload.room_id); + if (!room) break; - // persist last viewed room from a space - - // Don't save if the room is a space room. This would cause a problem: - // When switching to a space home, we first view that room and - // only after that we switch to that space. This causes us to - // save the space home to be the last viewed room in the home - // space. - if (room && !room.isSpaceRoom()) { - window.localStorage.setItem(getLastViewedRoomsStorageKey(this.activeSpace), payload.room_id); - } - - if (room?.getMyMembership() === "join") { - if (room.isSpaceRoom()) { - this.setActiveSpace(room); - } else if (!this.spaceFilteredRooms.get(this._activeSpace?.roomId || HOME_SPACE).has(room.roomId)) { - // TODO maybe reverse these first 2 clauses once space panel active is fixed - let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId)); - if (!parent) { - parent = this.getCanonicalParent(room.roomId); - } - if (!parent) { - const parents = Array.from(this.parentMap.get(room.roomId) || []); - parent = parents.find(p => this.matrixClient.getRoom(p)); - } - if (parent) { - this.setActiveSpace(parent); - } + if (room.isSpaceRoom()) { + this.setActiveSpace(room); + } else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(room.roomId)) { + // TODO maybe reverse these first 2 clauses once space panel active is fixed + let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId)); + if (!parent) { + parent = this.getCanonicalParent(room.roomId); + } + if (!parent) { + const parents = Array.from(this.parentMap.get(room.roomId) || []); + parent = parents.find(p => this.matrixClient.getRoom(p)); + } + if (parent) { + // don't trigger a context switch when we are switching a space to match the chosen room + this.setActiveSpace(parent, false); } } + + // Persist last viewed room from a space + // we don't await setActiveSpace above as we only care about this.activeSpace being up to date + // synchronously for the below code - everything else can and should be async. + window.localStorage.setItem(getLastViewedRoomsStorageKey(this.activeSpace), payload.room_id); break; } case "after_leave_room": From ec0612f70dbef27425238127a7d7be1e9bb0e1fc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 08:30:44 +0100 Subject: [PATCH 147/330] Fix spaces notification dots wrongly including upgraded (hidden) rooms --- src/stores/SpaceStore.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 7ee6067805..55eee4586e 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -389,8 +389,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.spaceFilteredRooms.forEach((roomIds, s) => { // Update NotificationStates - const rooms = this.matrixClient.getRooms().filter(room => roomIds.has(room.roomId)); - this.getNotificationState(s)?.setRooms(rooms); + this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => roomIds.has(room.roomId))); }); }, 100, {trailing: true, leading: true}); From b64b956aa46d8c7da889489d2da4026ee88ea9cb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 08:39:16 +0100 Subject: [PATCH 148/330] when automatically switching space to match room fall back to the home space --- src/stores/SpaceStore.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 5b087bb054..daa05af7cf 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -535,10 +535,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const parents = Array.from(this.parentMap.get(room.roomId) || []); parent = parents.find(p => this.matrixClient.getRoom(p)); } - if (parent) { - // don't trigger a context switch when we are switching a space to match the chosen room - this.setActiveSpace(parent, false); - } + // don't trigger a context switch when we are switching a space to match the chosen room + this.setActiveSpace(parent || null, false); } // Persist last viewed room from a space From 28fa1cb44ce3ea0f1fe5083c88b185fd87bf96a4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 09:05:02 +0100 Subject: [PATCH 149/330] Reset space contexts as some users may have loops stuck in their local storage --- src/stores/SpaceStore.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index daa05af7cf..80722ad3ac 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -51,11 +51,7 @@ export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); const MAX_SUGGESTED_ROOMS = 20; -const getLastViewedRoomsStorageKey = (space?: Room) => { - const lastViewRooms = "mx_last_viewed_rooms"; - const homeSpace = "home_space"; - return `${lastViewRooms}_${space?.roomId || homeSpace}`; -} +const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "home_space"}`; const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] return arr.reduce((result, room: Room) => { @@ -119,7 +115,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (contextSwitch) { // view last selected room from space - const roomId = window.localStorage.getItem(getLastViewedRoomsStorageKey(this.activeSpace)); + const roomId = window.localStorage.getItem(getSpaceContextKey(this.activeSpace)); if (roomId && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join") { defaultDispatcher.dispatch({ @@ -542,7 +538,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Persist last viewed room from a space // we don't await setActiveSpace above as we only care about this.activeSpace being up to date // synchronously for the below code - everything else can and should be async. - window.localStorage.setItem(getLastViewedRoomsStorageKey(this.activeSpace), payload.room_id); + window.localStorage.setItem(getSpaceContextKey(this.activeSpace), payload.room_id); break; } case "after_leave_room": From ca07b1ed04fc1357296a7dfca83b0265544193bb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 09:06:53 +0100 Subject: [PATCH 150/330] Update res/css/views/elements/_FacePile.scss Co-authored-by: Germain --- res/css/views/elements/_FacePile.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/elements/_FacePile.scss b/res/css/views/elements/_FacePile.scss index 0f453eb3ff..c691baffb5 100644 --- a/res/css/views/elements/_FacePile.scss +++ b/res/css/views/elements/_FacePile.scss @@ -45,8 +45,8 @@ limitations under the License. position: absolute; top: 0; left: 0; - height: 30px; - width: 30px; + height: inherit; + width: inherit; background: $tertiary-fg-color; mask-position: center; mask-size: 20px; From 23c61752cdfcaebde67db358d38684225c969900 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 09:08:25 +0100 Subject: [PATCH 151/330] Add comment --- src/components/views/elements/FacePile.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/FacePile.tsx b/src/components/views/elements/FacePile.tsx index 67b218494a..aeca2e844b 100644 --- a/src/components/views/elements/FacePile.tsx +++ b/src/components/views/elements/FacePile.tsx @@ -53,6 +53,8 @@ const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, . const shownMembers = sortBy(members.filter(m => m.userId !== cli.getUserId()), iteratees).slice(0, numShown); if (shownMembers.length < 1) return null; + // We reverse the order of the shown faces in CSS to simplify their visual overlap, + // reverse members in tooltip order to make the order between the two match up. const commaSeparatedMembers = shownMembers.map(m => m.rawDisplayName).reverse().join(", "); let tooltip: ReactNode; From 60ef657f64fda8cb4a20c8331cef1eff4b9e335d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 09:41:07 +0100 Subject: [PATCH 152/330] Properly hide spaces from the room list --- src/stores/room-list/RoomListStore.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 88df05b5d0..caab46a0c2 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -599,11 +599,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { private getPlausibleRooms(): Room[] { if (!this.matrixClient) return []; - let rooms = [ - ...this.matrixClient.getVisibleRooms(), - // also show space invites in the room list - ...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"), - ].filter(r => VisibilityProvider.instance.isRoomVisible(r)); + let rooms = this.matrixClient.getVisibleRooms().filter(r => VisibilityProvider.instance.isRoomVisible(r)); if (this.prefilterConditions.length > 0) { rooms = rooms.filter(r => { From 7efd4a43a5d1a9477379f215d47e0845ed7dde7a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 11:01:49 +0100 Subject: [PATCH 153/330] Show space invites at the top of the space panel --- src/components/views/spaces/SpacePanel.tsx | 22 +++++++-- .../views/spaces/SpaceTreeLevel.tsx | 6 ++- src/stores/SpaceStore.tsx | 47 +++++++++++++------ 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index bacf1bd929..36ab423885 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -25,7 +25,12 @@ import SpaceCreateMenu from "./SpaceCreateMenu"; import {SpaceItem} from "./SpaceTreeLevel"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore"; +import SpaceStore, { + HOME_SPACE, + UPDATE_INVITED_SPACES, + UPDATE_SELECTED_SPACE, + UPDATE_TOP_LEVEL_SPACES, +} from "../../../stores/SpaceStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState"; import NotificationBadge from "../rooms/NotificationBadge"; @@ -105,19 +110,21 @@ const SpaceButton: React.FC = ({
  • ; } -const useSpaces = (): [Room[], Room | null] => { +const useSpaces = (): [Room[], Room[], Room | null] => { + const [invites, setInvites] = useState(SpaceStore.instance.invitedSpaces); + useEventEmitter(SpaceStore.instance, UPDATE_INVITED_SPACES, setInvites); const [spaces, setSpaces] = useState(SpaceStore.instance.spacePanelSpaces); useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces); const [activeSpace, setActiveSpace] = useState(SpaceStore.instance.activeSpace); useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace); - return [spaces, activeSpace]; + return [invites, spaces, activeSpace]; }; 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 [spaces, activeSpace] = useSpaces(); + const [invites, spaces, activeSpace] = useSpaces(); const [isPanelCollapsed, setPanelCollapsed] = useState(true); const newClasses = classNames("mx_SpaceButton_new", { @@ -209,6 +216,13 @@ const SpacePanel = () => { notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)} isNarrow={isPanelCollapsed} /> + { invites.map(s => setPanelCollapsed(false)} + />) } { spaces.map(s => { mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, mx_SpaceButton_narrow: isNarrow, }); - const notificationState = SpaceStore.instance.getNotificationState(space.roomId); + const notificationState = space.getMyMembership() === "invite" + ? StaticNotificationState.forSymbol("!", NotificationColor.Red) + : SpaceStore.instance.getNotificationState(space.roomId); let childItems; if (childSpaces && !collapsed) { diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 80722ad3ac..c28e24a460 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -46,6 +46,7 @@ export const HOME_SPACE = Symbol("home-space"); export const SUGGESTED_ROOMS = Symbol("suggested-rooms"); export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces"); +export const UPDATE_INVITED_SPACES = Symbol("invited-spaces"); export const UPDATE_SELECTED_SPACE = Symbol("selected-space"); // Space Room ID/HOME_SPACE will be emitted when a Space's children change @@ -93,6 +94,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // The space currently selected in the Space Panel - if null then `Home` is selected private _activeSpace?: Room = null; private _suggestedRooms: ISpaceSummaryRoom[] = []; + private _invitedSpaces = new Set(); + + public get invitedSpaces(): Room[] { + return Array.from(this._invitedSpaces); + } public get spacePanelSpaces(): Room[] { return this.rootSpaces; @@ -214,25 +220,27 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return sortBy(parents, r => r.roomId)?.[0] || null; } - public getSpaces = () => { - return this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "join"); - }; - public getSpaceFilteredRoomIds = (space: Room | null): Set => { return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); }; private rebuild = throttle(() => { - // get all most-upgraded rooms & spaces except spaces which have been left (historical) - const visibleRooms = this.matrixClient.getVisibleRooms().filter(r => { - return !r.isSpaceRoom() || r.getMyMembership() === "join"; - }); + const [visibleSpaces, visibleRooms] = partitionSpacesAndRooms(this.matrixClient.getVisibleRooms()); + const [joinedSpaces, invitedSpaces] = visibleSpaces.reduce((arr, s) => { + if (s.getMyMembership() === "join") { + arr[0].push(s); + } else if (s.getMyMembership() === "invite") { + arr[1].push(s); + } + return arr; + }, [[], []]); - const unseenChildren = new Set(visibleRooms); + // exclude invited spaces from unseenChildren as they will be forcibly shown at the top level of the treeview + const unseenChildren = new Set([...visibleRooms, ...joinedSpaces]); const backrefs = new EnhancedMap>(); // Sort spaces by room ID to force the cycle breaking to be deterministic - const spaces = sortBy(visibleRooms.filter(r => r.isSpaceRoom()), space => space.roomId); + const spaces = sortBy(joinedSpaces, space => space.roomId); // TODO handle cleaning up links when a Space is removed spaces.forEach(space => { @@ -296,6 +304,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.onRoomsUpdate(); // TODO only do this if a change has happened 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.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); }, 100, {trailing: true, leading: true}); onSpaceUpdate = () => { @@ -303,6 +315,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } private showInHomeSpace = (room: Room) => { + if (room.isSpaceRoom()) return false; return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites @@ -333,8 +346,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); - // put all invites (rooms & spaces) in the Home Space - const invites = this.matrixClient.getRooms().filter(r => r.getMyMembership() === "invite"); + // put all room invites in the Home Space + const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); visibleRooms.forEach(room => { @@ -392,8 +405,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); }, 100, {trailing: true, leading: true}); - private onRoom = (room: Room) => { - if (room?.isSpaceRoom()) { + private onRoom = (room: Room, membership?: string, oldMembership?: string) => { + if ((membership || room.getMyMembership()) === "invite") { + this._invitedSpaces.add(room); + this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); + } else if (oldMembership === "invite") { + this._invitedSpaces.delete(room); + this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); + } else if (room?.isSpaceRoom()) { this.onSpaceUpdate(); this.emit(room.roomId); } else { From a51aeaa04d10291fccb46c8b3f4099f3eeae3843 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 11:24:52 +0100 Subject: [PATCH 154/330] Disable context menu on space invite tiles as no options sensibly work --- src/components/views/spaces/SpaceTreeLevel.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 71ef4c562c..2e2901ce64 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -85,6 +85,7 @@ export class SpaceItem extends React.PureComponent { } private onContextMenu = (ev: React.MouseEvent) => { + if (this.props.space.getMyMembership() !== "join") return; ev.preventDefault(); ev.stopPropagation(); this.setState({ @@ -187,6 +188,8 @@ export class SpaceItem extends React.PureComponent { }; private renderContextMenu(): React.ReactElement { + if (this.props.space.getMyMembership() !== "join") return null; + let contextMenu = null; if (this.state.contextMenuPosition) { const userId = this.context.getUserId(); From 108a3088efe3bc30a090cc215fea77d5e8d6a52b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 11:25:11 +0100 Subject: [PATCH 155/330] Hide explore rooms quick action when active space is an invite --- src/components/views/rooms/RoomList.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 8ac706fc15..e4e638fc67 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -548,6 +548,9 @@ export default class RoomList extends React.PureComponent { } public render() { + const cli = MatrixClientPeg.get(); + const userId = cli.getUserId(); + let explorePrompt: JSX.Element; if (!this.props.isMinimized) { if (this.state.isNameFiltering) { @@ -568,21 +571,23 @@ export default class RoomList extends React.PureComponent { { this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
    ; - } else if (this.props.activeSpace) { + } else if ( + this.props.activeSpace?.canInvite(userId) || this.props.activeSpace?.getMyMembership() === "join" + ) { explorePrompt =
    { _t("Quick actions") }
    - { this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && {_t("Invite people")} } - {_t("Explore rooms")} - + }
    ; } else if (Object.values(this.state.sublists).some(list => list.length > 0)) { const unfilteredLists = RoomListStore.instance.unfilteredLists From e05200269f814b53b5a4b0ea7bcc1ecdf1af8392 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 12:07:03 +0100 Subject: [PATCH 156/330] fix comment --- src/components/views/spaces/SpaceTreeLevel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 2e2901ce64..6825d84013 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -69,7 +69,7 @@ export class SpaceItem extends React.PureComponent { super(props); this.state = { - collapsed: !props.isNested, // default to collapsed for root items + collapsed: !props.isNested, // default to collapsed for root items contextMenuPosition: null, }; } From e219fe082adf3c2669323251bd9571493bee58d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 12:07:16 +0100 Subject: [PATCH 157/330] Tweak context switching edge case for space invites --- src/stores/SpaceStore.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index c28e24a460..a9a73e164f 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -123,7 +123,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // view last selected room from space const roomId = window.localStorage.getItem(getSpaceContextKey(this.activeSpace)); - if (roomId && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join") { + // if the space being selected is an invite then always view that invite + // else if the last viewed room in this space is joined then view that + // else view space home or home depending on what is being clicked on + if (space?.getMyMembership !== "invite" && + this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" + ) { defaultDispatcher.dispatch({ action: "view_room", room_id: roomId, From f2d0a56c1ffaffc87f7dbb1c21f099e3845ef77b Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Thu, 22 Apr 2021 16:53:20 +0530 Subject: [PATCH 158/330] Handle encoded matrix urls Signed-off-by: Jaiwanth --- src/linkify-matrix.js | 2 +- src/utils/permalinks/Permalinks.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index 1a40fde26f..84a131f23a 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -255,7 +255,7 @@ matrixLinkify.options = { target: function(href, type) { if (type === 'url') { const transformed = tryTransformPermalinkToLocalHref(href); - if (transformed !== href || href.match(matrixLinkify.ELEMENT_URL_PATTERN)) { + if (transformed !== href || decodeURIComponent(href).match(matrixLinkify.ELEMENT_URL_PATTERN)) { return null; } else { return '_blank'; diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.js index bcf4d87136..2db8b9f998 100644 --- a/src/utils/permalinks/Permalinks.js +++ b/src/utils/permalinks/Permalinks.js @@ -337,7 +337,7 @@ export function tryTransformPermalinkToLocalHref(permalink: string): string { return permalink; } - const m = permalink.match(matrixLinkify.ELEMENT_URL_PATTERN); + const m = decodeURIComponent(permalink).match(matrixLinkify.ELEMENT_URL_PATTERN); if (m) { return m[1]; } @@ -402,8 +402,8 @@ function getPermalinkConstructor(): PermalinkConstructor { export function parsePermalink(fullUrl: string): PermalinkParts { const elementPrefix = SdkConfig.get()['permalinkPrefix']; - if (fullUrl.startsWith(matrixtoBaseUrl)) { - return new SpecPermalinkConstructor().parsePermalink(fullUrl); + if (decodeURIComponent(fullUrl).startsWith(matrixtoBaseUrl)) { + return new SpecPermalinkConstructor().parsePermalink(decodeURIComponent(fullUrl)); } else if (elementPrefix && fullUrl.startsWith(elementPrefix)) { return new ElementPermalinkConstructor(elementPrefix).parsePermalink(fullUrl); } From ad53b0e2e26756f169eb5400fb0cdcd33f3bcfe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Apr 2021 14:56:12 +0200 Subject: [PATCH 159/330] Add normalizeWheelEvent() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/Mouse.ts | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/utils/Mouse.ts diff --git a/src/utils/Mouse.ts b/src/utils/Mouse.ts new file mode 100644 index 0000000000..a85c6492c4 --- /dev/null +++ b/src/utils/Mouse.ts @@ -0,0 +1,50 @@ +/* +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. +*/ + +/** + * Different browsers use different deltaModes. This causes different behaviour. + * To avoid that we use this function to convert any event to pixels. + * @param {WheelEvent} event to normalize + * @returns {WheelEvent} normalized event event + */ +export function normalizeWheelEvent(event: WheelEvent): WheelEvent { + const LINE_HEIGHT = 18; + + let deltaX; + let deltaY; + let deltaZ; + + if (event.deltaMode === 1) { // Units are lines + deltaX = (event.deltaX * LINE_HEIGHT); + deltaY = (event.deltaY * LINE_HEIGHT); + deltaZ = (event.deltaZ * LINE_HEIGHT); + } else { + deltaX = event.deltaX; + deltaY = event.deltaY; + deltaZ = event.deltaZ; + } + + return new WheelEvent( + "syntheticWheel", + { + deltaMode: 0, + deltaY: deltaY, + deltaX: deltaX, + deltaZ: deltaZ, + ...event, + }, + ); +} From 2e6397d8aac1b10cc1011427893cb6eed15800c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 22 Apr 2021 14:56:35 +0200 Subject: [PATCH 160/330] Wire up normalizeWheelEvent() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index bb69e24855..cbced07bfe 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -32,13 +32,14 @@ import dis from '../../../dispatcher/dispatcher'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" import {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {normalizeWheelEvent} from "../../../utils/Mouse"; const MIN_ZOOM = 100; const MAX_ZOOM = 300; // This is used for the buttons const ZOOM_STEP = 10; // This is used for mouse wheel events -const ZOOM_COEFFICIENT = 7.5; +const ZOOM_COEFFICIENT = 0.5; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; @@ -115,7 +116,9 @@ export default class ImageView extends React.Component { private onWheel = (ev: WheelEvent) => { ev.stopPropagation(); ev.preventDefault(); - const newZoom = this.state.zoom - (ev.deltaY * ZOOM_COEFFICIENT); + + const {deltaY} = normalizeWheelEvent(ev); + const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT); if (newZoom <= MIN_ZOOM) { this.setState({ From b332f6b1aec6d0c1e3f5a03c9d494a641e8a8a25 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 22 Apr 2021 13:59:02 +0100 Subject: [PATCH 161/330] Use floats for image background opacity It seems percentages for opacity are still newish, and they seem to confuse something which is clamping them to the 0 - 1 range (which makes sense for floats, not percentages). Anyway, for now we can get what we want here by using float values. Fixes https://github.com/vector-im/element-web/issues/17036 --- res/themes/dark/css/_dark.scss | 2 +- res/themes/legacy-dark/css/_legacy-dark.scss | 2 +- res/themes/legacy-light/css/_legacy-light.scss | 2 +- res/themes/light/css/_light.scss | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index bd7057c3e4..925d268eb0 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -85,7 +85,7 @@ $dialog-close-fg-color: #9fa9ba; $dialog-background-bg-color: $header-panel-bg-color; $lightbox-background-bg-color: #000; -$lightbox-background-bg-opacity: 85%; +$lightbox-background-bg-opacity: 0.85; $settings-grey-fg-color: #a2a2a2; $settings-profile-placeholder-bg-color: #21262c; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 9b2365a621..28e6e22326 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -83,7 +83,7 @@ $dialog-close-fg-color: #9fa9ba; $dialog-background-bg-color: $header-panel-bg-color; $lightbox-background-bg-color: #000; -$lightbox-background-bg-opacity: 85%; +$lightbox-background-bg-opacity: 0.85; $settings-grey-fg-color: #a2a2a2; $settings-profile-placeholder-bg-color: #e7e7e7; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 0956f433b2..7b6bdad4a4 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -127,7 +127,7 @@ $dialog-close-fg-color: #c1c1c1; $dialog-background-bg-color: #e9e9e9; $lightbox-background-bg-color: #000; -$lightbox-background-bg-opacity: 95%; +$lightbox-background-bg-opacity: 0.95; $imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index b307dbaba3..5b46138dae 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -118,7 +118,7 @@ $dialog-close-fg-color: #c1c1c1; $dialog-background-bg-color: #e9e9e9; $lightbox-background-bg-color: #000; -$lightbox-background-bg-opacity: 95%; +$lightbox-background-bg-opacity: 0.95; $imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); From fba7465ad4a611ddc6c6be95bb6cf1ad4c727865 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 22 Apr 2021 14:45:13 +0100 Subject: [PATCH 162/330] Initial SpaceStore tests work --- test/stores/SpaceStore-test.ts | 244 +++++++++++++++++++++++++++++++++ test/test-utils.js | 5 +- test/utils/test-utils.ts | 25 ++++ 3 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 test/stores/SpaceStore-test.ts create mode 100644 test/utils/test-utils.ts diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts new file mode 100644 index 0000000000..60960fd5cf --- /dev/null +++ b/test/stores/SpaceStore-test.ts @@ -0,0 +1,244 @@ +/* +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 { EventType } from "matrix-js-sdk/src/@types/event"; + +import "../skinned-sdk"; // Must be first for skinning to work +import SpaceStore from "../../src/stores/SpaceStore"; +import { setupAsyncStoreWithClient } from "../utils/test-utils"; +import { createTestClient, mkEvent, mkStubRoom } from "../test-utils"; +import { EnhancedMap } from "../../src/utils/maps"; +import SettingsStore from "../../src/settings/SettingsStore"; + +type MatrixEvent = any; // importing from js-sdk upsets things + +jest.useFakeTimers(); + +const mockStateEventImplementation = (events: MatrixEvent[]) => { + const stateMap = new EnhancedMap>(); + events.forEach(event => { + stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event); + }); + + return (eventType: string, stateKey?: string) => { + if (stateKey || stateKey === "") { + return stateMap.get(eventType)?.get(stateKey) || null; + } + return Array.from(stateMap.get(eventType)?.values() || []); + }; +}; + +const testUserId = "@test:user"; + +let rooms = []; + +const mkSpace = (spaceId: string, children: string[] = []) => { + const space = mkStubRoom(spaceId); + space.isSpaceRoom.mockReturnValue(true); + space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId => + mkEvent({ + event: true, + type: EventType.SpaceChild, + room: spaceId, + user: testUserId, + skey: roomId, + content: { via: [] }, + ts: Date.now(), + }), + ))); + rooms.push(space); + return space; +}; + +const getValue = jest.fn(); +SettingsStore.getValue = getValue; + +describe("SpaceStore", () => { + const store = SpaceStore.instance; + const client = createTestClient(); + + const run = async () => { + client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId)); + await setupAsyncStoreWithClient(store, client); + }; + + beforeEach(() => { + jest.runAllTimers(); + client.getVisibleRooms.mockReturnValue(rooms = []); + getValue.mockImplementation(settingName => { + if (settingName === "feature_spaces") { + return true; + } + }); + }); + afterEach(() => { + // @ts-ignore + store.onNotReady(); + }); + + describe("static hierarchy resolution tests", () => { + it("handles no spaces", async () => { + await run(); + + expect(store.spacePanelSpaces).toStrictEqual([]); + expect(store.invitedSpaces).toStrictEqual([]); + }); + + it("handles 3 joined top level spaces", async () => { + mkSpace("!space1:server"); + mkSpace("!space2:server"); + mkSpace("!space3:server"); + await run(); + + expect(store.spacePanelSpaces.sort()).toStrictEqual(client.getVisibleRooms().sort()); + expect(store.invitedSpaces).toStrictEqual([]); + }); + + it("handles a basic hierarchy", async () => { + mkSpace("!space1:server"); + mkSpace("!space2:server"); + mkSpace("!company:server", [ + mkSpace("!company_dept1:server", [ + mkSpace("!company_dept1_group1:server").roomId, + ]).roomId, + mkSpace("!company_dept2:server").roomId, + ]); + await run(); + + expect(store.spacePanelSpaces.map(r => r.roomId).sort()).toStrictEqual([ + "!space1:server", + "!space2:server", + "!company:server", + ].sort()); + expect(store.invitedSpaces).toStrictEqual([]); + // TODO verify actual tree structure + }); + + it("handles a sub-space existing in multiple places in the space tree", async () => { + const subspace = mkSpace("!subspace:server"); + mkSpace("!space1:server"); + mkSpace("!space2:server"); + mkSpace("!company:server", [ + mkSpace("!company_dept1:server", [ + mkSpace("!company_dept1_group1:server", [subspace.roomId]).roomId, + ]).roomId, + mkSpace("!company_dept2:server", [subspace.roomId]).roomId, + subspace.roomId, + ]); + await run(); + + expect(store.spacePanelSpaces.map(r => r.roomId).sort()).toStrictEqual([ + "!space1:server", + "!space2:server", + "!company:server", + ].sort()); + expect(store.invitedSpaces).toStrictEqual([]); + // TODO verify actual tree structure + }); + + it("handles basic cycles", async () => { + // TODO test all input order permutations + mkSpace("!a:server", [ + mkSpace("!b:server", [ + mkSpace("!c:server", [ + "!a:server", + ]).roomId, + ]).roomId, + ]); + await run(); + + expect(store.spacePanelSpaces.map(r => r.roomId)).toStrictEqual(["!a:server"]); + expect(store.invitedSpaces).toStrictEqual([]); + // TODO verify actual tree structure + }); + + it("handles complex cycles", async () => { + // TODO test all input order permutations + mkSpace("!b:server", [ + mkSpace("!a:server", [ + mkSpace("!c:server", [ + "!a:server", + ]).roomId, + ]).roomId, + ]); + await run(); + + expect(store.spacePanelSpaces.map(r => r.roomId)).toStrictEqual(["!b:server"]); + expect(store.invitedSpaces).toStrictEqual([]); + // TODO verify actual tree structure + }); + + it("handles really complex cycles", async () => { + // TODO test all input order permutations + mkSpace("!a:server", [ + mkSpace("!b:server", [ + mkSpace("!c:server", [ + "!a:server", + mkSpace("!d:server").roomId, + ]).roomId, + ]).roomId, + ]); + await run(); + + expect(store.spacePanelSpaces.map(r => r.roomId)).toStrictEqual(["!a:server"]); + expect(store.invitedSpaces).toStrictEqual([]); + // TODO verify actual tree structure + // TODO this test should be failing right now + }); + + describe("home space behaviour", () => { + test.todo("home space contains orphaned rooms"); + test.todo("home space contains favourites"); + test.todo("home space contains dm rooms"); + test.todo("home space contains invites"); + test.todo("home space contains invites even if they are also shown in a space"); + }); + }); + + describe("hierarchy resolution update tests", () => { + test.todo("updates state when spaces are joined"); + test.todo("updates state when spaces are left"); + test.todo("updates state when space invite comes in"); + test.todo("updates state when space invite is accepted"); + test.todo("updates state when space invite is rejected"); + }); + + describe("notification state tests", () => { + test.todo("//notification states"); + }); + + describe("room list prefilter tests", () => { + test.todo("//room list filter"); + }); + + describe("active space switching tests", () => { + test.todo("//active space"); + }); + + describe("context switching tests", () => { + test.todo("//context switching"); + }); + + describe("space auto switching tests", () => { + test.todo("//auto pick space for a room"); + }); + + describe("traverseSpace", () => { + test.todo("avoids cycles"); + test.todo("including rooms"); + test.todo("excluding rooms"); + }); +}); diff --git a/test/test-utils.js b/test/test-utils.js index d259fcb95f..33f7e1626e 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -88,8 +88,8 @@ export function createTestClient() { * @param {string} opts.type The event.type * @param {string} opts.room The event.room_id * @param {string} opts.user The event.user_id - * @param {string} opts.skey Optional. The state key (auto inserts empty string) - * @param {Number} opts.ts Optional. Timestamp for the event + * @param {string=} opts.skey Optional. The state key (auto inserts empty string) + * @param {number=} opts.ts Optional. Timestamp for the event * @param {Object} opts.content The event.content * @param {boolean} opts.event True to make a MatrixEvent. * @return {Object} a JSON object representing this event. @@ -244,6 +244,7 @@ export function mkStubRoom(roomId = null) { getDMInviter: jest.fn(), getAvatarUrl: () => 'mxc://avatar.url/room.png', getMxcAvatarUrl: () => 'mxc://avatar.url/room.png', + isSpaceRoom: jest.fn(() => false), }; } diff --git a/test/utils/test-utils.ts b/test/utils/test-utils.ts new file mode 100644 index 0000000000..f86196ffbd --- /dev/null +++ b/test/utils/test-utils.ts @@ -0,0 +1,25 @@ +/* +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 { MatrixClient } from "matrix-js-sdk/src/client"; +import {AsyncStoreWithClient} from "../../src/stores/AsyncStoreWithClient"; + +export const setupAsyncStoreWithClient = async (store: AsyncStoreWithClient, client: MatrixClient) => { + // @ts-ignore + store.readyStore.useUnitTestClient(client); + // @ts-ignore + await store.onReady(); +}; From d15e84602537fbd4f4347c3a904f17ee0e92f2a4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 22 Apr 2021 13:59:02 +0100 Subject: [PATCH 163/330] Use floats for image background opacity It seems percentages for opacity are still newish, and they seem to confuse something which is clamping them to the 0 - 1 range (which makes sense for floats, not percentages). Anyway, for now we can get what we want here by using float values. Fixes https://github.com/vector-im/element-web/issues/17036 --- res/themes/dark/css/_dark.scss | 2 +- res/themes/legacy-dark/css/_legacy-dark.scss | 2 +- res/themes/legacy-light/css/_legacy-light.scss | 2 +- res/themes/light/css/_light.scss | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index bd7057c3e4..925d268eb0 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -85,7 +85,7 @@ $dialog-close-fg-color: #9fa9ba; $dialog-background-bg-color: $header-panel-bg-color; $lightbox-background-bg-color: #000; -$lightbox-background-bg-opacity: 85%; +$lightbox-background-bg-opacity: 0.85; $settings-grey-fg-color: #a2a2a2; $settings-profile-placeholder-bg-color: #21262c; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 9b2365a621..28e6e22326 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -83,7 +83,7 @@ $dialog-close-fg-color: #9fa9ba; $dialog-background-bg-color: $header-panel-bg-color; $lightbox-background-bg-color: #000; -$lightbox-background-bg-opacity: 85%; +$lightbox-background-bg-opacity: 0.85; $settings-grey-fg-color: #a2a2a2; $settings-profile-placeholder-bg-color: #e7e7e7; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 0956f433b2..7b6bdad4a4 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -127,7 +127,7 @@ $dialog-close-fg-color: #c1c1c1; $dialog-background-bg-color: #e9e9e9; $lightbox-background-bg-color: #000; -$lightbox-background-bg-opacity: 95%; +$lightbox-background-bg-opacity: 0.95; $imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index b307dbaba3..5b46138dae 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -118,7 +118,7 @@ $dialog-close-fg-color: #c1c1c1; $dialog-background-bg-color: #e9e9e9; $lightbox-background-bg-color: #000; -$lightbox-background-bg-opacity: 95%; +$lightbox-background-bg-opacity: 0.95; $imagebody-giflabel: rgba(0, 0, 0, 0.7); $imagebody-giflabel-border: rgba(0, 0, 0, 0.2); From 14809dfda7f5b1e58539e239028124b6da4e6f79 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Apr 2021 08:22:31 -0600 Subject: [PATCH 164/330] Misc cleanup --- src/@types/global.d.ts | 1 - src/voice/RecorderWorklet.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 78dad28566..41257c21f0 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -138,7 +138,6 @@ declare global { outputs: Float32Array[][], parameters: Record ): boolean; - } // https://github.com/microsoft/TypeScript/issues/28308#issuecomment-650802278 diff --git a/src/voice/RecorderWorklet.ts b/src/voice/RecorderWorklet.ts index 48387fc06e..7343d37066 100644 --- a/src/voice/RecorderWorklet.ts +++ b/src/voice/RecorderWorklet.ts @@ -25,10 +25,6 @@ declare const currentTime: number; class MxVoiceWorklet extends AudioWorkletProcessor { private nextAmplitudeSecond = 0; - constructor() { - super(); - } - process(inputs, outputs, parameters) { // We only fire amplitude updates once a second to avoid flooding the recording instance // with useless data. Much of the data would end up discarded, so we ratelimit ourselves From 2b6551d06aac930eaf947e2b97ecb36a1b60dd27 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 22 Apr 2021 16:17:53 +0100 Subject: [PATCH 165/330] Remove reliance on DOM API to generated message preview --- src/HtmlUtils.tsx | 13 ++++++++----- .../room-list/previews/MessageEventPreview.ts | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 1dc342fac5..6b2568d68c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -130,11 +130,14 @@ export function sanitizedHtmlNode(insaneHtml: string) { return
    ; } -export function sanitizedHtmlNodeInnerText(insaneHtml: string) { - const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); - const contentDiv = document.createElement("div"); - contentDiv.innerHTML = saneHtml; - return contentDiv.innerText; +export function getHtmlText(insaneHtml: string) { + return sanitizeHtml(insaneHtml, { + allowedTags: [], + allowedAttributes: {}, + selfClosing: [], + allowedSchemes: [], + disallowedTagsMode: 'discard', + }) } /** diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts index deed7dcf2c..b900afc13f 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import ReplyThread from "../../../components/views/elements/ReplyThread"; -import { sanitizedHtmlNodeInnerText } from "../../../HtmlUtils"; +import { getHtmlText } from "../../../HtmlUtils"; export class MessageEventPreview implements IPreview { public getTextFor(event: MatrixEvent, tagId?: TagID): string { @@ -55,7 +55,7 @@ export class MessageEventPreview implements IPreview { } if (hasHtml) { - body = sanitizedHtmlNodeInnerText(body); + body = getHtmlText(body); } if (msgtype === 'm.emote') { From cc5a7671a724a908571fc8c20d46cdb1751f650d Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 22 Apr 2021 11:39:58 -0400 Subject: [PATCH 166/330] Keep invite button separate from space info 60828913d22541b8295ea8cce2874a210be23887 caused the space info and invite buttons to have no separation when you are the only person in a space, since the margin was set on the face pile, which may be absent. Signed-off-by: Robin Townsend --- res/css/structures/_SpaceRoomView.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 2dbf0fe0fe..269f16beb7 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -214,12 +214,11 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SpaceRoomView_info { display: inline-block; - margin: 0; + margin: 0 auto 0 0; } .mx_FacePile { display: inline-block; - margin-left: auto; margin-right: 12px; .mx_FacePile_faces { From a3e846685d795d81a369ff4b23f39ead31163095 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Apr 2021 16:24:41 -0600 Subject: [PATCH 167/330] Add array utility tests (and support upsampling in resample) See contained diff. The upsampling is important for Voice Messages, but is being done here because it's easier to add all the tests at once. This also introduces a new Object utility - that will be tested on its own commit. --- src/utils/arrays.ts | 51 ++++++-- src/utils/objects.ts | 20 ++- test/arrays-test.ts | 294 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 354 insertions(+), 11 deletions(-) create mode 100644 test/arrays-test.ts diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index 8ab66dfb29..cea377bfe9 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -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. @@ -15,23 +15,47 @@ limitations under the License. */ /** - * Quickly resample an array to have less data points. This isn't a perfect representation, - * though this does work best if given a large array to downsample to a much smaller array. - * @param {number[]} input The input array to downsample. + * Quickly resample an array to have less/more data points. If an input which is larger + * than the desired size is provided, it will be downsampled. Similarly, if the input + * is smaller than the desired size then it will be upsampled. + * @param {number[]} input The input array to resample. * @param {number} points The number of samples to end up with. - * @returns {number[]} The downsampled array. + * @returns {number[]} The resampled array. */ export function arrayFastResample(input: number[], points: number): number[] { - // Heavily inpired by matrix-media-repo (used with permission) + if (input.length === points) return input; // short-circuit a complicated call + + // Heavily inspired by matrix-media-repo (used with permission) // https://github.com/turt2live/matrix-media-repo/blob/abe72c87d2e29/util/util_audio/fastsample.go#L10 - const everyNth = Math.round(input.length / points); - const samples: number[] = []; - for (let i = 0; i < input.length; i += everyNth) { - samples.push(input[i]); + let samples: number[] = []; + if (input.length > points) { + // Danger: this loop can cause out of memory conditions if the input is too small. + const everyNth = Math.round(input.length / points); + for (let i = 0; i < input.length; i += everyNth) { + samples.push(input[i]); + } + } else { + // Smaller inputs mean we have to spread the values over the desired length. We + // end up overshooting the target length in doing this, so we'll resample down + // before returning. This recursion is risky, but mathematically should not go + // further than 1 level deep. + const spreadFactor = Math.ceil(points / input.length); + for (const val of input) { + samples.push(...arraySeed(val, spreadFactor)); + } + samples = arrayFastResample(samples, points); } + + // Sanity fill, just in case while (samples.length < points) { samples.push(input[input.length - 1]); } + + // Sanity trim, just in case + if (samples.length > points) { + samples = samples.slice(0, points); + } + return samples; } @@ -178,6 +202,13 @@ export class GroupedArray { constructor(private val: Map) { } + /** + * The value of this group, after all applicable alterations. + */ + public get value(): Map { + return this.val; + } + /** * Orders the grouping into an array using the provided key order. * @param keyOrder The key order. diff --git a/src/utils/objects.ts b/src/utils/objects.ts index e7f4f0f907..2c9361beba 100644 --- a/src/utils/objects.ts +++ b/src/utils/objects.ts @@ -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. @@ -141,3 +141,21 @@ export function objectKeyChanges(a: O, b: O): (keyof O)[] { export function objectClone(obj: O): O { return JSON.parse(JSON.stringify(obj)); } + +/** + * Converts a series of entries to an object. + * @param entries The entries to convert. + * @returns The converted object. + */ +// NOTE: Deprecated once we have Object.fromEntries() support. +// @ts-ignore - return type is complaining about non-string keys, but we know better +export function objectFromEntries(entries: Iterable<[K, V]>): {[k: K]: V} { + const obj: { + // @ts-ignore - same as return type + [k: K]: V} = {}; + for (const e of entries) { + // @ts-ignore - same as return type + obj[e[0]] = e[1]; + } + return obj; +} diff --git a/test/arrays-test.ts b/test/arrays-test.ts new file mode 100644 index 0000000000..33c4ee452e --- /dev/null +++ b/test/arrays-test.ts @@ -0,0 +1,294 @@ +/* +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 { + arrayDiff, + arrayFastClone, + arrayFastResample, + arrayHasDiff, + arrayHasOrderChange, + arrayMerge, + arraySeed, + arrayUnion, + ArrayUtil, + GroupedArray, +} from "../src/utils/arrays"; +import {objectFromEntries} from "../src/utils/objects"; + +function expectSample(i: number, input: number[], expected: number[]) { + console.log(`Resample case index: ${i}`); // for debugging test failures + const result = arrayFastResample(input, expected.length); + expect(result).toBeDefined(); + expect(result).toHaveLength(expected.length); + expect(result).toEqual(expected); +} + +describe('arrays', () => { + describe('arrayFastResample', () => { + it('should downsample', () => { + [ + {input: [1, 2, 3, 4, 5], output: [1, 4]}, // Odd -> Even + {input: [1, 2, 3, 4, 5], output: [1, 3, 5]}, // Odd -> Odd + {input: [1, 2, 3, 4], output: [1, 2, 3]}, // Even -> Odd + {input: [1, 2, 3, 4], output: [1, 3]}, // Even -> Even + ].forEach((c, i) => expectSample(i, c.input, c.output)); + }); + + it('should upsample', () => { + [ + {input: [1, 2, 3], output: [1, 1, 2, 2, 3, 3]}, // Odd -> Even + {input: [1, 2, 3], output: [1, 1, 2, 2, 3]}, // Odd -> Odd + {input: [1, 2], output: [1, 1, 1, 2, 2]}, // Even -> Odd + {input: [1, 2], output: [1, 1, 1, 2, 2, 2]}, // Even -> Even + ].forEach((c, i) => expectSample(i, c.input, c.output)); + }); + + it('should maintain sample', () => { + [ + {input: [1, 2, 3], output: [1, 2, 3]}, // Odd + {input: [1, 2], output: [1, 2]}, // Even + ].forEach((c, i) => expectSample(i, c.input, c.output)); + }); + }); + + describe('arraySeed', () => { + it('should create an array of given length', () => { + const val = 1; + const output = [val, val, val]; + const result = arraySeed(val, output.length); + expect(result).toBeDefined(); + expect(result).toHaveLength(output.length); + expect(result).toEqual(output); + }); + it('should maintain pointers', () => { + const val = {}; // this works because `{} !== {}`, which is what toEqual checks + const output = [val, val, val]; + const result = arraySeed(val, output.length); + expect(result).toBeDefined(); + expect(result).toHaveLength(output.length); + expect(result).toEqual(output); + }); + }); + + describe('arrayFastClone', () => { + it('should break pointer reference on source array', () => { + const val = {}; // we'll test to make sure the values maintain pointers too + const input = [val, val, val]; + const result = arrayFastClone(input); + expect(result).toBeDefined(); + expect(result).toHaveLength(input.length); + expect(result).toEqual(input); // we want the array contents to match... + expect(result).not.toBe(input); // ... but be a different reference + }); + }); + + describe('arrayHasOrderChange', () => { + it('should flag true on B ordering difference', () => { + const a = [1, 2, 3]; + const b = [3, 2, 1]; + const result = arrayHasOrderChange(a, b); + expect(result).toBe(true); + }); + + it('should flag false on no ordering difference', () => { + const a = [1, 2, 3]; + const b = [1, 2, 3]; + const result = arrayHasOrderChange(a, b); + expect(result).toBe(false); + }); + + it('should flag true on A length > B length', () => { + const a = [1, 2, 3, 4]; + const b = [1, 2, 3]; + const result = arrayHasOrderChange(a, b); + expect(result).toBe(true); + }); + + it('should flag true on A length < B length', () => { + const a = [1, 2, 3]; + const b = [1, 2, 3, 4]; + const result = arrayHasOrderChange(a, b); + expect(result).toBe(true); + }); + }); + + describe('arrayHasDiff', () => { + it('should flag true on A length > B length', () => { + const a = [1, 2, 3, 4]; + const b = [1, 2, 3]; + const result = arrayHasDiff(a, b); + expect(result).toBe(true); + }); + + it('should flag true on A length < B length', () => { + const a = [1, 2, 3]; + const b = [1, 2, 3, 4]; + const result = arrayHasDiff(a, b); + expect(result).toBe(true); + }); + + it('should flag true on element differences', () => { + const a = [1, 2, 3]; + const b = [4, 5, 6]; + const result = arrayHasDiff(a, b); + expect(result).toBe(true); + }); + + it('should flag false if same but order different', () => { + const a = [1, 2, 3]; + const b = [3, 1, 2]; + const result = arrayHasDiff(a, b); + expect(result).toBe(false); + }); + + it('should flag false if same', () => { + const a = [1, 2, 3]; + const b = [1, 2, 3]; + const result = arrayHasDiff(a, b); + expect(result).toBe(false); + }); + }); + + describe('arrayDiff', () => { + it('should see added from A->B', () => { + const a = [1, 2, 3]; + const b = [1, 2, 3, 4]; + const result = arrayDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.added).toHaveLength(1); + expect(result.removed).toHaveLength(0); + expect(result.added).toEqual([4]); + }); + + it('should see removed from A->B', () => { + const a = [1, 2, 3]; + const b = [1, 2]; + const result = arrayDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(1); + expect(result.removed).toEqual([3]); + }); + + it('should see added and removed in the same set', () => { + const a = [1, 2, 3]; + const b = [1, 2, 4]; // note diff + const result = arrayDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.added).toHaveLength(1); + expect(result.removed).toHaveLength(1); + expect(result.added).toEqual([4]); + expect(result.removed).toEqual([3]); + }); + }); + + describe('arrayUnion', () => { + it('should return a union', () => { + const a = [1, 2, 3]; + const b = [1, 2, 4]; // note diff + const result = arrayUnion(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(2); + expect(result).toEqual([1, 2]); + }); + + it('should return an empty array on no matches', () => { + const a = [1, 2, 3]; + const b = [4, 5, 6]; + const result = arrayUnion(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(0); + }); + }); + + describe('arrayMerge', () => { + it('should merge 3 arrays with deduplication', () => { + const a = [1, 2, 3]; + const b = [1, 2, 4, 5]; // note missing 3 + const c = [6, 7, 8, 9]; + const result = arrayMerge(a, b, c); + expect(result).toBeDefined(); + expect(result).toHaveLength(9); + expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + + it('should deduplicate a single array', () => { + // dev note: this is technically an edge case, but it is described behaviour if the + // function is only provided one function (it'll merge the array against itself) + const a = [1, 1, 2, 2, 3, 3]; + const result = arrayMerge(a); + expect(result).toBeDefined(); + expect(result).toHaveLength(3); + expect(result).toEqual([1, 2, 3]); + }); + }); + + describe('ArrayUtil', () => { + it('should maintain the pointer to the given array', () => { + const input = [1, 2, 3]; + const result = new ArrayUtil(input); + expect(result.value).toBe(input); + }); + + it('should group appropriately', () => { + const input = [['a', 1], ['b', 2], ['c', 3], ['a', 4], ['a', 5], ['b', 6]]; + const output = { + 'a': [['a', 1], ['a', 4], ['a', 5]], + 'b': [['b', 2], ['b', 6]], + 'c': [['c', 3]], + }; + const result = new ArrayUtil(input).groupBy(p => p[0]); + expect(result).toBeDefined(); + expect(result.value).toBeDefined(); + + const asObject = objectFromEntries(result.value.entries()); + expect(asObject).toMatchObject(output); + }); + }); + + describe('GroupedArray', () => { + it('should maintain the pointer to the given map', () => { + const input = new Map([ + ['a', [1, 2, 3]], + ['b', [7, 8, 9]], + ['c', [4, 5, 6]], + ]); + const result = new GroupedArray(input); + expect(result.value).toBe(input); + }); + + it('should ordering by the provided key order', () => { + const input = new Map([ + ['a', [1, 2, 3]], + ['b', [7, 8, 9]], // note counting diff + ['c', [4, 5, 6]], + ]); + const output = [4, 5, 6, 1, 2, 3, 7, 8, 9]; + const keyOrder = ['c', 'a', 'b']; // note weird order to cause the `output` to be strange + const result = new GroupedArray(input).orderBy(keyOrder); + expect(result).toBeDefined(); + expect(result.value).toBeDefined(); + expect(result.value).toEqual(output); + }); + }); +}); + From 772ff4e257930ae65bc3843f98de1740400f852e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Apr 2021 19:28:56 -0600 Subject: [PATCH 168/330] Add object utility tests --- test/objects-test.ts | 262 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 test/objects-test.ts diff --git a/test/objects-test.ts b/test/objects-test.ts new file mode 100644 index 0000000000..912d371ba2 --- /dev/null +++ b/test/objects-test.ts @@ -0,0 +1,262 @@ +/* +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 { + objectClone, + objectDiff, + objectExcluding, + objectFromEntries, + objectHasDiff, + objectKeyChanges, + objectShallowClone, + objectWithOnly, +} from "../src/utils/objects"; + +describe('objects', () => { + describe('objectExcluding', () => { + it('should exclude the given properties', () => { + const input = {hello: "world", test: true}; + const output = {hello: "world"}; + const props = ["test", "doesnotexist"]; // we also make sure it doesn't explode on missing props + const result = objectExcluding(input, props); // any is to test the missing prop + expect(result).toBeDefined(); + expect(result).toMatchObject(output); + }); + }); + + describe('objectWithOnly', () => { + it('should exclusively use the given properties', () => { + const input = {hello: "world", test: true}; + const output = {hello: "world"}; + const props = ["hello", "doesnotexist"]; // we also make sure it doesn't explode on missing props + const result = objectWithOnly(input, props); // any is to test the missing prop + expect(result).toBeDefined(); + expect(result).toMatchObject(output); + }); + }); + + describe('objectShallowClone', () => { + it('should create a new object', () => { + const input = {test: 1}; + const result = objectShallowClone(input); + expect(result).toBeDefined(); + expect(result).not.toBe(input); + expect(result).toMatchObject(input); + }); + + it('should only clone the top level properties', () => { + const input = {a: 1, b: {c: 2}}; + const result = objectShallowClone(input); + expect(result).toBeDefined(); + expect(result).toMatchObject(input); + expect(result.b).toBe(input.b); + }); + + it('should support custom clone functions', () => { + const input = {a: 1, b: 2}; + const output = {a: 4, b: 8}; + const result = objectShallowClone(input, (k, v) => { + // XXX: inverted expectation for ease of assertion + expect(Object.keys(input)).toContain(k); + + return v * 4; + }); + expect(result).toBeDefined(); + expect(result).toMatchObject(output); + }); + }); + + describe('objectHasDiff', () => { + it('should return false for the same pointer', () => { + const a = {}; + const result = objectHasDiff(a, a); + expect(result).toBe(false); + }); + + it('should return true if keys for A > keys for B', () => { + const a = {a: 1, b: 2}; + const b = {a: 1}; + const result = objectHasDiff(a, b); + expect(result).toBe(true); + }); + + it('should return true if keys for A < keys for B', () => { + const a = {a: 1}; + const b = {a: 1, b: 2}; + const result = objectHasDiff(a, b); + expect(result).toBe(true); + }); + + it('should return false if the objects are the same but different pointers', () => { + const a = {a: 1, b: 2}; + const b = {a: 1, b: 2}; + const result = objectHasDiff(a, b); + expect(result).toBe(false); + }); + + it('should consider pointers when testing values', () => { + const a = {a: {}, b: 2}; // `{}` is shorthand for `new Object()` + const b = {a: {}, b: 2}; + const result = objectHasDiff(a, b); + expect(result).toBe(true); // even though the keys are the same, the value pointers vary + }); + }); + + describe('objectDiff', () => { + it('should return empty sets for the same object', () => { + const a = {a: 1, b: 2}; + const b = {a: 1, b: 2}; + const result = objectDiff(a, b); + expect(result).toBeDefined(); + expect(result.changed).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toHaveLength(0); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(0); + }); + + it('should return empty sets for the same object pointer', () => { + const a = {a: 1, b: 2}; + const result = objectDiff(a, a); + expect(result).toBeDefined(); + expect(result.changed).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toHaveLength(0); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(0); + }); + + it('should indicate when property changes are made', () => { + const a = {a: 1, b: 2}; + const b = {a: 11, b: 2}; + const result = objectDiff(a, b); + expect(result.changed).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toHaveLength(1); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(0); + expect(result.changed).toEqual(['a']); + }); + + it('should indicate when properties are added', () => { + const a = {a: 1, b: 2}; + const b = {a: 1, b: 2, c: 3}; + const result = objectDiff(a, b); + expect(result.changed).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toHaveLength(0); + expect(result.added).toHaveLength(1); + expect(result.removed).toHaveLength(0); + expect(result.added).toEqual(['c']); + }); + + it('should indicate when properties are removed', () => { + const a = {a: 1, b: 2}; + const b = {a: 1}; + const result = objectDiff(a, b); + expect(result.changed).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toHaveLength(0); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(1); + expect(result.removed).toEqual(['b']); + }); + + it('should indicate when multiple aspects change', () => { + const a = {a: 1, b: 2, c: 3}; + const b: (typeof a | {d: number}) = {a: 1, b: 22, d: 4}; + const result = objectDiff(a, b); + expect(result.changed).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toHaveLength(1); + expect(result.added).toHaveLength(1); + expect(result.removed).toHaveLength(1); + expect(result.changed).toEqual(['b']); + expect(result.removed).toEqual(['c']); + expect(result.added).toEqual(['d']); + }); + }); + + describe('objectKeyChanges', () => { + it('should return an empty set if no properties changed', () => { + const a = {a: 1, b: 2}; + const b = {a: 1, b: 2}; + const result = objectKeyChanges(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(0); + }); + + it('should return an empty set if no properties changed for the same pointer', () => { + const a = {a: 1, b: 2}; + const result = objectKeyChanges(a, a); + expect(result).toBeDefined(); + expect(result).toHaveLength(0); + }); + + it('should return properties which were changed, added, or removed', () => { + const a = {a: 1, b: 2, c: 3}; + const b: (typeof a | {d: number}) = {a: 1, b: 22, d: 4}; + const result = objectKeyChanges(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(3); + expect(result).toEqual(['c', 'd', 'b']); // order isn't important, but the test cares + }); + }); + + describe('objectClone', () => { + it('should deep clone an object', () => { + const a = { + hello: "world", + test: { + another: "property", + test: 42, + third: { + prop: true, + }, + }, + }; + const result = objectClone(a); + expect(result).toBeDefined(); + expect(result).not.toBe(a); + expect(result).toMatchObject(a); + expect(result.test).not.toBe(a.test); + expect(result.test.third).not.toBe(a.test.third); + }); + }); + + describe('objectFromEntries', () => { + it('should create an object from an array of entries', () => { + const output = {a: 1, b: 2, c: 3}; + const result = objectFromEntries(Object.entries(output)); + expect(result).toBeDefined(); + expect(result).toMatchObject(output); + }); + + it('should maintain pointers in values', () => { + const output = {a: {}, b: 2, c: 3}; + const result = objectFromEntries(Object.entries(output)); + expect(result).toBeDefined(); + expect(result).toMatchObject(output); + expect(result['a']).toBe(output.a); + }); + }); +}); From 21cae1502a3003b02d8700b60f94a5e6b27fb5a7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Apr 2021 19:51:11 -0600 Subject: [PATCH 169/330] Add map utility tests --- test/maps-test.ts | 245 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 test/maps-test.ts diff --git a/test/maps-test.ts b/test/maps-test.ts new file mode 100644 index 0000000000..5363ab3d03 --- /dev/null +++ b/test/maps-test.ts @@ -0,0 +1,245 @@ +/* +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 {EnhancedMap, mapDiff, mapKeyChanges} from "../src/utils/maps"; + +describe('maps', () => { + describe('mapDiff', () => { + it('should indicate no differences when the pointers are the same', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const result = mapDiff(a, a); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toBeDefined(); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(0); + expect(result.changed).toHaveLength(0); + }); + + it('should indicate no differences when there are none', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const b = new Map([[1, 1], [2, 2], [3, 3]]); + const result = mapDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toBeDefined(); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(0); + expect(result.changed).toHaveLength(0); + }); + + it('should indicate added properties', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const b = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]); + const result = mapDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toBeDefined(); + expect(result.added).toHaveLength(1); + expect(result.removed).toHaveLength(0); + expect(result.changed).toHaveLength(0); + expect(result.added).toEqual([4]); + }); + + it('should indicate removed properties', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const b = new Map([[1, 1], [2, 2]]); + const result = mapDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toBeDefined(); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(1); + expect(result.changed).toHaveLength(0); + expect(result.removed).toEqual([3]); + }); + + it('should indicate changed properties', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const b = new Map([[1, 1], [2, 2], [3, 4]]); // note change + const result = mapDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toBeDefined(); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(0); + expect(result.changed).toHaveLength(1); + expect(result.changed).toEqual([3]); + }); + + it('should indicate changed, added, and removed properties', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const b = new Map([[1, 1], [2, 8], [4, 4]]); // note change + const result = mapDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toBeDefined(); + expect(result.added).toHaveLength(1); + expect(result.removed).toHaveLength(1); + expect(result.changed).toHaveLength(1); + expect(result.added).toEqual([4]); + expect(result.removed).toEqual([3]); + expect(result.changed).toEqual([2]); + }); + + it('should indicate changes for difference in pointers', () => { + const a = new Map([[1, {}]]); // {} always creates a new object + const b = new Map([[1, {}]]); + const result = mapDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.changed).toBeDefined(); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(0); + expect(result.changed).toHaveLength(1); + expect(result.changed).toEqual([1]); + }); + }); + + describe('mapKeyChanges', () => { + it('should indicate no changes for unchanged pointers', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const result = mapKeyChanges(a, a); + expect(result).toBeDefined(); + expect(result).toHaveLength(0); + }); + + it('should indicate no changes for unchanged maps with different pointers', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const b = new Map([[1, 1], [2, 2], [3, 3]]); + const result = mapKeyChanges(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(0); + }); + + it('should indicate changes for added properties', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const b = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]); + const result = mapKeyChanges(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(1); + expect(result).toEqual([4]); + }); + + it('should indicate changes for removed properties', () => { + const a = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]); + const b = new Map([[1, 1], [2, 2], [3, 3]]); + const result = mapKeyChanges(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(1); + expect(result).toEqual([4]); + }); + + it('should indicate changes for changed properties', () => { + const a = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]); + const b = new Map([[1, 1], [2, 2], [3, 3], [4, 55]]); + const result = mapKeyChanges(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(1); + expect(result).toEqual([4]); + }); + + it('should indicate changes for properties with different pointers', () => { + const a = new Map([[1, {}]]); // {} always creates a new object + const b = new Map([[1, {}]]); + const result = mapKeyChanges(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(1); + expect(result).toEqual([1]); + }); + + it('should indicate changes for changed, added, and removed properties', () => { + const a = new Map([[1, 1], [2, 2], [3, 3]]); + const b = new Map([[1, 1], [2, 8], [4, 4]]); // note change + const result = mapKeyChanges(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(3); + expect(result).toEqual([3, 4, 2]); // order irrelevant, but the test cares + }); + }); + + describe('EnhancedMap', () => { + // Most of these tests will make sure it implements the Map class + + it('should be empty by default', () => { + const result = new EnhancedMap(); + expect(result.size).toBe(0); + }); + + it('should use the provided entries', () => { + const obj = {a: 1, b: 2}; + const result = new EnhancedMap(Object.entries(obj)); + expect(result.size).toBe(2); + expect(result.get('a')).toBe(1); + expect(result.get('b')).toBe(2); + }); + + it('should create keys if they do not exist', () => { + const key = 'a'; + const val = {}; // we'll check pointers + + const result = new EnhancedMap(); + expect(result.size).toBe(0); + + let get = result.getOrCreate(key, val); + expect(get).toBeDefined(); + expect(get).toBe(val); + expect(result.size).toBe(1); + + get = result.getOrCreate(key, 44); // specifically change `val` + expect(get).toBeDefined(); + expect(get).toBe(val); + expect(result.size).toBe(1); + + get = result.get(key); // use the base class function + expect(get).toBeDefined(); + expect(get).toBe(val); + expect(result.size).toBe(1); + }); + + it('should proxy remove to delete and return it', () => { + const val = {}; + const result = new EnhancedMap(); + result.set('a', val); + + expect(result.size).toBe(1); + + const removed = result.remove('a'); + expect(result.size).toBe(0); + expect(removed).toBeDefined(); + expect(removed).toBe(val); + }); + + it('should support removing unknown keys', () => { + const val = {}; + const result = new EnhancedMap(); + result.set('a', val); + + expect(result.size).toBe(1); + + const removed = result.remove('not-a'); + expect(result.size).toBe(1); + expect(removed).not.toBeDefined(); + }); + }); +}); From 0d4218ee35371ddd0354ffd8d770927c8abe20f4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Apr 2021 20:07:38 -0600 Subject: [PATCH 170/330] Add enum utility tests --- src/utils/enums.ts | 22 +++++++++++---- test/enums-test.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 test/enums-test.ts diff --git a/src/utils/enums.ts b/src/utils/enums.ts index f7f4787896..d3ca318c28 100644 --- a/src/utils/enums.ts +++ b/src/utils/enums.ts @@ -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. @@ -19,11 +19,23 @@ limitations under the License. * @param e The enum. * @returns The enum values. */ -export function getEnumValues(e: any): T[] { +export function getEnumValues(e: any): (string | number)[] { + // String-based enums will simply be objects ({Key: "value"}), but number-based + // enums will instead map themselves twice: in one direction for {Key: 12} and + // the reverse for easy lookup, presumably ({12: Key}). In the reverse mapping, + // the key is a string, not a number. + // + // For this reason, we try to determine what kind of enum we're dealing with. + const keys = Object.keys(e); - return keys - .filter(k => ['string', 'number'].includes(typeof(e[k]))) - .map(k => e[k]); + const values: (string | number)[] = []; + for (const key of keys) { + const value = e[key]; + if (Number.isFinite(value) || e[value.toString()] !== Number(key)) { + values.push(value); + } + } + return values; } /** diff --git a/test/enums-test.ts b/test/enums-test.ts new file mode 100644 index 0000000000..e519186a51 --- /dev/null +++ b/test/enums-test.ts @@ -0,0 +1,67 @@ +/* +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 {getEnumValues, isEnumValue} from "../src/utils/enums"; + +enum TestStringEnum { + First = "__first__", + Second = "__second__", +} + +enum TestNumberEnum { + FirstKey = 10, + SecondKey = 20, +} + +describe('enums', () => { + describe('getEnumValues', () => { + it('should work on string enums', () => { + const result = getEnumValues(TestStringEnum); + expect(result).toBeDefined(); + expect(result).toHaveLength(2); + expect(result).toEqual(['__first__', '__second__']); + }); + + it('should work on number enums', () => { + const result = getEnumValues(TestNumberEnum); + expect(result).toBeDefined(); + expect(result).toHaveLength(2); + expect(result).toEqual([10, 20]); + }); + }); + + describe('isEnumValue', () => { + it('should return true on values in a string enum', () => { + const result = isEnumValue(TestStringEnum, '__first__'); + expect(result).toBe(true); + }); + + it('should return false on values not in a string enum', () => { + const result = isEnumValue(TestStringEnum, 'not a value'); + expect(result).toBe(false); + }); + + it('should return true on values in a number enum', () => { + const result = isEnumValue(TestNumberEnum, 10); + expect(result).toBe(true); + }); + + it('should return false on values not in a number enum', () => { + const result = isEnumValue(TestStringEnum, 99); + expect(result).toBe(false); + }); + }); +}); From 6124a8319b803ce36e91d84f60597e4b28499b1e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Apr 2021 20:11:20 -0600 Subject: [PATCH 171/330] Add iterable utility tests Unsurprisingly, it's a copy/paste of the array tests --- test/iterables-test.ts | 77 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test/iterables-test.ts diff --git a/test/iterables-test.ts b/test/iterables-test.ts new file mode 100644 index 0000000000..af0232fb93 --- /dev/null +++ b/test/iterables-test.ts @@ -0,0 +1,77 @@ +/* +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 {iterableDiff, iterableUnion} from "../src/utils/iterables"; + +describe('iterables', () => { + describe('iterableUnion', () => { + it('should return a union', () => { + const a = [1, 2, 3]; + const b = [1, 2, 4]; // note diff + const result = iterableUnion(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(2); + expect(result).toEqual([1, 2]); + }); + + it('should return an empty array on no matches', () => { + const a = [1, 2, 3]; + const b = [4, 5, 6]; + const result = iterableUnion(a, b); + expect(result).toBeDefined(); + expect(result).toHaveLength(0); + }); + }); + + describe('iterableDiff', () => { + it('should see added from A->B', () => { + const a = [1, 2, 3]; + const b = [1, 2, 3, 4]; + const result = iterableDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.added).toHaveLength(1); + expect(result.removed).toHaveLength(0); + expect(result.added).toEqual([4]); + }); + + it('should see removed from A->B', () => { + const a = [1, 2, 3]; + const b = [1, 2]; + const result = iterableDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.added).toHaveLength(0); + expect(result.removed).toHaveLength(1); + expect(result.removed).toEqual([3]); + }); + + it('should see added and removed in the same set', () => { + const a = [1, 2, 3]; + const b = [1, 2, 4]; // note diff + const result = iterableDiff(a, b); + expect(result).toBeDefined(); + expect(result.added).toBeDefined(); + expect(result.removed).toBeDefined(); + expect(result.added).toHaveLength(1); + expect(result.removed).toHaveLength(1); + expect(result.added).toEqual([4]); + expect(result.removed).toEqual([3]); + }); + }); +}); From 27af3291ed85c3a95a0b139c992e10dbd2847c62 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Apr 2021 20:26:48 -0600 Subject: [PATCH 172/330] Add number utility tests --- test/numbers-test.ts | 163 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 test/numbers-test.ts diff --git a/test/numbers-test.ts b/test/numbers-test.ts new file mode 100644 index 0000000000..6e0e3f58ce --- /dev/null +++ b/test/numbers-test.ts @@ -0,0 +1,163 @@ +/* +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 {clamp, defaultNumber, percentageOf, percentageWithin, sum} from "../src/utils/numbers"; + +describe('numbers', () => { + describe('defaultNumber', () => { + it('should use the default when the input is not a number', () => { + const def = 42; + + let result = defaultNumber(null, def); + expect(result).toBe(def); + + result = defaultNumber(undefined, def); + expect(result).toBe(def); + + result = defaultNumber(Number.NaN, def); + expect(result).toBe(def); + }); + + it('should use the number when it is a number', () => { + const input = 24; + const def = 42; + const result = defaultNumber(input, def); + expect(result).toBe(input); + }); + }); + + describe('clamp', () => { + it('should clamp high numbers', () => { + const input = 101; + const min = 0; + const max = 100; + const result = clamp(input, min, max); + expect(result).toBe(max); + }); + + it('should clamp low numbers', () => { + const input = -1; + const min = 0; + const max = 100; + const result = clamp(input, min, max); + expect(result).toBe(min); + }); + + it('should not clamp numbers in range', () => { + const input = 50; + const min = 0; + const max = 100; + const result = clamp(input, min, max); + expect(result).toBe(input); + }); + + it('should clamp floats', () => { + const min = -0.10; + const max = +0.10; + + let result = clamp(-1.2, min, max); + expect(result).toBe(min); + + result = clamp(1.2, min, max); + expect(result).toBe(max); + + result = clamp(0.02, min, max); + expect(result).toBe(0.02); + }); + }); + + describe('sum', () => { + it('should sum', () => { // duh + const result = sum(1, 2, 1, 4); + expect(result).toBe(8); + }); + }); + + describe('percentageWithin', () => { + it('should work within 0-100', () => { + const result = percentageWithin(0.4, 0, 100); + expect(result).toBe(40); + }); + + it('should work within 0-100 when pct > 1', () => { + const result = percentageWithin(1.4, 0, 100); + expect(result).toBe(140); + }); + + it('should work within 0-100 when pct < 0', () => { + const result = percentageWithin(-1.4, 0, 100); + expect(result).toBe(-140); + }); + + it('should work with ranges other than 0-100', () => { + const result = percentageWithin(0.4, 10, 20); + expect(result).toBe(14); + }); + + it('should work with ranges other than 0-100 when pct > 1', () => { + const result = percentageWithin(1.4, 10, 20); + expect(result).toBe(24); + }); + + it('should work with ranges other than 0-100 when pct < 0', () => { + const result = percentageWithin(-1.4, 10, 20); + expect(result).toBe(-4); + }); + + it('should work with floats', () => { + const result = percentageWithin(0.4, 10.2, 20.4); + expect(result).toBe(14.28); + }); + }); + + // These are the inverse of percentageWithin + describe('percentageOf', () => { + it('should work within 0-100', () => { + const result = percentageOf(40, 0, 100); + expect(result).toBe(0.4); + }); + + it('should work within 0-100 when val > 100', () => { + const result = percentageOf(140, 0, 100); + expect(result).toBe(1.40); + }); + + it('should work within 0-100 when val < 0', () => { + const result = percentageOf(-140, 0, 100); + expect(result).toBe(-1.40); + }); + + it('should work with ranges other than 0-100', () => { + const result = percentageOf(14, 10, 20); + expect(result).toBe(0.4); + }); + + it('should work with ranges other than 0-100 when val > 100', () => { + const result = percentageOf(24, 10, 20); + expect(result).toBe(1.4); + }); + + it('should work with ranges other than 0-100 when val < 0', () => { + const result = percentageOf(-4, 10, 20); + expect(result).toBe(-1.4); + }); + + it('should work with floats', () => { + const result = percentageOf(14.28, 10.2, 20.4); + expect(result).toBe(0.4); + }); + }); +}); From 374f51452ee364e4bc5f8921ef992ceebc562778 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Apr 2021 20:28:49 -0600 Subject: [PATCH 173/330] Add set utility tests --- test/sets-test.ts | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 test/sets-test.ts diff --git a/test/sets-test.ts b/test/sets-test.ts new file mode 100644 index 0000000000..e884d0e9af --- /dev/null +++ b/test/sets-test.ts @@ -0,0 +1,56 @@ +/* +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 {setHasDiff} from "../src/utils/sets"; + +describe('sets', () => { + describe('setHasDiff', () => { + it('should flag true on A length > B length', () => { + const a = new Set([1, 2, 3, 4]); + const b = new Set([1, 2, 3]); + const result = setHasDiff(a, b); + expect(result).toBe(true); + }); + + it('should flag true on A length < B length', () => { + const a = new Set([1, 2, 3]); + const b = new Set([1, 2, 3, 4]); + const result = setHasDiff(a, b); + expect(result).toBe(true); + }); + + it('should flag true on element differences', () => { + const a = new Set([1, 2, 3]); + const b = new Set([4, 5, 6]); + const result = setHasDiff(a, b); + expect(result).toBe(true); + }); + + it('should flag false if same but order different', () => { + const a = new Set([1, 2, 3]); + const b = new Set([3, 1, 2]); + const result = setHasDiff(a, b); + expect(result).toBe(false); + }); + + it('should flag false if same', () => { + const a = new Set([1, 2, 3]); + const b = new Set([1, 2, 3]); + const result = setHasDiff(a, b); + expect(result).toBe(false); + }); + }); +}); From 2c459c482872a6314ab3749a1f04e6b056ff8460 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 22 Apr 2021 20:30:14 -0600 Subject: [PATCH 174/330] Move utility tests to the right place --- test/{ => utils}/Singleflight-test.ts | 2 +- test/{ => utils}/arrays-test.ts | 4 ++-- test/{ => utils}/enums-test.ts | 2 +- test/{ => utils}/iterables-test.ts | 2 +- test/{ => utils}/maps-test.ts | 2 +- test/{ => utils}/numbers-test.ts | 2 +- test/{ => utils}/objects-test.ts | 2 +- test/{ => utils}/sets-test.ts | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) rename test/{ => utils}/Singleflight-test.ts (98%) rename test/{ => utils}/arrays-test.ts (99%) rename test/{ => utils}/enums-test.ts (96%) rename test/{ => utils}/iterables-test.ts (97%) rename test/{ => utils}/maps-test.ts (99%) rename test/{ => utils}/numbers-test.ts (99%) rename test/{ => utils}/objects-test.ts (99%) rename test/{ => utils}/sets-test.ts (97%) diff --git a/test/Singleflight-test.ts b/test/utils/Singleflight-test.ts similarity index 98% rename from test/Singleflight-test.ts rename to test/utils/Singleflight-test.ts index 4f0c6e0da3..80258701bb 100644 --- a/test/Singleflight-test.ts +++ b/test/utils/Singleflight-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Singleflight} from "../src/utils/Singleflight"; +import {Singleflight} from "../../src/utils/Singleflight"; describe('Singleflight', () => { afterEach(() => { diff --git a/test/arrays-test.ts b/test/utils/arrays-test.ts similarity index 99% rename from test/arrays-test.ts rename to test/utils/arrays-test.ts index 33c4ee452e..ececd274b2 100644 --- a/test/arrays-test.ts +++ b/test/utils/arrays-test.ts @@ -25,8 +25,8 @@ import { arrayUnion, ArrayUtil, GroupedArray, -} from "../src/utils/arrays"; -import {objectFromEntries} from "../src/utils/objects"; +} from "../../src/utils/arrays"; +import {objectFromEntries} from "../../src/utils/objects"; function expectSample(i: number, input: number[], expected: number[]) { console.log(`Resample case index: ${i}`); // for debugging test failures diff --git a/test/enums-test.ts b/test/utils/enums-test.ts similarity index 96% rename from test/enums-test.ts rename to test/utils/enums-test.ts index e519186a51..423b135f77 100644 --- a/test/enums-test.ts +++ b/test/utils/enums-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {getEnumValues, isEnumValue} from "../src/utils/enums"; +import {getEnumValues, isEnumValue} from "../../src/utils/enums"; enum TestStringEnum { First = "__first__", diff --git a/test/iterables-test.ts b/test/utils/iterables-test.ts similarity index 97% rename from test/iterables-test.ts rename to test/utils/iterables-test.ts index af0232fb93..9b30b6241c 100644 --- a/test/iterables-test.ts +++ b/test/utils/iterables-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {iterableDiff, iterableUnion} from "../src/utils/iterables"; +import {iterableDiff, iterableUnion} from "../../src/utils/iterables"; describe('iterables', () => { describe('iterableUnion', () => { diff --git a/test/maps-test.ts b/test/utils/maps-test.ts similarity index 99% rename from test/maps-test.ts rename to test/utils/maps-test.ts index 5363ab3d03..8764a8f2cf 100644 --- a/test/maps-test.ts +++ b/test/utils/maps-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {EnhancedMap, mapDiff, mapKeyChanges} from "../src/utils/maps"; +import {EnhancedMap, mapDiff, mapKeyChanges} from "../../src/utils/maps"; describe('maps', () => { describe('mapDiff', () => { diff --git a/test/numbers-test.ts b/test/utils/numbers-test.ts similarity index 99% rename from test/numbers-test.ts rename to test/utils/numbers-test.ts index 6e0e3f58ce..36e7d4f7e7 100644 --- a/test/numbers-test.ts +++ b/test/utils/numbers-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {clamp, defaultNumber, percentageOf, percentageWithin, sum} from "../src/utils/numbers"; +import {clamp, defaultNumber, percentageOf, percentageWithin, sum} from "../../src/utils/numbers"; describe('numbers', () => { describe('defaultNumber', () => { diff --git a/test/objects-test.ts b/test/utils/objects-test.ts similarity index 99% rename from test/objects-test.ts rename to test/utils/objects-test.ts index 912d371ba2..b7a80e6761 100644 --- a/test/objects-test.ts +++ b/test/utils/objects-test.ts @@ -23,7 +23,7 @@ import { objectKeyChanges, objectShallowClone, objectWithOnly, -} from "../src/utils/objects"; +} from "../../src/utils/objects"; describe('objects', () => { describe('objectExcluding', () => { diff --git a/test/sets-test.ts b/test/utils/sets-test.ts similarity index 97% rename from test/sets-test.ts rename to test/utils/sets-test.ts index e884d0e9af..98dc218309 100644 --- a/test/sets-test.ts +++ b/test/utils/sets-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {setHasDiff} from "../src/utils/sets"; +import {setHasDiff} from "../../src/utils/sets"; describe('sets', () => { describe('setHasDiff', () => { From 107575692996f89123e082156767ab0f4e3760e0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Apr 2021 09:55:30 +0100 Subject: [PATCH 175/330] add more tests --- src/stores/SpaceStore.tsx | 4 +- test/stores/SpaceStore-test.ts | 73 +++++++++++++++++++++++++++++----- test/utils/test-utils.ts | 10 ++++- 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index a9a73e164f..a6ccf314d9 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -583,7 +583,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return state; } - // traverse space tree with DFS calling fn on each space including the given root one + // traverse space tree with DFS calling fn on each space including the given root one, + // if includeRooms is true then fn will be called on each leaf room, if it is present in multiple sub-spaces + // then fn will be called with it multiple times. public traverseSpace( spaceId: string, fn: (roomId: string) => void, diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 60960fd5cf..5528fff66d 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -18,7 +18,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import "../skinned-sdk"; // Must be first for skinning to work import SpaceStore from "../../src/stores/SpaceStore"; -import { setupAsyncStoreWithClient } from "../utils/test-utils"; +import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; import { createTestClient, mkEvent, mkStubRoom } from "../test-utils"; import { EnhancedMap } from "../../src/utils/maps"; import SettingsStore from "../../src/settings/SettingsStore"; @@ -45,8 +45,14 @@ const testUserId = "@test:user"; let rooms = []; +const mkRoom = (roomId: string) => { + const room = mkStubRoom(roomId); + rooms.push(room); + return room; +}; + const mkSpace = (spaceId: string, children: string[] = []) => { - const space = mkStubRoom(spaceId); + const space = mkRoom(spaceId); space.isSpaceRoom.mockReturnValue(true); space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId => mkEvent({ @@ -59,7 +65,6 @@ const mkSpace = (spaceId: string, children: string[] = []) => { ts: Date.now(), }), ))); - rooms.push(space); return space; }; @@ -84,9 +89,8 @@ describe("SpaceStore", () => { } }); }); - afterEach(() => { - // @ts-ignore - store.onNotReady(); + afterEach(async () => { + await resetAsyncStoreWithClient(store); }); describe("static hierarchy resolution tests", () => { @@ -237,8 +241,59 @@ describe("SpaceStore", () => { }); describe("traverseSpace", () => { - test.todo("avoids cycles"); - test.todo("including rooms"); - test.todo("excluding rooms"); + beforeEach(() => { + mkSpace("!a:server", [ + mkSpace("!b:server", [ + mkSpace("!c:server", [ + "!a:server", + mkRoom("!c-child:server").roomId, + mkRoom("!shared-child:server").roomId, + ]).roomId, + mkRoom("!b-child:server").roomId, + ]).roomId, + mkRoom("!a-child:server").roomId, + "!shared-child:server", + ]); + }); + + it("avoids cycles", () => { + const seenMap = new Map(); + store.traverseSpace("!b:server", roomId => { + seenMap.set(roomId, (seenMap.get(roomId) || 0) + 1); + }); + + expect(seenMap.size).toBe(3); + expect(seenMap.get("!a:server")).toBe(1); + expect(seenMap.get("!b:server")).toBe(1); + expect(seenMap.get("!c:server")).toBe(1); + }); + + it("including rooms", () => { + const seenMap = new Map(); + store.traverseSpace("!b:server", roomId => { + seenMap.set(roomId, (seenMap.get(roomId) || 0) + 1); + }, true); + + expect(seenMap.size).toBe(7); + expect(seenMap.get("!a:server")).toBe(1); + expect(seenMap.get("!a-child:server")).toBe(1); + expect(seenMap.get("!b:server")).toBe(1); + expect(seenMap.get("!b-child:server")).toBe(1); + expect(seenMap.get("!c:server")).toBe(1); + expect(seenMap.get("!c-child:server")).toBe(1); + expect(seenMap.get("!shared-child:server")).toBe(2); + }); + + it("excluding rooms", () => { + const seenMap = new Map(); + store.traverseSpace("!b:server", roomId => { + seenMap.set(roomId, (seenMap.get(roomId) || 0) + 1); + }, false); + + expect(seenMap.size).toBe(3); + expect(seenMap.get("!a:server")).toBe(1); + expect(seenMap.get("!b:server")).toBe(1); + expect(seenMap.get("!c:server")).toBe(1); + }); }); }); diff --git a/test/utils/test-utils.ts b/test/utils/test-utils.ts index f86196ffbd..af92987a3d 100644 --- a/test/utils/test-utils.ts +++ b/test/utils/test-utils.ts @@ -15,7 +15,10 @@ limitations under the License. */ import { MatrixClient } from "matrix-js-sdk/src/client"; -import {AsyncStoreWithClient} from "../../src/stores/AsyncStoreWithClient"; +import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient"; + +// These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent +// ready state without needing to wire up a dispatcher and pretend to be a js-sdk client. export const setupAsyncStoreWithClient = async (store: AsyncStoreWithClient, client: MatrixClient) => { // @ts-ignore @@ -23,3 +26,8 @@ export const setupAsyncStoreWithClient = async (store: AsyncStoreWithClient // @ts-ignore await store.onReady(); }; + +export const resetAsyncStoreWithClient = async (store: AsyncStoreWithClient) => { + // @ts-ignore + await store.onNotReady(); +}; From a38419defb1c8ff505713bce35bccab7226ceaf0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Apr 2021 11:20:26 +0100 Subject: [PATCH 176/330] extend space tests some more --- test/stores/SpaceStore-test.ts | 87 +++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 5528fff66d..b27ca8622a 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -128,7 +128,24 @@ describe("SpaceStore", () => { "!company:server", ].sort()); expect(store.invitedSpaces).toStrictEqual([]); - // TODO verify actual tree structure + + expect(store.getChildRooms("!space1:server")).toStrictEqual([]); + expect(store.getChildSpaces("!space1:server")).toStrictEqual([]); + expect(store.getChildRooms("!space2:server")).toStrictEqual([]); + expect(store.getChildSpaces("!space2:server")).toStrictEqual([]); + expect(store.getChildRooms("!company:server")).toStrictEqual([]); + expect(store.getChildSpaces("!company:server")).toStrictEqual([ + client.getRoom("!company_dept1:server"), + client.getRoom("!company_dept2:server"), + ]); + expect(store.getChildRooms("!company_dept1:server")).toStrictEqual([]); + expect(store.getChildSpaces("!company_dept1:server")).toStrictEqual([ + client.getRoom("!company_dept1_group1:server"), + ]); + expect(store.getChildRooms("!company_dept1_group1:server")).toStrictEqual([]); + expect(store.getChildSpaces("!company_dept1_group1:server")).toStrictEqual([]); + expect(store.getChildRooms("!company_dept2:server")).toStrictEqual([]); + expect(store.getChildSpaces("!company_dept2:server")).toStrictEqual([]); }); it("handles a sub-space existing in multiple places in the space tree", async () => { @@ -150,11 +167,28 @@ describe("SpaceStore", () => { "!company:server", ].sort()); expect(store.invitedSpaces).toStrictEqual([]); - // TODO verify actual tree structure + + expect(store.getChildRooms("!space1:server")).toStrictEqual([]); + expect(store.getChildSpaces("!space1:server")).toStrictEqual([]); + expect(store.getChildRooms("!space2:server")).toStrictEqual([]); + expect(store.getChildSpaces("!space2:server")).toStrictEqual([]); + expect(store.getChildRooms("!company:server")).toStrictEqual([]); + expect(store.getChildSpaces("!company:server")).toStrictEqual([ + client.getRoom("!company_dept1:server"), + client.getRoom("!company_dept2:server"), + subspace, + ]); + expect(store.getChildRooms("!company_dept1:server")).toStrictEqual([]); + expect(store.getChildSpaces("!company_dept1:server")).toStrictEqual([ + client.getRoom("!company_dept1_group1:server"), + ]); + expect(store.getChildRooms("!company_dept1_group1:server")).toStrictEqual([]); + expect(store.getChildSpaces("!company_dept1_group1:server")).toStrictEqual([subspace]); + expect(store.getChildRooms("!company_dept2:server")).toStrictEqual([]); + expect(store.getChildSpaces("!company_dept2:server")).toStrictEqual([subspace]); }); - it("handles basic cycles", async () => { - // TODO test all input order permutations + it("handles full cycles", async () => { mkSpace("!a:server", [ mkSpace("!b:server", [ mkSpace("!c:server", [ @@ -166,11 +200,16 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces.map(r => r.roomId)).toStrictEqual(["!a:server"]); expect(store.invitedSpaces).toStrictEqual([]); - // TODO verify actual tree structure + + expect(store.getChildRooms("!a:server")).toStrictEqual([]); + expect(store.getChildSpaces("!a:server")).toStrictEqual([client.getRoom("!b:server")]); + expect(store.getChildRooms("!b:server")).toStrictEqual([]); + expect(store.getChildSpaces("!b:server")).toStrictEqual([client.getRoom("!c:server")]); + expect(store.getChildRooms("!c:server")).toStrictEqual([]); + expect(store.getChildSpaces("!c:server")).toStrictEqual([client.getRoom("!a:server")]); }); - it("handles complex cycles", async () => { - // TODO test all input order permutations + it("handles partial cycles", async () => { mkSpace("!b:server", [ mkSpace("!a:server", [ mkSpace("!c:server", [ @@ -182,11 +221,17 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces.map(r => r.roomId)).toStrictEqual(["!b:server"]); expect(store.invitedSpaces).toStrictEqual([]); - // TODO verify actual tree structure + + expect(store.getChildRooms("!b:server")).toStrictEqual([]); + expect(store.getChildSpaces("!b:server")).toStrictEqual([client.getRoom("!a:server")]); + expect(store.getChildRooms("!a:server")).toStrictEqual([]); + expect(store.getChildSpaces("!a:server")).toStrictEqual([client.getRoom("!c:server")]); + expect(store.getChildRooms("!c:server")).toStrictEqual([]); + expect(store.getChildSpaces("!c:server")).toStrictEqual([client.getRoom("!a:server")]); }); - it("handles really complex cycles", async () => { - // TODO test all input order permutations + it("handles partial cycles with additional spaces coming off them", async () => { + // TODO this test should be failing right now mkSpace("!a:server", [ mkSpace("!b:server", [ mkSpace("!c:server", [ @@ -199,8 +244,18 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces.map(r => r.roomId)).toStrictEqual(["!a:server"]); expect(store.invitedSpaces).toStrictEqual([]); - // TODO verify actual tree structure - // TODO this test should be failing right now + + expect(store.getChildRooms("!a:server")).toStrictEqual([]); + expect(store.getChildSpaces("!a:server")).toStrictEqual([client.getRoom("!b:server")]); + expect(store.getChildRooms("!b:server")).toStrictEqual([]); + expect(store.getChildSpaces("!b:server")).toStrictEqual([client.getRoom("!c:server")]); + expect(store.getChildRooms("!c:server")).toStrictEqual([]); + expect(store.getChildSpaces("!c:server")).toStrictEqual([ + client.getRoom("!a:server"), + client.getRoom("!d:server"), + ]); + expect(store.getChildRooms("!d:server")).toStrictEqual([]); + expect(store.getChildSpaces("!d:server")).toStrictEqual([]); }); describe("home space behaviour", () => { @@ -220,6 +275,10 @@ describe("SpaceStore", () => { test.todo("updates state when space invite is rejected"); }); + describe("active space switching tests", () => { + test.todo("//active space"); + }); + describe("notification state tests", () => { test.todo("//notification states"); }); @@ -228,10 +287,6 @@ describe("SpaceStore", () => { test.todo("//room list filter"); }); - describe("active space switching tests", () => { - test.todo("//active space"); - }); - describe("context switching tests", () => { test.todo("//context switching"); }); From a12cefee8e2f9dc1fef806a600404592f1bfdee7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Apr 2021 12:19:08 +0100 Subject: [PATCH 177/330] Tweak some tests --- src/Unread.js | 2 +- test/test-utils.js | 11 +++++------ test/utils/ShieldUtils-test.js | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Unread.js b/src/Unread.js index ddf225ac64..12c15eb6af 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -45,7 +45,7 @@ export function eventTriggersUnreadCount(ev) { } export function doesRoomHaveUnreadMessages(room) { - const myUserId = MatrixClientPeg.get().credentials.userId; + const myUserId = MatrixClientPeg.get().getUserId(); // get the most recent read receipt sent by our account. // N.B. this is NOT a read marker (RM, aka "read up to marker"), diff --git a/test/test-utils.js b/test/test-utils.js index 33f7e1626e..4fc9bdf377 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -224,7 +224,7 @@ export function mkStubRoom(roomId = null) { hasMembershipState: () => null, getVersion: () => '1', shouldUpgradeToVersion: () => null, - getMyMembership: () => "join", + getMyMembership: jest.fn().mockReturnValue("join"), maySendMessage: jest.fn().mockReturnValue(true), currentState: { getStateEvents: jest.fn(), @@ -233,11 +233,7 @@ export function mkStubRoom(roomId = null) { maySendEvent: jest.fn().mockReturnValue(true), members: [], }, - tags: { - "m.favourite": { - order: 0.5, - }, - }, + tags: {}, setBlacklistUnverifiedDevices: jest.fn(), on: jest.fn(), removeListener: jest.fn(), @@ -245,6 +241,9 @@ export function mkStubRoom(roomId = null) { getAvatarUrl: () => 'mxc://avatar.url/room.png', getMxcAvatarUrl: () => 'mxc://avatar.url/room.png', isSpaceRoom: jest.fn(() => false), + getUnreadNotificationCount: jest.fn(() => 0), + getEventReadUpTo: jest.fn(() => null), + timeline: [], }; } diff --git a/test/utils/ShieldUtils-test.js b/test/utils/ShieldUtils-test.js index 8e3b19c1c4..bea3d26565 100644 --- a/test/utils/ShieldUtils-test.js +++ b/test/utils/ShieldUtils-test.js @@ -128,7 +128,7 @@ describe("shieldStatusForMembership self-trust behaviour", function() { describe("shieldStatusForMembership other-trust behaviour", function() { beforeAll(() => { - DMRoomMap._sharedInstance = { + DMRoomMap.sharedInstance = { getUserIdForRoomId: (roomId) => roomId === "DM" ? "@any:h" : null, }; }); From a3ca48b4dab568f32062924c14264c1502d47a3a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Apr 2021 12:19:38 +0100 Subject: [PATCH 178/330] Write more space store tests --- test/stores/SpaceStore-test.ts | 90 +++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index b27ca8622a..8ff671f78f 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -19,9 +19,11 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import "../skinned-sdk"; // Must be first for skinning to work import SpaceStore from "../../src/stores/SpaceStore"; import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; -import { createTestClient, mkEvent, mkStubRoom } from "../test-utils"; +import { mkEvent, mkStubRoom, stubClient } from "../test-utils"; import { EnhancedMap } from "../../src/utils/maps"; import SettingsStore from "../../src/settings/SettingsStore"; +import DMRoomMap from "../../src/utils/DMRoomMap"; +import { MatrixClientPeg } from "../../src/MatrixClientPeg"; type MatrixEvent = any; // importing from js-sdk upsets things @@ -71,9 +73,14 @@ const mkSpace = (spaceId: string, children: string[] = []) => { const getValue = jest.fn(); SettingsStore.getValue = getValue; +const getUserIdForRoomId = jest.fn(); +// @ts-ignore +DMRoomMap.sharedInstance = { getUserIdForRoomId }; + describe("SpaceStore", () => { + stubClient(); const store = SpaceStore.instance; - const client = createTestClient(); + const client = MatrixClientPeg.get(); const run = async () => { client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId)); @@ -259,11 +266,80 @@ describe("SpaceStore", () => { }); describe("home space behaviour", () => { - test.todo("home space contains orphaned rooms"); - test.todo("home space contains favourites"); - test.todo("home space contains dm rooms"); - test.todo("home space contains invites"); - test.todo("home space contains invites even if they are also shown in a space"); + const fav1 = "!fav1:server"; + const fav2 = "!fav2:server"; + const fav3 = "!fav3:server"; + const dm1 = "!dm1:server"; + const dm1Partner = "@dm1Partner:server"; + const dm2 = "!dm2:server"; + const dm2Partner = "@dm2Partner:server"; + const dm3 = "!dm3:server"; + const dm3Partner = "@dm3Partner:server"; + const orphan1 = "!orphan1:server"; + const orphan2 = "!orphan2:server"; + const invite1 = "!invite1:server"; + const invite2 = "!invite2:server"; + const spaceRoom1 = "!spaceRoom1:server"; + const space1 = "!space1:server"; + const space2 = "!space2:server"; + const space3 = "!space3:server"; + + beforeEach(async () => { + [fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, spaceRoom1].forEach(mkRoom); + mkSpace(space1, [fav1, spaceRoom1]); + mkSpace(space2, [fav2, fav3]); + mkSpace(space3, [invite2]); + + [fav1, fav2, fav3].forEach(roomId => { + client.getRoom(roomId).tags = { + "m.favourite": { + order: 0.5, + }, + }; + }); + + [invite1, invite2].forEach(roomId => { + client.getRoom(roomId).getMyMembership.mockReturnValue("invite"); + }); + + getUserIdForRoomId.mockImplementation(roomId => { + return { + [dm1]: dm1Partner, + [dm2]: dm2Partner, + [dm3]: dm3Partner, + }[roomId]; + }); + await run(); + }); + + it("home space contains orphaned rooms", () => { + expect(store.getSpaceFilteredRoomIds(null).has(orphan1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(null).has(orphan2)).toBeTruthy(); + }); + + it("home space contains favourites", () => { + expect(store.getSpaceFilteredRoomIds(null).has(fav1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(null).has(fav2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(null).has(fav3)).toBeTruthy(); + }); + + it("home space contains dm rooms", () => { + expect(store.getSpaceFilteredRoomIds(null).has(dm1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(null).has(dm2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(null).has(dm3)).toBeTruthy(); + }); + + it("home space contains invites", () => { + expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy(); + }); + + it("home space contains invites even if they are also shown in a space", () => { + expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy(); + }); + + it("home space does not contain rooms/low priority from rooms within spaces", () => { + expect(store.getSpaceFilteredRoomIds(null).has(spaceRoom1)).toBeFalsy(); + }); }); }); From 7b215cd2756a8f260a34f31b19ee387c73931548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 23 Apr 2021 14:14:51 +0200 Subject: [PATCH 179/330] Use suggested colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 2 +- res/themes/legacy-dark/css/_legacy-dark.scss | 2 +- res/themes/legacy-light/css/_legacy-light.scss | 2 +- res/themes/light/css/_light.scss | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 736a2df30b..f4724a4c90 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -110,7 +110,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #24292f; +$voipcall-plinth-color: #394049; // ******************** diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 9b2365a621..60123d7011 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -107,7 +107,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #f2f5f8; +$voipcall-plinth-color: #394049; // ******************** diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 0956f433b2..17a5c21d8d 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -174,7 +174,7 @@ $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #f2f5f8; +$voipcall-plinth-color: #F4F6FA; // ******************** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 03b945380c..374d391be3 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -165,7 +165,7 @@ $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #dddfe2; +$voipcall-plinth-color: #F4F6FA; // ******************** From c35678c64a8c17ad3e0331261965f5818ddc9c17 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Apr 2021 13:40:16 +0100 Subject: [PATCH 180/330] Add yet more tests --- test/stores/SpaceStore-test.ts | 78 ++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 8ff671f78f..b7f4f7b49d 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -265,7 +265,7 @@ describe("SpaceStore", () => { expect(store.getChildSpaces("!d:server")).toStrictEqual([]); }); - describe("home space behaviour", () => { + describe("test fixture 1", () => { const fav1 = "!fav1:server"; const fav2 = "!fav2:server"; const fav3 = "!fav3:server"; @@ -287,7 +287,7 @@ describe("SpaceStore", () => { beforeEach(async () => { [fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, spaceRoom1].forEach(mkRoom); mkSpace(space1, [fav1, spaceRoom1]); - mkSpace(space2, [fav2, fav3]); + mkSpace(space2, [fav1, fav2, fav3, spaceRoom1]); mkSpace(space3, [invite2]); [fav1, fav2, fav3].forEach(roomId => { @@ -340,6 +340,25 @@ describe("SpaceStore", () => { it("home space does not contain rooms/low priority from rooms within spaces", () => { expect(store.getSpaceFilteredRoomIds(null).has(spaceRoom1)).toBeFalsy(); }); + + it("space contains child rooms", () => { + const space = client.getRoom(space1); + expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space).has(spaceRoom1)).toBeTruthy(); + }); + + it("space contains child favourites", () => { + const space = client.getRoom(space2); + expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space).has(fav2)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space).has(fav3)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space).has(spaceRoom1)).toBeTruthy(); + }); + + it("space contains child invites", () => { + const space = client.getRoom(space3); + expect(store.getSpaceFilteredRoomIds(space).has(invite2)).toBeTruthy(); + }); }); }); @@ -368,7 +387,10 @@ describe("SpaceStore", () => { }); describe("space auto switching tests", () => { - test.todo("//auto pick space for a room"); + // it("no switch required, room is in target space"); + // it("switch to canonical parent space for room"); + // it("switch to first containing space for room"); + // it("switch to home for orphaned room"); }); describe("traverseSpace", () => { @@ -388,43 +410,37 @@ describe("SpaceStore", () => { }); it("avoids cycles", () => { - const seenMap = new Map(); - store.traverseSpace("!b:server", roomId => { - seenMap.set(roomId, (seenMap.get(roomId) || 0) + 1); - }); + const fn = jest.fn(); + store.traverseSpace("!b:server", fn); - expect(seenMap.size).toBe(3); - expect(seenMap.get("!a:server")).toBe(1); - expect(seenMap.get("!b:server")).toBe(1); - expect(seenMap.get("!c:server")).toBe(1); + expect(fn).toBeCalledTimes(3); + expect(fn).toBeCalledWith("!a:server"); + expect(fn).toBeCalledWith("!b:server"); + expect(fn).toBeCalledWith("!c:server"); }); it("including rooms", () => { - const seenMap = new Map(); - store.traverseSpace("!b:server", roomId => { - seenMap.set(roomId, (seenMap.get(roomId) || 0) + 1); - }, true); + const fn = jest.fn(); + store.traverseSpace("!b:server", fn, true); - expect(seenMap.size).toBe(7); - expect(seenMap.get("!a:server")).toBe(1); - expect(seenMap.get("!a-child:server")).toBe(1); - expect(seenMap.get("!b:server")).toBe(1); - expect(seenMap.get("!b-child:server")).toBe(1); - expect(seenMap.get("!c:server")).toBe(1); - expect(seenMap.get("!c-child:server")).toBe(1); - expect(seenMap.get("!shared-child:server")).toBe(2); + expect(fn).toBeCalledTimes(8); // twice for shared-child + expect(fn).toBeCalledWith("!a:server"); + expect(fn).toBeCalledWith("!a-child:server"); + expect(fn).toBeCalledWith("!b:server"); + expect(fn).toBeCalledWith("!b-child:server"); + expect(fn).toBeCalledWith("!c:server"); + expect(fn).toBeCalledWith("!c-child:server"); + expect(fn).toBeCalledWith("!shared-child:server"); }); it("excluding rooms", () => { - const seenMap = new Map(); - store.traverseSpace("!b:server", roomId => { - seenMap.set(roomId, (seenMap.get(roomId) || 0) + 1); - }, false); + const fn = jest.fn(); + store.traverseSpace("!b:server", fn, false); - expect(seenMap.size).toBe(3); - expect(seenMap.get("!a:server")).toBe(1); - expect(seenMap.get("!b:server")).toBe(1); - expect(seenMap.get("!c:server")).toBe(1); + expect(fn).toBeCalledTimes(3); + expect(fn).toBeCalledWith("!a:server"); + expect(fn).toBeCalledWith("!b:server"); + expect(fn).toBeCalledWith("!c:server"); }); }); }); From 320ff7b87004a73c9ee5fd0b3b27882aa69d42c2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Apr 2021 13:41:42 +0100 Subject: [PATCH 181/330] Fix invites relating to a space not showing in the space --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 982c3d5d9f..d66d0c008c 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -195,7 +195,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via); return sortBy(childEvents, getOrder) .map(ev => this.matrixClient.getRoom(ev.getStateKey())) - .filter(room => room?.getMyMembership() === "join") || []; + .filter(room => room?.getMyMembership() === "join" || room?.getMyMembership() === "invite") || []; } public getChildRooms(spaceId: string): Room[] { From 2d17ba445a45e9807ed8d7c6b78e50bd5b635a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 23 Apr 2021 15:33:10 +0200 Subject: [PATCH 182/330] Increase drop shadow alpha to 0.45 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallView.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 27473851c8..a71bc1db46 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -41,7 +41,7 @@ limitations under the License. padding-bottom: 8px; margin-top: 10px; background-color: $voipcall-plinth-color; - box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); + box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.45); border-radius: 8px; .mx_CallView_voice { From dc3d05bc889259d524ae801da4a71429719ff41c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 23 Apr 2021 14:39:39 +0100 Subject: [PATCH 183/330] Test for asserted identity This is out first CallHandler test(!) Switches react-sdk to use createCall on the client object so we can stub this out in the test. Add a bunch more stubs to the test client. There's more stuff in this test that has scope to be used more widely, like waiting for a certain dispatch and mocking out rooms with particular sets of users in them: we could consider moving these out to test utils if we wanted. --- src/CallHandler.tsx | 7 +- src/utils/DMRoomMap.ts | 9 ++ test/CallHandler-test.ts | 214 +++++++++++++++++++++++++++++++++++++++ test/test-utils.js | 5 + 4 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 test/CallHandler-test.ts diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 1686a671ed..605e5a4a89 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -59,7 +59,6 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; -import { createNewMatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; @@ -395,7 +394,7 @@ export default class CallHandler { // We don't allow placing more than one call per room, but that doesn't mean there // can't be more than one, eg. in a glare situation. This checks that the given call // is the call we consider 'the' call for its room. - const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); + const mappedRoomId = this.roomIdForCall(call); const callForThisRoom = this.getCallForRoom(mappedRoomId); return callForThisRoom && call.callId === callForThisRoom.callId; @@ -539,7 +538,7 @@ export default class CallHandler { // on a call with can cause you to send a room invite to someone. await ensureDMExists(MatrixClientPeg.get(), newNativeAssertedIdentity); - const newMappedRoomId = CallHandler.sharedInstance().roomIdForCall(call); + const newMappedRoomId = this.roomIdForCall(call); console.log(`Old room ID: ${mappedRoomId}, new room ID: ${newMappedRoomId}`); if (newMappedRoomId !== mappedRoomId) { this.removeCallForRoom(mappedRoomId); @@ -691,7 +690,7 @@ export default class CallHandler { const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now(); console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms"); - const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId); + const call = MatrixClientPeg.get().createCall(mappedRoomId); this.calls.set(roomId, call); if (transferee) { diff --git a/src/utils/DMRoomMap.ts b/src/utils/DMRoomMap.ts index e49b74c380..b166674043 100644 --- a/src/utils/DMRoomMap.ts +++ b/src/utils/DMRoomMap.ts @@ -55,6 +55,15 @@ export default class DMRoomMap { return DMRoomMap.sharedInstance; } + /** + * Set the shared instance to the instance supplied + * Used by tests + * @param inst the new shared instance + */ + public static setShared(inst: DMRoomMap) { + DMRoomMap.sharedInstance = inst; + } + /** * Returns a shared instance of the class * that uses the singleton matrix client diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts new file mode 100644 index 0000000000..04dec8979f --- /dev/null +++ b/test/CallHandler-test.ts @@ -0,0 +1,214 @@ +/* +Copyright 2015-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 './skinned-sdk'; + +import CallHandler, { PlaceCallType } from '../src/CallHandler'; +import { stubClient, mkStubRoom } from './test-utils'; +import { MatrixClientPeg } from '../src/MatrixClientPeg'; +import dis from '../src/dispatcher/dispatcher'; +import { CallEvent, CallState } from 'matrix-js-sdk/src/webrtc/call'; +import DMRoomMap from '../src/utils/DMRoomMap'; +import EventEmitter from 'events'; +import { Action } from '../src/dispatcher/actions'; +import SdkConfig from '../src/SdkConfig'; + +const REAL_ROOM_ID = '$room1:example.org'; +const MAPPED_ROOM_ID = '$room2:example.org'; +const MAPPED_ROOM_ID_2 = '$room3:example.org'; + +function mkStubDM(roomId, userId) { + const room = mkStubRoom(roomId); + room.getJoinedMembers = jest.fn().mockReturnValue([ + { + userId: '@me:example.org', + name: 'Member', + rawDisplayName: 'Member', + roomId: roomId, + membership: 'join', + getAvatarUrl: () => 'mxc://avatar.url/image.png', + getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', + }, + { + userId: userId, + name: 'Member', + rawDisplayName: 'Member', + roomId: roomId, + membership: 'join', + getAvatarUrl: () => 'mxc://avatar.url/image.png', + getMxcAvatarUrl: () => 'mxc://avatar.url/image.png', + }, + ]); + room.currentState.getMembers = room.getJoinedMembers; + + return room; +} + +class FakeCall extends EventEmitter { + roomId: string; + callId = "fake call id"; + + constructor(roomId) { + super(); + + this.roomId = roomId; + } + + setRemoteOnHold() {} + setRemoteAudioElement() {} + + placeVoiceCall() { + this.emit(CallEvent.State, CallState.Connected, null); + } +} + +describe('CallHandler', () => { + let dmRoomMap; + let callHandler; + let audioElement; + let fakeCall; + + beforeEach(() => { + stubClient(); + MatrixClientPeg.get().createCall = roomId => { + if (fakeCall && fakeCall.roomId !== roomId) { + throw new Error("Only one call is supported!"); + } + fakeCall = new FakeCall(roomId); + return fakeCall; + }; + + callHandler = new CallHandler(); + callHandler.start(); + + dmRoomMap = { + getUserIdForRoomId: roomId => { + if (roomId === REAL_ROOM_ID) { + return '@user1:example.org'; + } else if (roomId === MAPPED_ROOM_ID) { + return '@user2:example.org'; + } else if (roomId === MAPPED_ROOM_ID_2) { + return '@user3:example.org'; + } else { + return null; + } + }, + getDMRoomsForUserId: userId => { + if (userId === '@user2:example.org') { + return [MAPPED_ROOM_ID]; + } else if (userId === '@user3:example.org') { + return [MAPPED_ROOM_ID_2]; + } else { + return []; + } + }, + }; + DMRoomMap.setShared(dmRoomMap); + + audioElement = document.createElement('audio'); + audioElement.id = "remoteAudio"; + document.body.appendChild(audioElement); + }); + + afterEach(() => { + callHandler.stop(); + DMRoomMap.setShared(null); + // @ts-ignore + window.mxCallHandler = null; + MatrixClientPeg.unset(); + + document.body.removeChild(audioElement); + SdkConfig.unset(); + }); + + it('should move calls between rooms when remote asserted identity changes', async () => { + const realRoom = mkStubDM(REAL_ROOM_ID, '@user1:example.org'); + const mappedRoom = mkStubDM(MAPPED_ROOM_ID, '@user2:example.org'); + const mappedRoom2 = mkStubDM(MAPPED_ROOM_ID_2, '@user3:example.org'); + + MatrixClientPeg.get().getRoom = roomId => { + switch (roomId) { + case REAL_ROOM_ID: + return realRoom; + case MAPPED_ROOM_ID: + return mappedRoom; + case MAPPED_ROOM_ID_2: + return mappedRoom2; + } + }; + + dis.dispatch({ + action: 'place_call', + type: PlaceCallType.Voice, + room_id: REAL_ROOM_ID, + }, true); + + let dispatchHandle; + // wait for the call to be set up + await new Promise(resolve => { + dispatchHandle = dis.register(payload => { + if (payload.action === 'call_state') { + resolve(); + } + }); + }); + dis.unregister(dispatchHandle); + + // should start off in the actual room ID it's in at the protocol level + expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBe(fakeCall); + + let callRoomChangeEventCount = 0; + const roomChangePromise = new Promise(resolve => { + dispatchHandle = dis.register(payload => { + if (payload.action === Action.CallChangeRoom) { + ++callRoomChangeEventCount; + resolve(); + } + }); + }); + + // Now emit an asserted identity for user2: this should be ignored + // because we haven't set the config option to obey asserted identity + fakeCall.getRemoteAssertedIdentity = jest.fn().mockReturnValue({ + id: "@user2:example.org", + }); + fakeCall.emit(CallEvent.AssertedIdentityChanged); + + // Now set the config option + SdkConfig.put({ + voipObeyAssertedIdentity: true, + }); + + // ...and send another asserted identity event for a different user + fakeCall.getRemoteAssertedIdentity = jest.fn().mockReturnValue({ + id: "@user3:example.org", + }); + fakeCall.emit(CallEvent.AssertedIdentityChanged); + + await roomChangePromise; + dis.unregister(dispatchHandle); + + // If everything's gone well, we should have seen only one room change + // event and the call should now be in user 3's room. + // If it's not obeying any, the call will still be in REAL_ROOM_ID. + // If it incorrectly obeyed both asserted identity changes, either it will + // have just processed one and the call will be in the wrong room, or we'll + // have seen two room change dispatches. + expect(callRoomChangeEventCount).toEqual(1); + expect(callHandler.getCallForRoom(REAL_ROOM_ID)).toBeNull(); + expect(callHandler.getCallForRoom(MAPPED_ROOM_ID_2)).toBe(fakeCall); + }); +}); diff --git a/test/test-utils.js b/test/test-utils.js index d259fcb95f..d9332580f7 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -64,6 +64,11 @@ export function createTestClient() { getRoomIdForAlias: jest.fn().mockResolvedValue(undefined), getRoomDirectoryVisibility: jest.fn().mockResolvedValue(undefined), getProfileInfo: jest.fn().mockResolvedValue({}), + getThirdpartyProtocols: jest.fn().mockResolvedValue({}), + getClientWellKnown: jest.fn().mockReturnValue(null), + supportsVoip: jest.fn().mockReturnValue(true), + getTurnServersExpiry: jest.fn().mockReturnValue(2^32), + getThirdpartyUser: jest.fn().mockResolvedValue([]), getAccountData: (type) => { return mkEvent({ type, From 4446022327b6bcddde9a0be1c8f9dc64341713d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Apr 2021 14:45:22 +0100 Subject: [PATCH 184/330] Add automatic space switching tests --- test/stores/SpaceStore-test.ts | 60 +++++++++++++++++++++++++++++++--- test/test-utils.js | 4 +++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index b7f4f7b49d..4ee6111f88 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -24,6 +24,7 @@ import { EnhancedMap } from "../../src/utils/maps"; import SettingsStore from "../../src/settings/SettingsStore"; import DMRoomMap from "../../src/utils/DMRoomMap"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +import defaultDispatcher from "../../src/dispatcher/dispatcher"; type MatrixEvent = any; // importing from js-sdk upsets things @@ -49,6 +50,7 @@ let rooms = []; const mkRoom = (roomId: string) => { const room = mkStubRoom(roomId); + room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([])); rooms.push(room); return room; }; @@ -82,6 +84,8 @@ describe("SpaceStore", () => { const store = SpaceStore.instance; const client = MatrixClientPeg.get(); + const viewRoom = roomId => defaultDispatcher.dispatch({ action: "view_room", room_id: roomId }, true); + const run = async () => { client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId)); await setupAsyncStoreWithClient(store, client); @@ -387,10 +391,58 @@ describe("SpaceStore", () => { }); describe("space auto switching tests", () => { - // it("no switch required, room is in target space"); - // it("switch to canonical parent space for room"); - // it("switch to first containing space for room"); - // it("switch to home for orphaned room"); + const space1 = "!space1:server"; + const space2 = "!space2:server"; + const room1 = "!room1:server"; // in space 1 & 2 + const room2 = "!room2:server"; // in space 1 & 2 (canonical) + const orphan1 = "!orphan:server"; + + beforeEach(async () => { + [room1, room2, orphan1].forEach(mkRoom); + mkSpace(space1, [room1, room2]); + mkSpace(space2, [room1, room2]); + + client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([ + mkEvent({ + event: true, + type: EventType.SpaceParent, + room: room2, + user: testUserId, + skey: space2, + content: { via: [], canonical: true }, + ts: Date.now(), + }), + ])); + await run(); + }); + + it("no switch required, room is in current space", async () => { + viewRoom(room1); + await store.setActiveSpace(client.getRoom(space1), false); + viewRoom(room2); + expect(store.activeSpace).toBe(client.getRoom(space1)); + }); + + it("switch to canonical parent space for room", async () => { + viewRoom(room1); + await store.setActiveSpace(null, false); + viewRoom(room2); + expect(store.activeSpace).toBe(client.getRoom(space2)); + }); + + it("switch to first containing space for room", async () => { + viewRoom(room2); + await store.setActiveSpace(null, false); + viewRoom(room1); + expect(store.activeSpace).toBe(client.getRoom(space1)); + }); + + it("switch to home for orphaned room", async () => { + viewRoom(room1); + await store.setActiveSpace(client.getRoom(space1), false); + viewRoom(orphan1); + expect(store.activeSpace).toBeNull(); + }); }); describe("traverseSpace", () => { diff --git a/test/test-utils.js b/test/test-utils.js index 4fc9bdf377..d344a7e9b1 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -79,6 +79,10 @@ export function createTestClient() { generateClientSecret: () => "t35tcl1Ent5ECr3T", isGuest: () => false, isCryptoEnabled: () => false, + getSpaceSummary: jest.fn().mockReturnValue({ + rooms: [], + events: [], + }), }; } From f5ab75cfddf0834821e5a32ac5236a9d6d0ede3c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Apr 2021 14:45:34 +0100 Subject: [PATCH 185/330] Fix automatic space switching behaviour to prioritise canonical parents --- src/stores/SpaceStore.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d66d0c008c..a72b270e0b 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -540,15 +540,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // as this is not helpful and can create loops of rooms/space switching if (!room || payload.context_switch) break; - // persist last viewed room from a space - if (room.isSpaceRoom()) { this.setActiveSpace(room); } else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(room.roomId)) { - // TODO maybe reverse these first 2 clauses once space panel active is fixed - let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId)); + let parent = this.getCanonicalParent(room.roomId); if (!parent) { - parent = this.getCanonicalParent(room.roomId); + parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId)); } if (!parent) { const parents = Array.from(this.parentMap.get(room.roomId) || []); From 024cf7f66c7eb27cc354b4d56ac22ae1615c1c34 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 22 Apr 2021 11:44:33 -0400 Subject: [PATCH 186/330] Cut off long names in add rooms to space dialog Signed-off-by: Robin Townsend --- res/css/views/dialogs/_AddExistingToSpaceDialog.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 80ad4d6c0e..826bb3f7e9 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -148,6 +148,10 @@ limitations under the License. font-size: $font-15px; line-height: 30px; flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; } .mx_FormButton { From 0f84b3dff315b0d19ac99a2685eaf56e81d41ea4 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 22 Apr 2021 11:46:31 -0400 Subject: [PATCH 187/330] Align checkboxes with names in add rooms to space dialog Signed-off-by: Robin Townsend --- res/css/views/dialogs/_AddExistingToSpaceDialog.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 826bb3f7e9..f288241e04 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -154,6 +154,10 @@ limitations under the License. margin-right: 12px; } + .mx_Checkbox { + align-items: center; + } + .mx_FormButton { min-width: 92px; font-weight: normal; From 3d7842d6964af7c2698abc706b5e3bafee584d5f Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 22 Apr 2021 11:46:56 -0400 Subject: [PATCH 188/330] Remove old FormButton CSS Signed-off-by: Robin Townsend --- res/css/views/dialogs/_AddExistingToSpaceDialog.scss | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index f288241e04..247df52b4a 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -157,12 +157,6 @@ limitations under the License. .mx_Checkbox { align-items: center; } - - .mx_FormButton { - min-width: 92px; - font-weight: normal; - box-sizing: border-box; - } } } @@ -200,8 +194,4 @@ limitations under the License. padding: 0; } } - - .mx_FormButton { - padding: 8px 22px; - } } From 31a28b1a9e486e447931c5975926cbdce72dc959 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 1 Apr 2021 13:26:36 +0100 Subject: [PATCH 189/330] Update extensions for some files with types This migrates one bucket of files using some amount of Flow typing to mark them as TypeScript instead. The remaining type errors are fixed in subsequent commits. --- src/{ScalarAuthClient.js => ScalarAuthClient.ts} | 0 .../{ManageEventIndexDialog.js => ManageEventIndexDialog.tsx} | 0 .../structures/auth/{SoftLogout.js => SoftLogout.tsx} | 0 src/components/views/rooms/{EventTile.js => EventTile.tsx} | 0 .../views/rooms/{MessageComposer.js => MessageComposer.tsx} | 0 .../rooms/{ThirdPartyMemberInfo.js => ThirdPartyMemberInfo.tsx} | 0 .../settings/{E2eAdvancedPanel.js => E2eAdvancedPanel.tsx} | 0 .../views/settings/{EventIndexPanel.js => EventIndexPanel.tsx} | 0 .../views/settings/{SetIdServer.js => SetIdServer.tsx} | 0 .../room/{RolesRoomSettingsTab.js => RolesRoomSettingsTab.tsx} | 0 .../{SecurityRoomSettingsTab.js => SecurityRoomSettingsTab.tsx} | 0 .../user/{HelpUserSettingsTab.js => HelpUserSettingsTab.tsx} | 2 +- .../{MjolnirUserSettingsTab.js => MjolnirUserSettingsTab.tsx} | 0 ...erencesUserSettingsTab.js => PreferencesUserSettingsTab.tsx} | 0 src/indexing/{EventIndexPeg.js => EventIndexPeg.ts} | 0 src/mjolnir/{BanList.js => BanList.ts} | 0 src/mjolnir/{ListRule.js => ListRule.ts} | 0 src/mjolnir/{Mjolnir.js => Mjolnir.ts} | 0 src/stores/{TypingStore.js => TypingStore.ts} | 0 src/stores/{WidgetEchoStore.js => WidgetEchoStore.ts} | 0 src/utils/{AutoDiscoveryUtils.js => AutoDiscoveryUtils.tsx} | 0 src/utils/{MatrixGlob.js => MatrixGlob.ts} | 0 src/utils/{StorageManager.js => StorageManager.ts} | 0 src/utils/{TypeUtils.js => TypeUtils.ts} | 0 ...ntPermalinkConstructor.js => ElementPermalinkConstructor.ts} | 0 .../{PermalinkConstructor.js => PermalinkConstructor.ts} | 0 src/utils/permalinks/{Permalinks.js => Permalinks.ts} | 0 ...{SpecPermalinkConstructor.js => SpecPermalinkConstructor.ts} | 0 28 files changed, 1 insertion(+), 1 deletion(-) rename src/{ScalarAuthClient.js => ScalarAuthClient.ts} (100%) rename src/async-components/views/dialogs/eventindex/{ManageEventIndexDialog.js => ManageEventIndexDialog.tsx} (100%) rename src/components/structures/auth/{SoftLogout.js => SoftLogout.tsx} (100%) rename src/components/views/rooms/{EventTile.js => EventTile.tsx} (100%) rename src/components/views/rooms/{MessageComposer.js => MessageComposer.tsx} (100%) rename src/components/views/rooms/{ThirdPartyMemberInfo.js => ThirdPartyMemberInfo.tsx} (100%) rename src/components/views/settings/{E2eAdvancedPanel.js => E2eAdvancedPanel.tsx} (100%) rename src/components/views/settings/{EventIndexPanel.js => EventIndexPanel.tsx} (100%) rename src/components/views/settings/{SetIdServer.js => SetIdServer.tsx} (100%) rename src/components/views/settings/tabs/room/{RolesRoomSettingsTab.js => RolesRoomSettingsTab.tsx} (100%) rename src/components/views/settings/tabs/room/{SecurityRoomSettingsTab.js => SecurityRoomSettingsTab.tsx} (100%) rename src/components/views/settings/tabs/user/{HelpUserSettingsTab.js => HelpUserSettingsTab.tsx} (99%) rename src/components/views/settings/tabs/user/{MjolnirUserSettingsTab.js => MjolnirUserSettingsTab.tsx} (100%) rename src/components/views/settings/tabs/user/{PreferencesUserSettingsTab.js => PreferencesUserSettingsTab.tsx} (100%) rename src/indexing/{EventIndexPeg.js => EventIndexPeg.ts} (100%) rename src/mjolnir/{BanList.js => BanList.ts} (100%) rename src/mjolnir/{ListRule.js => ListRule.ts} (100%) rename src/mjolnir/{Mjolnir.js => Mjolnir.ts} (100%) rename src/stores/{TypingStore.js => TypingStore.ts} (100%) rename src/stores/{WidgetEchoStore.js => WidgetEchoStore.ts} (100%) rename src/utils/{AutoDiscoveryUtils.js => AutoDiscoveryUtils.tsx} (100%) rename src/utils/{MatrixGlob.js => MatrixGlob.ts} (100%) rename src/utils/{StorageManager.js => StorageManager.ts} (100%) rename src/utils/{TypeUtils.js => TypeUtils.ts} (100%) rename src/utils/permalinks/{ElementPermalinkConstructor.js => ElementPermalinkConstructor.ts} (100%) rename src/utils/permalinks/{PermalinkConstructor.js => PermalinkConstructor.ts} (100%) rename src/utils/permalinks/{Permalinks.js => Permalinks.ts} (100%) rename src/utils/permalinks/{SpecPermalinkConstructor.js => SpecPermalinkConstructor.ts} (100%) diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.ts similarity index 100% rename from src/ScalarAuthClient.js rename to src/ScalarAuthClient.ts diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx similarity index 100% rename from src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js rename to src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx diff --git a/src/components/structures/auth/SoftLogout.js b/src/components/structures/auth/SoftLogout.tsx similarity index 100% rename from src/components/structures/auth/SoftLogout.js rename to src/components/structures/auth/SoftLogout.tsx diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.tsx similarity index 100% rename from src/components/views/rooms/EventTile.js rename to src/components/views/rooms/EventTile.tsx diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.tsx similarity index 100% rename from src/components/views/rooms/MessageComposer.js rename to src/components/views/rooms/MessageComposer.tsx diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.js b/src/components/views/rooms/ThirdPartyMemberInfo.tsx similarity index 100% rename from src/components/views/rooms/ThirdPartyMemberInfo.js rename to src/components/views/rooms/ThirdPartyMemberInfo.tsx diff --git a/src/components/views/settings/E2eAdvancedPanel.js b/src/components/views/settings/E2eAdvancedPanel.tsx similarity index 100% rename from src/components/views/settings/E2eAdvancedPanel.js rename to src/components/views/settings/E2eAdvancedPanel.tsx diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.tsx similarity index 100% rename from src/components/views/settings/EventIndexPanel.js rename to src/components/views/settings/EventIndexPanel.tsx diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.tsx similarity index 100% rename from src/components/views/settings/SetIdServer.js rename to src/components/views/settings/SetIdServer.tsx diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx similarity index 100% rename from src/components/views/settings/tabs/room/RolesRoomSettingsTab.js rename to src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx similarity index 100% rename from src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js rename to src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx similarity index 99% rename from src/components/views/settings/tabs/user/HelpUserSettingsTab.js rename to src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index e16ee686f5..05f14cebc8 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -23,7 +23,7 @@ import AccessibleButton from "../../../elements/AccessibleButton"; import SdkConfig from "../../../../../SdkConfig"; import createRoom from "../../../../../createRoom"; import Modal from "../../../../../Modal"; -import * as sdk from "../../../../../"; +import * as sdk from "../../../../.."; import PlatformPeg from "../../../../../PlatformPeg"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import UpdateCheckButton from "../../UpdateCheckButton"; diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx similarity index 100% rename from src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js rename to src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx similarity index 100% rename from src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js rename to src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.ts similarity index 100% rename from src/indexing/EventIndexPeg.js rename to src/indexing/EventIndexPeg.ts diff --git a/src/mjolnir/BanList.js b/src/mjolnir/BanList.ts similarity index 100% rename from src/mjolnir/BanList.js rename to src/mjolnir/BanList.ts diff --git a/src/mjolnir/ListRule.js b/src/mjolnir/ListRule.ts similarity index 100% rename from src/mjolnir/ListRule.js rename to src/mjolnir/ListRule.ts diff --git a/src/mjolnir/Mjolnir.js b/src/mjolnir/Mjolnir.ts similarity index 100% rename from src/mjolnir/Mjolnir.js rename to src/mjolnir/Mjolnir.ts diff --git a/src/stores/TypingStore.js b/src/stores/TypingStore.ts similarity index 100% rename from src/stores/TypingStore.js rename to src/stores/TypingStore.ts diff --git a/src/stores/WidgetEchoStore.js b/src/stores/WidgetEchoStore.ts similarity index 100% rename from src/stores/WidgetEchoStore.js rename to src/stores/WidgetEchoStore.ts diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.tsx similarity index 100% rename from src/utils/AutoDiscoveryUtils.js rename to src/utils/AutoDiscoveryUtils.tsx diff --git a/src/utils/MatrixGlob.js b/src/utils/MatrixGlob.ts similarity index 100% rename from src/utils/MatrixGlob.js rename to src/utils/MatrixGlob.ts diff --git a/src/utils/StorageManager.js b/src/utils/StorageManager.ts similarity index 100% rename from src/utils/StorageManager.js rename to src/utils/StorageManager.ts diff --git a/src/utils/TypeUtils.js b/src/utils/TypeUtils.ts similarity index 100% rename from src/utils/TypeUtils.js rename to src/utils/TypeUtils.ts diff --git a/src/utils/permalinks/ElementPermalinkConstructor.js b/src/utils/permalinks/ElementPermalinkConstructor.ts similarity index 100% rename from src/utils/permalinks/ElementPermalinkConstructor.js rename to src/utils/permalinks/ElementPermalinkConstructor.ts diff --git a/src/utils/permalinks/PermalinkConstructor.js b/src/utils/permalinks/PermalinkConstructor.ts similarity index 100% rename from src/utils/permalinks/PermalinkConstructor.js rename to src/utils/permalinks/PermalinkConstructor.ts diff --git a/src/utils/permalinks/Permalinks.js b/src/utils/permalinks/Permalinks.ts similarity index 100% rename from src/utils/permalinks/Permalinks.js rename to src/utils/permalinks/Permalinks.ts diff --git a/src/utils/permalinks/SpecPermalinkConstructor.js b/src/utils/permalinks/SpecPermalinkConstructor.ts similarity index 100% rename from src/utils/permalinks/SpecPermalinkConstructor.js rename to src/utils/permalinks/SpecPermalinkConstructor.ts From 9c3bd12830e7ba1775202649f7030e590d6847be Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 23 Apr 2021 15:47:01 +0100 Subject: [PATCH 190/330] Copy indent rule to TS as well --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index 99695b7a03..09fd8de74f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,6 +30,7 @@ module.exports = { "@typescript-eslint/ban-ts-comment": "off", "quotes": "off", + "indent": "off", "no-extra-boolean-cast": "off", }, }], From dd2a1d063a1d6360887b875b5d4d4c50a97cca9b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 23 Apr 2021 16:14:55 +0100 Subject: [PATCH 191/330] Write tests for spaces context switching behavious --- test/stores/SpaceStore-test.ts | 119 ++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 31 deletions(-) diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 4ee6111f88..34e0186e90 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -79,6 +79,25 @@ const getUserIdForRoomId = jest.fn(); // @ts-ignore DMRoomMap.sharedInstance = { getUserIdForRoomId }; +const fav1 = "!fav1:server"; +const fav2 = "!fav2:server"; +const fav3 = "!fav3:server"; +const dm1 = "!dm1:server"; +const dm1Partner = "@dm1Partner:server"; +const dm2 = "!dm2:server"; +const dm2Partner = "@dm2Partner:server"; +const dm3 = "!dm3:server"; +const dm3Partner = "@dm3Partner:server"; +const orphan1 = "!orphan1:server"; +const orphan2 = "!orphan2:server"; +const invite1 = "!invite1:server"; +const invite2 = "!invite2:server"; +const room1 = "!room1:server"; +const room2 = "!room2:server"; +const space1 = "!space1:server"; +const space2 = "!space2:server"; +const space3 = "!space3:server"; + describe("SpaceStore", () => { stubClient(); const store = SpaceStore.instance; @@ -270,28 +289,10 @@ describe("SpaceStore", () => { }); describe("test fixture 1", () => { - const fav1 = "!fav1:server"; - const fav2 = "!fav2:server"; - const fav3 = "!fav3:server"; - const dm1 = "!dm1:server"; - const dm1Partner = "@dm1Partner:server"; - const dm2 = "!dm2:server"; - const dm2Partner = "@dm2Partner:server"; - const dm3 = "!dm3:server"; - const dm3Partner = "@dm3Partner:server"; - const orphan1 = "!orphan1:server"; - const orphan2 = "!orphan2:server"; - const invite1 = "!invite1:server"; - const invite2 = "!invite2:server"; - const spaceRoom1 = "!spaceRoom1:server"; - const space1 = "!space1:server"; - const space2 = "!space2:server"; - const space3 = "!space3:server"; - beforeEach(async () => { - [fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, spaceRoom1].forEach(mkRoom); - mkSpace(space1, [fav1, spaceRoom1]); - mkSpace(space2, [fav1, fav2, fav3, spaceRoom1]); + [fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1].forEach(mkRoom); + mkSpace(space1, [fav1, room1]); + mkSpace(space2, [fav1, fav2, fav3, room1]); mkSpace(space3, [invite2]); [fav1, fav2, fav3].forEach(roomId => { @@ -342,13 +343,13 @@ describe("SpaceStore", () => { }); it("home space does not contain rooms/low priority from rooms within spaces", () => { - expect(store.getSpaceFilteredRoomIds(null).has(spaceRoom1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy(); }); it("space contains child rooms", () => { const space = client.getRoom(space1); expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(space).has(spaceRoom1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy(); }); it("space contains child favourites", () => { @@ -356,7 +357,7 @@ describe("SpaceStore", () => { expect(store.getSpaceFilteredRoomIds(space).has(fav1)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(space).has(fav2)).toBeTruthy(); expect(store.getSpaceFilteredRoomIds(space).has(fav3)).toBeTruthy(); - expect(store.getSpaceFilteredRoomIds(space).has(spaceRoom1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(space).has(room1)).toBeTruthy(); }); it("space contains child invites", () => { @@ -387,16 +388,72 @@ describe("SpaceStore", () => { }); describe("context switching tests", () => { - test.todo("//context switching"); + const fn = jest.spyOn(defaultDispatcher, "dispatch"); + + beforeEach(async () => { + [room1, room2, orphan1].forEach(mkRoom); + mkSpace(space1, [room1, room2]); + mkSpace(space2, [room2]); + await run(); + }); + afterEach(() => { + fn.mockClear(); + localStorage.clear(); + }); + + const getCurrentRoom = () => fn.mock.calls.reverse().find(([p]) => p.action === "view_room")?.[0].room_id; + + it("last viewed room in target space is the current viewed and in both spaces", async () => { + await store.setActiveSpace(client.getRoom(space1)); + viewRoom(room2); + await store.setActiveSpace(client.getRoom(space2)); + viewRoom(room2); + await store.setActiveSpace(client.getRoom(space1)); + expect(getCurrentRoom()).toBe(room2); + }); + + it("last viewed room in target space is in the current space", async () => { + await store.setActiveSpace(client.getRoom(space1)); + viewRoom(room2); + await store.setActiveSpace(client.getRoom(space2)); + expect(getCurrentRoom()).toBe(space2); + await store.setActiveSpace(client.getRoom(space1)); + expect(getCurrentRoom()).toBe(room2); + }); + + it("last viewed room in target space is not in the current space", async () => { + await store.setActiveSpace(client.getRoom(space1)); + viewRoom(room1); + await store.setActiveSpace(client.getRoom(space2)); + viewRoom(room2); + await store.setActiveSpace(client.getRoom(space1)); + expect(getCurrentRoom()).toBe(room1); + }); + + it("last viewed room is target space is not known", async () => { + await store.setActiveSpace(client.getRoom(space1)); + viewRoom(room1); + localStorage.setItem(`mx_space_context_${space2}`, orphan2); + await store.setActiveSpace(client.getRoom(space2)); + expect(getCurrentRoom()).toBe(space2); + }); + + it("no last viewed room in target space", async () => { + await store.setActiveSpace(client.getRoom(space1)); + viewRoom(room1); + await store.setActiveSpace(client.getRoom(space2)); + expect(getCurrentRoom()).toBe(space2); + }); + + it("no last viewed room in home space", async () => { + await store.setActiveSpace(client.getRoom(space1)); + viewRoom(room1); + await store.setActiveSpace(null); + expect(fn.mock.calls[fn.mock.calls.length - 1][0]).toStrictEqual({ action: "view_home_page" }); + }); }); describe("space auto switching tests", () => { - const space1 = "!space1:server"; - const space2 = "!space2:server"; - const room1 = "!room1:server"; // in space 1 & 2 - const room2 = "!room2:server"; // in space 1 & 2 (canonical) - const orphan1 = "!orphan:server"; - beforeEach(async () => { [room1, room2, orphan1].forEach(mkRoom); mkSpace(space1, [room1, room2]); From 0e92251f70e92a983e3bff3418c32fc5d9572bca Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 1 Apr 2021 14:15:21 +0100 Subject: [PATCH 192/330] Fix simple lint errors --- src/@types/common.ts | 1 + src/CallHandler.tsx | 2 +- src/KeyBindingsManager.ts | 6 ++-- src/ScalarAuthClient.ts | 6 ++-- src/components/views/rooms/EventTile.tsx | 6 +++- .../views/rooms/MessageComposer.tsx | 4 ++- .../views/settings/EventIndexPanel.tsx | 3 +- .../tabs/user/HelpUserSettingsTab.tsx | 34 +++++++++---------- 8 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/@types/common.ts b/src/@types/common.ts index b887bd4090..e5503a0b68 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -17,6 +17,7 @@ limitations under the License. import { JSXElementConstructor } from "react"; // Based on https://stackoverflow.com/a/53229857/3532235 +// eslint-disable-next-line @typescript-eslint/type-annotation-spacing export type Without = {[P in Exclude] ? : never}; export type XOR = (T | U) extends object ? (Without & U) | (Without & T) : T | U; export type Writeable = { -readonly [P in keyof T]: T[P] }; diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index be687a4474..237b323ce7 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -673,7 +673,7 @@ export default class CallHandler { call.placeScreenSharingCall( remoteElement, localElement, - async () : Promise => { + async (): Promise => { const {finished} = Modal.createDialog(DesktopCapturerSourcePicker); const [source] = await finished; return source; diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index d862f10c02..aac14bde20 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -231,8 +231,10 @@ export class KeyBindingsManager { /** * Finds a matching KeyAction for a given KeyboardEvent */ - private getAction(getters: KeyBindingGetter[], ev: KeyboardEvent | React.KeyboardEvent) - : T | undefined { + private getAction( + getters: KeyBindingGetter[], + ev: KeyboardEvent | React.KeyboardEvent, + ): T | undefined { for (const getter of getters) { const bindings = getter(); const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac)); diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts index 200b4fd7b9..408a264138 100644 --- a/src/ScalarAuthClient.ts +++ b/src/ScalarAuthClient.ts @@ -194,7 +194,7 @@ export default class ScalarAuthClient { if (err) { reject(err); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(new Error(`Scalar request failed: ${response.statusCode}`)); } else if (!body || !body.scalar_token) { reject(new Error("Missing scalar_token in response")); } else { @@ -218,7 +218,7 @@ export default class ScalarAuthClient { if (err) { reject(err); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(new Error(`Scalar request failed: ${response.statusCode}`)); } else if (!body) { reject(new Error("Missing page title in response")); } else { @@ -257,7 +257,7 @@ export default class ScalarAuthClient { if (err) { reject(err); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(new Error(`Scalar request failed: ${response.statusCode}`)); } else if (!body) { reject(new Error("Failed to set widget assets state")); } else { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index f6fb83c064..b63a06f751 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1182,7 +1182,11 @@ function E2ePadlockUnknown(props) { function E2ePadlockUnauthenticated(props) { return ( - + ); } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 5178d52305..90f4a4ca48 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -394,7 +394,9 @@ export default class MessageComposer extends React.Component { controls.push(
    - + {_t("This room has been replaced and is no longer active.")}
    diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index d1a02de16d..3862ffa155 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -124,13 +124,12 @@ export default class EventIndexPanel extends React.Component { } _confirmEventStoreReset = () => { - const self = this; const { close } = Modal.createDialog(SeshatResetDialog, { onFinished: async (success) => { if (success) { await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); - await self._onEnable(); + await this._onEnable(); close(); } }, diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 05f14cebc8..ca9629179d 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -122,28 +122,28 @@ export default class HelpUserSettingsTab extends React.Component { {_t("Credits")}
    From d7e6f4b4b5ca1cd0898b7b9f49469f5f382ef77d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 6 Apr 2021 12:26:50 +0100 Subject: [PATCH 193/330] Add basic types --- src/@types/global.d.ts | 8 +- src/Login.ts | 7 +- src/ScalarAuthClient.ts | 15 +- src/{Terms.js => Terms.ts} | 45 ++- .../eventindex/ManageEventIndexDialog.tsx | 24 +- src/components/structures/auth/Login.tsx | 4 +- .../structures/auth/Registration.tsx | 4 +- src/components/structures/auth/SoftLogout.tsx | 41 ++- .../views/dialogs/ServerPickerDialog.tsx | 6 +- .../views/elements/ServerPicker.tsx | 4 +- src/components/views/rooms/EventTile.tsx | 261 ++++++++++-------- .../views/rooms/MessageComposer.tsx | 129 ++++----- .../views/rooms/ThirdPartyMemberInfo.tsx | 23 +- .../views/settings/EventIndexPanel.tsx | 19 +- src/components/views/settings/SetIdServer.tsx | 35 ++- .../tabs/room/RolesRoomSettingsTab.tsx | 28 +- .../tabs/room/SecurityRoomSettingsTab.tsx | 43 +-- .../tabs/user/HelpUserSettingsTab.tsx | 25 +- .../tabs/user/MjolnirUserSettingsTab.tsx | 14 +- .../tabs/user/PreferencesUserSettingsTab.tsx | 26 +- src/indexing/BaseEventIndexManager.ts | 6 +- src/indexing/EventIndexPeg.ts | 17 +- src/stores/TypingStore.ts | 16 +- src/stores/WidgetEchoStore.ts | 14 +- src/utils/AutoDiscoveryUtils.tsx | 32 ++- src/utils/StorageManager.ts | 8 +- src/utils/{Timer.js => Timer.ts} | 11 +- .../permalinks/ElementPermalinkConstructor.ts | 6 +- src/utils/permalinks/Permalinks.ts | 11 +- 29 files changed, 542 insertions(+), 340 deletions(-) rename src/{Terms.js => Terms.ts} (87%) rename src/utils/{Timer.js => Timer.ts} (91%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index ee0963e537..6470501769 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -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. @@ -40,6 +40,8 @@ import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import VoipUserMapper from "../VoipUserMapper"; import {SpaceStoreClass} from "../stores/SpaceStore"; import {VoiceRecording} from "../voice/VoiceRecording"; +import TypingStore from "../stores/TypingStore"; +import { EventIndexPeg } from "../indexing/EventIndexPeg"; declare global { interface Window { @@ -72,11 +74,15 @@ declare global { mxVoipUserMapper: VoipUserMapper; mxSpaceStore: SpaceStoreClass; mxVoiceRecorder: typeof VoiceRecording; + mxTypingStore: TypingStore; + mxEventIndexPeg: EventIndexPeg; } interface Document { // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess hasStorageAccess?: () => Promise; + // https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess + requestStorageAccess?: () => Promise; // Safari & IE11 only have this prefixed: we used prefixed versions // previously so let's continue to support them for now diff --git a/src/Login.ts b/src/Login.ts index db3c4c11e4..d584df7dfe 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -1,9 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2020 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. @@ -59,7 +56,7 @@ export type LoginFlow = ISSOFlow | IPasswordFlow; // TODO: Move this to JS SDK /* eslint-disable camelcase */ interface ILoginParams { - identifier?: string; + identifier?: object; password?: string; token?: string; device_id?: string; diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts index 408a264138..3c63bf8047 100644 --- a/src/ScalarAuthClient.ts +++ b/src/ScalarAuthClient.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016, 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. @@ -17,7 +16,7 @@ limitations under the License. import url from 'url'; import SettingsStore from "./settings/SettingsStore"; -import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; +import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms'; import {MatrixClientPeg} from "./MatrixClientPeg"; import request from "browser-request"; @@ -31,6 +30,12 @@ const imApiVersion = "1.1"; // TODO: Generify the name of this class and all components within - it's not just for Scalar. export default class ScalarAuthClient { + private apiUrl: string; + private uiUrl: string; + private scalarToken: string; + private termsInteractionCallback: TermsInteractionCallback; + private isDefaultManager: boolean; + constructor(apiUrl, uiUrl) { this.apiUrl = apiUrl; this.uiUrl = uiUrl; @@ -154,7 +159,7 @@ export default class ScalarAuthClient { parsedImRestUrl.pathname = ''; return startTermsFlow([new Service( SERVICE_TYPES.IM, - parsedImRestUrl.format(), + url.format(parsedImRestUrl), token, )], this.termsInteractionCallback).then(() => { return token; @@ -243,7 +248,7 @@ export default class ScalarAuthClient { disableWidgetAssets(widgetType: WidgetType, widgetId) { let url = this.apiUrl + '/widgets/set_assets_state'; url = this.getStarterLink(url); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { request({ method: 'GET', // XXX: Actions shouldn't be GET requests uri: url, diff --git a/src/Terms.js b/src/Terms.ts similarity index 87% rename from src/Terms.js rename to src/Terms.ts index 6ae89f9a2c..382e2d9782 100644 --- a/src/Terms.js +++ b/src/Terms.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +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. @@ -17,7 +17,7 @@ limitations under the License. import classNames from 'classnames'; import {MatrixClientPeg} from './MatrixClientPeg'; -import * as sdk from './'; +import * as sdk from '.'; import Modal from './Modal'; export class TermsNotSignedError extends Error {} @@ -27,6 +27,10 @@ export class TermsNotSignedError extends Error {} * require agreement from the user before the user can use that service. */ export class Service { + public serviceType: string; + public baseUrl: string; + public accessToken: string; + /** * @param {MatrixClient.SERVICE_TYPES} serviceType The type of service * @param {string} baseUrl The Base URL of the service (ie. before '/_matrix') @@ -39,6 +43,26 @@ export class Service { } } +interface Policy { + // @ts-ignore: No great way to express indexed types together with other keys + version: string; + [lang: string]: { + url: string; + }; +} +type Policies = { + [policy: string]: Policy, +}; + +export type TermsInteractionCallback = ( + policiesAndServicePairs: { + service: Service, + policies: Policies, + }[], + agreedUrls: string[], + extraClassNames?: string, +) => Promise; + /** * Start a flow where the user is presented with terms & conditions for some services * @@ -51,8 +75,8 @@ export class Service { * if they cancel. */ export async function startTermsFlow( - services, - interactionCallback = dialogTermsInteractionCallback, + services: Service[], + interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback, ) { const termsPromises = services.map( (s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl), @@ -77,7 +101,7 @@ export async function startTermsFlow( * } */ - const terms = await Promise.all(termsPromises); + const terms: { policies: Policies }[] = await Promise.all(termsPromises); const policiesAndServicePairs = terms.map((t, i) => { return { 'service': services[i], 'policies': t.policies }; }); // fetch the set of agreed policy URLs from account data @@ -158,10 +182,13 @@ export async function startTermsFlow( } export function dialogTermsInteractionCallback( - policiesAndServicePairs, - agreedUrls, - extraClassNames, -) { + policiesAndServicePairs: { + service: Service, + policies: { [policy: string]: Policy }, + }[], + agreedUrls: string[], + extraClassNames?: string, +): Promise { return new Promise((resolve, reject) => { console.log("Terms that need agreement", policiesAndServicePairs); const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index be3368b87b..783d40081a 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.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. @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../../index'; -import PropTypes from 'prop-types'; import { _t } from '../../../../languageHandler'; import SdkConfig from '../../../../SdkConfig'; import SettingsStore from "../../../../settings/SettingsStore"; @@ -26,14 +25,23 @@ import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import {SettingLevel} from "../../../../settings/SettingLevel"; +interface IProps { + onFinished: (boolean) => {}, +} + +interface IState { + eventIndexSize: number; + eventCount: number; + crawlingRoomsCount: number; + roomCount: number; + currentRoom: string; + crawlerSleepTime: number; +} + /* * Allows the user to introspect the event index state and disable it. */ -export default class ManageEventIndexDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - +export default class ManageEventIndexDialog extends React.Component { constructor(props) { super(props); @@ -84,7 +92,7 @@ export default class ManageEventIndexDialog extends React.Component { } } - async componentDidMount(): void { + async componentDidMount(): Promise { let eventIndexSize = 0; let crawlingRoomsCount = 0; let roomCount = 0; diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 3ab73fb9ac..34a5410928 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016, 2017, 2018, 2019 The Matrix.org Foundation C.I.C. +Copyright 2015-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. @@ -94,7 +94,7 @@ interface IState { // be seeing. serverIsAlive: boolean; serverErrorIsFatal: boolean; - serverDeadError: string; + serverDeadError?: ReactNode; } /* diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 73955e7832..96fb9bdc82 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016, 2017, 2018, 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2015-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. @@ -95,7 +95,7 @@ interface IState { // be seeing. serverIsAlive: boolean; serverErrorIsFatal: boolean; - serverDeadError: string; + serverDeadError?: ReactNode; // Our matrix client - part of state because we can't render the UI auth // component without it. diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 08db3b2efe..6b24535657 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +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,14 +15,13 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {_t} from '../../../languageHandler'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import * as Lifecycle from '../../../Lifecycle'; import Modal from '../../../Modal'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import {sendLoginRequest} from "../../../Login"; +import {ISSOFlow, LoginFlow, sendLoginRequest} from "../../../Login"; import AuthPage from "../../views/auth/AuthPage"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "../../../BasePlatform"; import SSOButtons from "../../views/elements/SSOButtons"; @@ -42,26 +41,38 @@ const FLOWS_TO_VIEWS = { "m.login.sso": LOGIN_VIEW.SSO, }; -@replaceableComponent("structures.auth.SoftLogout") -export default class SoftLogout extends React.Component { - static propTypes = { - // Query parameters from MatrixChat - realQueryParams: PropTypes.object, // {loginToken} - - // Called when the SSO login completes - onTokenLoginCompleted: PropTypes.func, +interface IProps { + // Query parameters from MatrixChat + realQueryParams: { + loginToken?: string; }; + fragmentAfterLogin?: string; - constructor() { - super(); + // Called when the SSO login completes + onTokenLoginCompleted: () => void, +} + +interface IState { + loginView: number; + keyBackupNeeded: boolean; + busy: boolean; + password: string; + errorText: string; + flows: LoginFlow[]; +} + +@replaceableComponent("structures.auth.SoftLogout") +export default class SoftLogout extends React.Component { + constructor(props) { + super(props); this.state = { loginView: LOGIN_VIEW.LOADING, keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount) - busy: false, password: "", errorText: "", + flows: [], }; } @@ -247,7 +258,7 @@ export default class SoftLogout extends React.Component { } // else we already have a message and should use it (key backup warning) const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"; - const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType); + const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow; return (
    diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx index 4abc0a88b1..62a2b95c68 100644 --- a/src/components/views/dialogs/ServerPickerDialog.tsx +++ b/src/components/views/dialogs/ServerPickerDialog.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. @@ -110,7 +110,7 @@ export default class ServerPickerDialog extends React.PureComponent diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx index 4551a49deb..9f06e2618c 100644 --- a/src/components/views/elements/ServerPicker.tsx +++ b/src/components/views/elements/ServerPicker.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. @@ -67,7 +67,7 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange } ; } - let serverName = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl; + let serverName: React.ReactNode = serverConfig.isNameResolvable ? serverConfig.hsName : serverConfig.hsUrl; if (serverConfig.hsNameIsDifferent) { serverName =
    + mxEvent={this.props.mxEvent} + highlights={this.props.highlights} + highlightLink={this.props.highlightLink} + showUrlPreview={this.props.showUrlPreview} + onHeightChanged={this.props.onHeightChanged} + />
    ); @@ -1063,12 +1065,13 @@ export default class EventTile extends React.Component {
    + mxEvent={this.props.mxEvent} + highlights={this.props.highlights} + highlightLink={this.props.highlightLink} + showUrlPreview={this.props.showUrlPreview} + tileShape={this.props.tileShape} + onHeightChanged={this.props.onHeightChanged} + />
    { { groupPadlock } { thread } + mxEvent={this.props.mxEvent} + highlights={this.props.highlights} + highlightLink={this.props.highlightLink} + onHeightChanged={this.props.onHeightChanged} + replacingEventId={this.props.replacingEventId} + showUrlPreview={false} + />
    ); @@ -1136,14 +1140,15 @@ export default class EventTile extends React.Component { { groupPadlock } { thread } + mxEvent={this.props.mxEvent} + replacingEventId={this.props.replacingEventId} + editState={this.props.editState} + highlights={this.props.highlights} + highlightLink={this.props.highlightLink} + showUrlPreview={this.props.showUrlPreview} + permalinkCreator={this.props.permalinkCreator} + onHeightChanged={this.props.onHeightChanged} + /> { keyRequestInfo } { reactionsRow } { actionBar } diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index 8ea2832405..c97b436854 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -153,18 +153,17 @@ export default class EventIndexPanel extends React.Component<{}, IState> { if (EventIndexPeg.get() !== null) { eventIndexingSettings = (
    -
    - {_t("Securely cache encrypted messages locally for them " + - "to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", - { - size: formatBytes(this.state.eventIndexSize, 0), - // This drives the singular / plural string - // selection for "room" / "rooms" only. - count: this.state.roomCount, - rooms: formatCountLong(this.state.roomCount), - }, - )} -
    +
    {_t( + "Securely cache encrypted messages locally for them " + + "to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", + { + size: formatBytes(this.state.eventIndexSize, 0), + // This drives the singular / plural string + // selection for "room" / "rooms" only. + count: this.state.roomCount, + rooms: formatCountLong(this.state.roomCount), + }, + )}
    {_t("Manage")} @@ -175,10 +174,10 @@ export default class EventIndexPanel extends React.Component<{}, IState> { } else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) { eventIndexingSettings = (
    -
    - {_t( "Securely cache encrypted messages locally for them to " + - "appear in search results.")} -
    +
    {_t( + "Securely cache encrypted messages locally for them to " + + "appear in search results.", + )}
    @@ -196,40 +195,36 @@ export default class EventIndexPanel extends React.Component<{}, IState> { ); eventIndexingSettings = ( - ); } else if (!EventIndexPeg.platformHasSupport()) { eventIndexingSettings = ( -
    +
    {_t( + "%(brand)s can't securely cache encrypted messages locally " + + "while running in a web browser. Use %(brand)s Desktop " + + "for encrypted messages to appear in search results.", { - _t( "%(brand)s can't securely cache encrypted messages locally " + - "while running in a web browser. Use %(brand)s Desktop " + - "for encrypted messages to appear in search results.", - { - brand, - }, - { - 'desktopLink': (sub) => {sub}, - }, - ) - } -
    + brand, + }, + { + desktopLink: sub => {sub}, + }, + )}
    ); } else { eventIndexingSettings = ( @@ -241,19 +236,18 @@ export default class EventIndexPanel extends React.Component<{}, IState> { }

    {EventIndexPeg.error && ( -
    - {_t("Advanced")} - - {EventIndexPeg.error.message} - -

    - - {_t("Reset")} - -

    -
    +
    + {_t("Advanced")} + + {EventIndexPeg.error.message} + +

    + + {_t("Reset")} + +

    +
    )} -
    ); } diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 8b791b5634..f5fd537918 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -87,8 +87,10 @@ export class BannedUser extends React.Component { if (this.props.canUnban) { unbanButton = ( - + { _t('Unban') } ); @@ -345,8 +347,9 @@ export default class RolesRoomSettingsTab extends React.Component { if (sender) bannedBy = sender.name; return ( + member={member} reason={banEvent.reason} + by={bannedBy} + /> ); })} diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index f238a76ed7..a8cd920eb2 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -113,10 +113,9 @@ export default class SecurityRoomSettingsTab extends React.ComponentLearn more about encryption.", {}, { - 'a': (sub) => { - return {sub}; - }, + a: sub => {sub}, }, ), onFinished: (confirm) => { @@ -385,7 +384,8 @@ export default class SecurityRoomSettingsTab extends React.Component{_t("Once enabled, encryption cannot be disabled.")}
    + label={_t("Encrypted")} disabled={!canEnableEncryption} + />
    {encryptionSettings}
    diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index c982de6713..b620088096 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -215,28 +215,27 @@ export default class HelpUserSettingsTab extends React.Component
    {_t('Bug reporting')}
    - { - _t( "If you've submitted a bug via GitHub, debug logs can help " + - "us track down the problem. Debug logs contain application " + - "usage data including your username, the IDs or aliases of " + - "the rooms or groups you have visited and the usernames of " + - "other users. They do not contain messages.", - ) - } + {_t( + "If you've submitted a bug via GitHub, debug logs can help " + + "us track down the problem. Debug logs contain application " + + "usage data including your username, the IDs or aliases of " + + "the rooms or groups you have visited and the usernames of " + + "other users. They do not contain messages.", + )}
    {_t("Submit debug logs")}
    - { - _t( "To report a Matrix-related security issue, please read the Matrix.org " + - "Security Disclosure Policy.", {}, - { - 'a': (sub) => - {sub}, - }) - } + {_t( + "To report a Matrix-related security issue, please read the Matrix.org " + + "Security Disclosure Policy.", {}, + { + a: sub => {sub}, + }, + )}
    ); @@ -272,7 +271,8 @@ export default class HelpUserSettingsTab extends React.Component {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
    {_t("Access Token:") + ' '} + data-spoiler={MatrixClientPeg.get().getAccessToken()} + > <{ _t("click to reveal") }>
    From 49f5f893514917b7082c856d8dbe2b8de03edbad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 23 Apr 2021 16:04:53 -0600 Subject: [PATCH 198/330] Give a bit of opacity to the divider colour --- res/themes/dark/css/_dark.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 9691b449e7..9c381ecb98 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -63,7 +63,7 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: $bg-color; -$resend-button-divider-color: $muted-fg-color; +$resend-button-divider-color: #b9bec64a; // muted-text with a 4A opacity. // scrollbars $scrollbar-thumb-color: rgba(255, 255, 255, 0.2); From dad7a2205522df484a967741f6601c834f254ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 08:03:39 +0200 Subject: [PATCH 199/330] Initial code for dynamic minZoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/elements/_ImageView.scss | 3 +-- src/components/views/elements/ImageView.tsx | 29 ++++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 93ebcc2d56..71035dadc3 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -31,8 +31,7 @@ limitations under the License. .mx_ImageView_image { pointer-events: all; - max-width: 95%; - max-height: 95%; + flex-shrink: 0; } .mx_ImageView_panel { diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index cbced07bfe..208a6d995b 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -36,13 +36,15 @@ import {normalizeWheelEvent} from "../../../utils/Mouse"; const MIN_ZOOM = 100; const MAX_ZOOM = 300; +// Max scale to keep gaps around the image +const MAX_SCALE = 0.95; // This is used for the buttons const ZOOM_STEP = 10; // This is used for mouse wheel events const ZOOM_COEFFICIENT = 0.5; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; - +const IMAGE_WRAPPER_CLASS = "mx_ImageView_image_wrapper"; interface IProps { src: string, // the source of the image being displayed @@ -62,8 +64,9 @@ interface IProps { } interface IState { - rotation: number, zoom: number, + minZoom: number, + rotation: number, translationX: number, translationY: number, moving: boolean, @@ -75,8 +78,9 @@ export default class ImageView extends React.Component { constructor(props) { super(props); this.state = { + zoom: 0, + minZoom: 100, rotation: 0, - zoom: MIN_ZOOM, translationX: 0, translationY: 0, moving: false, @@ -99,12 +103,29 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); + window.addEventListener("resize", this.onWindowResize); + this.calculateMinZoom(); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); } + private onWindowResize = (ev) => { + this.calculateMinZoom(); + } + + private calculateMinZoom() { + // TODO: What if we don't have width and height props? + const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; + const zoomX = (imageWrapper.clientWidth / this.props.width) * 100; + const zoomY = (imageWrapper.clientHeight / this.props.height) * 100; + const zoom = Math.min(zoomX, zoomY) * MAX_SCALE; + + if (this.state.zoom <= this.state.minZoom) this.setState({zoom: zoom}); + this.setState({minZoom: zoom}); + } + private onKeyDown = (ev: KeyboardEvent) => { if (ev.key === Key.ESCAPE) { ev.stopPropagation(); @@ -427,7 +448,7 @@ export default class ImageView extends React.Component { {this.renderContextMenu()}
    -
    +
    Date: Sat, 24 Apr 2021 08:32:28 +0200 Subject: [PATCH 200/330] Add dynamic maxZoom and wire it all up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 50 ++++++++++++--------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 208a6d995b..ecc4303764 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -34,8 +34,6 @@ import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import {normalizeWheelEvent} from "../../../utils/Mouse"; -const MIN_ZOOM = 100; -const MAX_ZOOM = 300; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; // This is used for the buttons @@ -66,6 +64,7 @@ interface IProps { interface IState { zoom: number, minZoom: number, + maxZoom: number, rotation: number, translationX: number, translationY: number, @@ -79,7 +78,8 @@ export default class ImageView extends React.Component { super(props); this.state = { zoom: 0, - minZoom: 100, + minZoom: MAX_SCALE, + maxZoom: 100, rotation: 0, translationX: 0, translationY: 0, @@ -100,11 +100,12 @@ export default class ImageView extends React.Component { private previousY = 0; componentDidMount() { + console.log("LOG calculating", this.props.width, this.props.height); // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); window.addEventListener("resize", this.onWindowResize); - this.calculateMinZoom(); + this.calculateZoom(); } componentWillUnmount() { @@ -112,18 +113,23 @@ export default class ImageView extends React.Component { } private onWindowResize = (ev) => { - this.calculateMinZoom(); + this.calculateZoom(); } - private calculateMinZoom() { - // TODO: What if we don't have width and height props? + private calculateZoom() { + // TODO: What if we don't have width and height props? + const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; const zoomX = (imageWrapper.clientWidth / this.props.width) * 100; const zoomY = (imageWrapper.clientHeight / this.props.height) * 100; - const zoom = Math.min(zoomX, zoomY) * MAX_SCALE; + const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; + const maxZoom = minZoom >= 100 ? minZoom : 100; - if (this.state.zoom <= this.state.minZoom) this.setState({zoom: zoom}); - this.setState({minZoom: zoom}); + if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); + this.setState({ + minZoom: minZoom, + maxZoom: maxZoom, + }); } private onKeyDown = (ev: KeyboardEvent) => { @@ -141,16 +147,16 @@ export default class ImageView extends React.Component { const {deltaY} = normalizeWheelEvent(ev); const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT); - if (newZoom <= MIN_ZOOM) { + if (newZoom <= this.state.minZoom) { this.setState({ - zoom: MIN_ZOOM, + zoom: this.state.minZoom, translationX: 0, translationY: 0, }); return; } - if (newZoom >= MAX_ZOOM) { - this.setState({zoom: MAX_ZOOM}); + if (newZoom >= this.state.maxZoom) { + this.setState({zoom: this.state.maxZoom}); return; } @@ -172,8 +178,8 @@ export default class ImageView extends React.Component { }; private onZoomInClick = () => { - if (this.state.zoom >= MAX_ZOOM) { - this.setState({zoom: MAX_ZOOM}); + if (this.state.zoom >= this.state.maxZoom) { + this.setState({zoom: this.state.maxZoom}); return; } @@ -183,9 +189,9 @@ export default class ImageView extends React.Component { }; private onZoomOutClick = () => { - if (this.state.zoom <= MIN_ZOOM) { + if (this.state.zoom <= this.state.minZoom) { this.setState({ - zoom: MIN_ZOOM, + zoom: this.state.minZoom, translationX: 0, translationY: 0, }); @@ -238,8 +244,8 @@ export default class ImageView extends React.Component { if (ev.button !== 0) return; // Zoom in if we are completely zoomed out - if (this.state.zoom === MIN_ZOOM) { - this.setState({zoom: MAX_ZOOM}); + if (this.state.zoom === this.state.minZoom) { + this.setState({zoom: this.state.maxZoom}); return; } @@ -272,7 +278,7 @@ export default class ImageView extends React.Component { Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE ) { this.setState({ - zoom: MIN_ZOOM, + zoom: this.state.minZoom, translationX: 0, translationY: 0, }); @@ -311,7 +317,7 @@ export default class ImageView extends React.Component { let cursor; if (this.state.moving) { cursor= "grabbing"; - } else if (this.state.zoom === MIN_ZOOM) { + } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; } else { cursor = "zoom-out"; From f8af9831a9ab79591568c78e192848ed5d72b682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 08:35:45 +0200 Subject: [PATCH 201/330] Don't use percanteages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I was an idiot to use them in the first place Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index ecc4303764..68567257f7 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -120,10 +120,10 @@ export default class ImageView extends React.Component { // TODO: What if we don't have width and height props? const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; - const zoomX = (imageWrapper.clientWidth / this.props.width) * 100; - const zoomY = (imageWrapper.clientHeight / this.props.height) * 100; + const zoomX = imageWrapper.clientWidth / this.props.width; + const zoomY = imageWrapper.clientHeight / this.props.height; const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; - const maxZoom = minZoom >= 100 ? minZoom : 100; + const maxZoom = minZoom >= 1 ? minZoom : 1; if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); this.setState({ @@ -323,7 +323,7 @@ export default class ImageView extends React.Component { cursor = "zoom-out"; } const rotationDegrees = this.state.rotation + "deg"; - const zoomPercentage = this.state.zoom/100; + const zoom = this.state.zoom; const translatePixelsX = this.state.translationX + "px"; const translatePixelsY = this.state.translationY + "px"; // The order of the values is important! @@ -335,7 +335,7 @@ export default class ImageView extends React.Component { transition: this.state.moving ? null : "transform 200ms ease 0s", transform: `translateX(${translatePixelsX}) translateY(${translatePixelsY}) - scale(${zoomPercentage}) + scale(${zoom}) rotate(${rotationDegrees})`, }; From 57b34f8dbc9e8999ffa5c3f2b5b3427a60de9d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 08:37:51 +0200 Subject: [PATCH 202/330] Get rid of onWindowResize() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 68567257f7..cca4b34ad6 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -104,7 +104,7 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); - window.addEventListener("resize", this.onWindowResize); + window.addEventListener("resize", this.calculateZoom); this.calculateZoom(); } @@ -112,11 +112,7 @@ export default class ImageView extends React.Component { this.focusLock.current.removeEventListener('wheel', this.onWheel); } - private onWindowResize = (ev) => { - this.calculateZoom(); - } - - private calculateZoom() { + private calculateZoom = () => { // TODO: What if we don't have width and height props? const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; From e0e9ccbf959bcaea693552bd239cbffbe441f441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 08:38:13 +0200 Subject: [PATCH 203/330] Remove logline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index cca4b34ad6..543828dc55 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -100,7 +100,6 @@ export default class ImageView extends React.Component { private previousY = 0; componentDidMount() { - console.log("LOG calculating", this.props.width, this.props.height); // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); From dcc060f6f70b2e36656f91f0a547b4323c10112d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 09:00:15 +0200 Subject: [PATCH 204/330] Use correct cursor when we can't zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 543828dc55..0db7d9401c 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -312,6 +312,8 @@ export default class ImageView extends React.Component { let cursor; if (this.state.moving) { cursor= "grabbing"; + } else if (this.state.maxZoom === this.state.minZoom) { + cursor = "pointer"; } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; } else { From 9b7a9fc865f108c07e7759f33fcf40ca5c42cd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 09:24:25 +0200 Subject: [PATCH 205/330] Use MAX_SCALE for maxZoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 0db7d9401c..a836409d4d 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -79,7 +79,7 @@ export default class ImageView extends React.Component { this.state = { zoom: 0, minZoom: MAX_SCALE, - maxZoom: 100, + maxZoom: MAX_SCALE, rotation: 0, translationX: 0, translationY: 0, From bcc6e5c5d5658b8959ab3112c98ee4b9e7f03724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 09:41:46 +0200 Subject: [PATCH 206/330] Add some comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index a836409d4d..1679c40e76 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -115,9 +115,18 @@ export default class ImageView extends React.Component { // TODO: What if we don't have width and height props? const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; + const zoomX = imageWrapper.clientWidth / this.props.width; const zoomY = imageWrapper.clientHeight / this.props.height; + // We set minZoom to the min of the zoomX and zoomY to avoid overflow in + // any direction. We also multiply by MAX_SCALE to get a gap around the + // image by default const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; + // If minZoom is bigger or equal to 1, it means we scaling the image up + // to fit the viewport and therefore we want to disable zooming, so we + // set the maxZoom to be the same as the minZoom. Otherwise, we are + // scaling the image down - we want the user to be allowed to zoom to + // 100% const maxZoom = minZoom >= 1 ? minZoom : 1; if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); From 90f2423eb72bb9839e067aef0bdcd3b6d34275a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 10:35:25 +0200 Subject: [PATCH 207/330] Fix zoom step and coeficient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 1679c40e76..af379a08e1 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -37,9 +37,9 @@ import {normalizeWheelEvent} from "../../../utils/Mouse"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; // This is used for the buttons -const ZOOM_STEP = 10; +const ZOOM_STEP = 0.10; // This is used for mouse wheel events -const ZOOM_COEFFICIENT = 0.5; +const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; const IMAGE_WRAPPER_CLASS = "mx_ImageView_image_wrapper"; From 0e312977e3f49e38d26302a549f24c57282bed0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 10:36:53 +0200 Subject: [PATCH 208/330] Rework zooming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 65 +++++++++------------ 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index af379a08e1..e5878d5c0e 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -136,20 +136,8 @@ export default class ImageView extends React.Component { }); } - private onKeyDown = (ev: KeyboardEvent) => { - if (ev.key === Key.ESCAPE) { - ev.stopPropagation(); - ev.preventDefault(); - this.props.onFinished(); - } - }; - - private onWheel = (ev: WheelEvent) => { - ev.stopPropagation(); - ev.preventDefault(); - - const {deltaY} = normalizeWheelEvent(ev); - const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT); + private zoom(delta: number) { + const newZoom = this.state.zoom + delta; if (newZoom <= this.state.minZoom) { this.setState({ @@ -167,6 +155,30 @@ export default class ImageView extends React.Component { this.setState({ zoom: newZoom, }); + } + + private onWheel = (ev: WheelEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + + const {deltaY} = normalizeWheelEvent(ev); + this.zoom(-(deltaY * ZOOM_COEFFICIENT)); + }; + + private onZoomInClick = () => { + this.zoom(ZOOM_STEP); + }; + + private onZoomOutClick = () => { + this.zoom(-ZOOM_STEP); + }; + + private onKeyDown = (ev: KeyboardEvent) => { + if (ev.key === Key.ESCAPE) { + ev.stopPropagation(); + ev.preventDefault(); + this.props.onFinished(); + } }; private onRotateCounterClockwiseClick = () => { @@ -181,31 +193,6 @@ export default class ImageView extends React.Component { this.setState({ rotation: rotationDegrees }); }; - private onZoomInClick = () => { - if (this.state.zoom >= this.state.maxZoom) { - this.setState({zoom: this.state.maxZoom}); - return; - } - - this.setState({ - zoom: this.state.zoom + ZOOM_STEP, - }); - }; - - private onZoomOutClick = () => { - if (this.state.zoom <= this.state.minZoom) { - this.setState({ - zoom: this.state.minZoom, - translationX: 0, - translationY: 0, - }); - return; - } - this.setState({ - zoom: this.state.zoom - ZOOM_STEP, - }); - }; - private onDownloadClick = () => { const a = document.createElement("a"); a.href = this.props.src; From f85d3643ee11bc6f90245446ab521883db70ed7e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 24 Apr 2021 11:31:52 +0100 Subject: [PATCH 209/330] Test and fix subspace invite receipt behaviour --- src/stores/SpaceStore.tsx | 3 ++- test/stores/SpaceStore-test.ts | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index a72b270e0b..ad2aeb26ac 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -203,7 +203,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public getChildSpaces(spaceId: string): Room[] { - return this.getChildren(spaceId).filter(r => r.isSpaceRoom()); + // don't show invited subspaces as they surface at the top level for better visibility + return this.getChildren(spaceId).filter(r => r.isSpaceRoom() && r.getMyMembership() === "join"); } public getParents(roomId: string, canonicalOnly = false): Room[] { diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 34e0186e90..30000dcced 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -288,6 +288,17 @@ describe("SpaceStore", () => { expect(store.getChildSpaces("!d:server")).toStrictEqual([]); }); + it("invite to a subspace is only shown at the top level", async () => { + mkSpace(invite1).getMyMembership.mockReturnValue("invite"); + mkSpace(space1, [invite1]); + await run(); + + expect(store.spacePanelSpaces).toStrictEqual([client.getRoom(space1)]); + expect(store.getChildSpaces(space1)).toStrictEqual([]); + expect(store.getChildRooms(space1)).toStrictEqual([]); + expect(store.invitedSpaces).toStrictEqual([client.getRoom(invite1)]); + }); + describe("test fixture 1", () => { beforeEach(async () => { [fav1, fav2, fav3, dm1, dm2, dm3, orphan1, orphan2, invite1, invite2, room1].forEach(mkRoom); From da46e90896207e0092fb02a44b3738acfb3277e0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 24 Apr 2021 11:32:12 +0100 Subject: [PATCH 210/330] Fix SpaceStore reset behaviour --- src/stores/SpaceStore.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index ad2aeb26ac..34a0f148ed 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -499,6 +499,17 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } }; + protected async reset() { + this.rootSpaces = []; + this.orphanedRooms = new Set(); + this.parentMap = new EnhancedMap(); + this.notificationStateMap = new Map(); + this.spaceFilteredRooms = new Map(); + this._activeSpace = null; + this._suggestedRooms = []; + this._invitedSpaces = new Set(); + } + protected async onNotReady() { if (!SettingsStore.getValue("feature_spaces")) return; if (this.matrixClient) { @@ -508,7 +519,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("accountData", this.onAccountData); } - await this.reset({}); + await this.reset(); } protected async onReady() { From 98851f8e644d7354834e2d42367624cfc71076ef Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 24 Apr 2021 11:32:55 +0100 Subject: [PATCH 211/330] Text space switching behaviour and fix invalid space edge case --- src/stores/SpaceStore.tsx | 2 +- test/stores/SpaceStore-test.ts | 56 ++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 34a0f148ed..a3a404fdce 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -113,7 +113,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public async setActiveSpace(space: Room | null, contextSwitch = true) { - if (space === this.activeSpace) return; + if (space === this.activeSpace || (space && !space?.isSpaceRoom())) return; this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 30000dcced..1e8048c8ff 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -17,7 +17,7 @@ limitations under the License. import { EventType } from "matrix-js-sdk/src/@types/event"; import "../skinned-sdk"; // Must be first for skinning to work -import SpaceStore from "../../src/stores/SpaceStore"; +import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../src/stores/SpaceStore"; import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; import { mkEvent, mkStubRoom, stubClient } from "../test-utils"; import { EnhancedMap } from "../../src/utils/maps"; @@ -387,7 +387,59 @@ describe("SpaceStore", () => { }); describe("active space switching tests", () => { - test.todo("//active space"); + const fn = jest.spyOn(store, "emit"); + + beforeEach(async () => { + mkRoom(room1); // not a space + mkSpace(space1, [ + mkSpace(space2).roomId, + ]); + mkSpace(space3).getMyMembership.mockReturnValue("invite"); + await run(); + await store.setActiveSpace(null); + expect(store.activeSpace).toBe(null); + }); + afterEach(() => { + fn.mockClear(); + }); + + it("switch to home space", async () => { + await store.setActiveSpace(client.getRoom(space1)); + fn.mockClear(); + + await store.setActiveSpace(null); + expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, null); + expect(store.activeSpace).toBe(null); + }); + + it("switch to invited space", async () => { + const space = client.getRoom(space3); + await store.setActiveSpace(space); + expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); + expect(store.activeSpace).toBe(space); + }); + + it("switch to top level space", async () => { + const space = client.getRoom(space1); + await store.setActiveSpace(space); + expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); + expect(store.activeSpace).toBe(space); + }); + + it("switch to subspace", async () => { + const space = client.getRoom(space2); + await store.setActiveSpace(space); + expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); + expect(store.activeSpace).toBe(space); + }); + + it("switch to unknown space is a nop", async () => { + expect(store.activeSpace).toBe(null); + const space = client.getRoom(room1); // not a space + await store.setActiveSpace(space); + expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); + expect(store.activeSpace).toBe(null); + }); }); describe("notification state tests", () => { From 9a11b346c97d0135cc8fd00a2383ed53c9674afd Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Sat, 24 Apr 2021 18:09:34 +0530 Subject: [PATCH 212/330] To fix the Redaction error and a few improvements --- src/components/structures/MessagePanel.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 132d9ab4c3..3376e3988b 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -562,7 +562,7 @@ export default class MessagePanel extends React.Component { return ret; } - _getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) { + _getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) { const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary'); const EventTile = sdk.getComponent('rooms.EventTile'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); @@ -582,7 +582,7 @@ export default class MessagePanel extends React.Component { // do we need a date separator since the last event? const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate); - if (wantsDateSeparator) { + if (wantsDateSeparator && !isGrouped) { const dateSeparator =
  • ; ret.push(dateSeparator); } @@ -966,7 +966,7 @@ class CreationGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); - + const isGrouped=true; const panel = this.panel; const ret = []; const createEvent = this.createEvent; @@ -982,7 +982,7 @@ class CreationGrouper { // If this m.room.create event should be shown (room upgrade) then show it before the summary if (panel._shouldShowEvent(createEvent)) { // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered - ret.push(...panel._getTilesForEvent(createEvent, createEvent, false)); + ret.push(...panel._getTilesForEvent(createEvent, createEvent)); } for (const ejected of this.ejectedEvents) { @@ -1052,6 +1052,7 @@ class RedactionGrouper { this.lastShownEvent = lastShownEvent; this.nextEvent = nextEvent; this.nextEventTile = nextEventTile; + } shouldGroup(ev) { @@ -1081,7 +1082,7 @@ class RedactionGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); - + const isGrouped=true; const panel = this.panel; const ret = []; const lastShownEvent = this.lastShownEvent; @@ -1098,10 +1099,11 @@ class RedactionGrouper { ); const senders = new Set(); + let eventTiles = this.events.map((e, i) => { senders.add(e.sender); const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1]; - return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile); + return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent,isGrouped, this.nextEvent, this.nextEventTile); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { @@ -1180,7 +1182,7 @@ class MemberGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); - + const isGrouped=true; const panel = this.panel; const lastShownEvent = this.lastShownEvent; const ret = []; @@ -1213,7 +1215,7 @@ class MemberGrouper { // of MemberEventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeparator is inserted. - return panel._getTilesForEvent(e, e, e === lastShownEvent); + return panel._getTilesForEvent(e, e, e === lastShownEvent,isGrouped); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { From 47c12a7d23d77a6507928f60d6ad13046a4f8792 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 25 Apr 2021 09:24:00 +0100 Subject: [PATCH 213/330] Add tests for rooms moving around the SpaceStore --- test/stores/SpaceStore-test.ts | 99 +++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 1e8048c8ff..426290256e 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -14,10 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { EventEmitter } from "events"; import { EventType } from "matrix-js-sdk/src/@types/event"; import "../skinned-sdk"; // Must be first for skinning to work -import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../src/stores/SpaceStore"; +import SpaceStore, { + UPDATE_INVITED_SPACES, + UPDATE_SELECTED_SPACE, + UPDATE_TOP_LEVEL_SPACES +} from "../../src/stores/SpaceStore"; import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; import { mkEvent, mkStubRoom, stubClient } from "../test-utils"; import { EnhancedMap } from "../../src/utils/maps"; @@ -44,6 +49,8 @@ const mockStateEventImplementation = (events: MatrixEvent[]) => { }; }; +const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e.once(k, r)); + const testUserId = "@test:user"; let rooms = []; @@ -108,6 +115,7 @@ describe("SpaceStore", () => { const run = async () => { client.getRoom.mockImplementation(roomId => rooms.find(room => room.roomId === roomId)); await setupAsyncStoreWithClient(store, client); + jest.runAllTimers(); }; beforeEach(() => { @@ -379,11 +387,82 @@ describe("SpaceStore", () => { }); describe("hierarchy resolution update tests", () => { - test.todo("updates state when spaces are joined"); - test.todo("updates state when spaces are left"); - test.todo("updates state when space invite comes in"); - test.todo("updates state when space invite is accepted"); - test.todo("updates state when space invite is rejected"); + let emitter: EventEmitter; + beforeEach(async () => { + emitter = new EventEmitter(); + client.on.mockImplementation(emitter.on.bind(emitter)); + client.removeListener.mockImplementation(emitter.removeListener.bind(emitter)); + }); + afterEach(() => { + client.on.mockReset(); + client.removeListener.mockReset(); + }); + + it("updates state when spaces are joined", async () => { + await run(); + expect(store.spacePanelSpaces).toStrictEqual([]); + const space = mkSpace(space1); + const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES); + emitter.emit("Room", space); + await prom; + expect(store.spacePanelSpaces).toStrictEqual([space]); + expect(store.invitedSpaces).toStrictEqual([]); + }); + + it("updates state when spaces are left", async () => { + const space = mkSpace(space1); + await run(); + + expect(store.spacePanelSpaces).toStrictEqual([space]); + space.getMyMembership.mockReturnValue("leave"); + const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES); + emitter.emit("Room.myMembership", space, "leave", "join"); + await prom; + expect(store.spacePanelSpaces).toStrictEqual([]); + }); + + it("updates state when space invite comes in", async () => { + await run(); + expect(store.spacePanelSpaces).toStrictEqual([]); + expect(store.invitedSpaces).toStrictEqual([]); + const space = mkSpace(space1); + space.getMyMembership.mockReturnValue("invite"); + const prom = emitPromise(store, UPDATE_INVITED_SPACES); + emitter.emit("Room", space); + await prom; + expect(store.spacePanelSpaces).toStrictEqual([]); + expect(store.invitedSpaces).toStrictEqual([space]); + }); + + it("updates state when space invite is accepted", async () => { + const space = mkSpace(space1); + space.getMyMembership.mockReturnValue("invite"); + await run(); + + expect(store.spacePanelSpaces).toStrictEqual([]); + expect(store.invitedSpaces).toStrictEqual([space]); + space.getMyMembership.mockReturnValue("join"); + const prom = emitPromise(store, UPDATE_TOP_LEVEL_SPACES); + emitter.emit("Room.myMembership", space, "join", "invite"); + await prom; + expect(store.spacePanelSpaces).toStrictEqual([space]); + expect(store.invitedSpaces).toStrictEqual([]); + }); + + it("updates state when space invite is rejected", async () => { + const space = mkSpace(space1); + space.getMyMembership.mockReturnValue("invite"); + await run(); + + expect(store.spacePanelSpaces).toStrictEqual([]); + expect(store.invitedSpaces).toStrictEqual([space]); + space.getMyMembership.mockReturnValue("leave"); + const prom = emitPromise(store, UPDATE_INVITED_SPACES); + emitter.emit("Room.myMembership", space, "leave", "invite"); + await prom; + expect(store.spacePanelSpaces).toStrictEqual([]); + expect(store.invitedSpaces).toStrictEqual([]); + }); }); describe("active space switching tests", () => { @@ -442,14 +521,6 @@ describe("SpaceStore", () => { }); }); - describe("notification state tests", () => { - test.todo("//notification states"); - }); - - describe("room list prefilter tests", () => { - test.todo("//room list filter"); - }); - describe("context switching tests", () => { const fn = jest.spyOn(defaultDispatcher, "dispatch"); From 3bb6edbda7c1424cc1816b636cf7a99188388cc3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 25 Apr 2021 09:24:26 +0100 Subject: [PATCH 214/330] Fix accepting invite edge case where it wouldn't show the newly joined space --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index a3a404fdce..61ef1167ae 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -414,7 +414,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if ((membership || room.getMyMembership()) === "invite") { this._invitedSpaces.add(room); this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); - } else if (oldMembership === "invite") { + } else if (oldMembership === "invite" && membership !== "join") { this._invitedSpaces.delete(room); this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); } else if (room?.isSpaceRoom()) { From 4411498057ef3fa5055e439ad2da2a159701a583 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 25 Apr 2021 09:33:44 +0100 Subject: [PATCH 215/330] Fix add existing to space dialog no longer showing rooms for public spaces --- .../views/dialogs/AddExistingToSpaceDialog.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 0f58a624f3..84eb8ba5ef 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -68,9 +68,15 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, if (room !== space && room !== selectedSpace && !existingSubspacesSet.has(room)) { arr[0].push(room); } - } else if (!existingRoomsSet.has(room) && joinRule !== "public") { - // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. - arr[DMRoomMap.shared().getUserIdForRoomId(room.roomId) ? 2 : 1].push(room); + } else if (!existingRoomsSet.has(room)) { + if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. + if (joinRule !== "public") { + arr[2].push(room); + } + } else { + arr[1].push(room); + } } return arr; }, [[], [], []]); From 1c7d68bb16e18a0cbaae673c337bf33a1db74333 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 25 Apr 2021 09:35:18 +0100 Subject: [PATCH 216/330] invert and outdent --- .../views/dialogs/AddExistingToSpaceDialog.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 84eb8ba5ef..dd4973d259 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -69,13 +69,11 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, arr[0].push(room); } } else if (!existingRoomsSet.has(room)) { - if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { - // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. - if (joinRule !== "public") { - arr[2].push(room); - } - } else { + if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { arr[1].push(room); + } else if (joinRule !== "public") { + // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. + arr[2].push(room); } } return arr; From e5f0119f66b4b1f0dff67392550c57877602f305 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Sun, 25 Apr 2021 23:18:09 -0500 Subject: [PATCH 217/330] Fix page up/page down scrolling only half a page Signed-off-by: Aaron Raimist --- src/components/structures/ScrollPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 976734680c..1827eb6e6b 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -525,7 +525,7 @@ export default class ScrollPanel extends React.Component { */ scrollRelative = mult => { const scrollNode = this._getScrollNode(); - const delta = mult * scrollNode.clientHeight * 0.5; + const delta = mult * scrollNode.clientHeight; scrollNode.scrollBy(0, delta); this._saveScrollState(); }; From ac486c847e2acc10f3bd2fdfc952b6afe08de901 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Mon, 26 Apr 2021 01:10:00 -0500 Subject: [PATCH 218/330] Try 0.9 Signed-off-by: Aaron Raimist --- src/components/structures/ScrollPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 1827eb6e6b..a014a6e4fe 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -525,7 +525,7 @@ export default class ScrollPanel extends React.Component { */ scrollRelative = mult => { const scrollNode = this._getScrollNode(); - const delta = mult * scrollNode.clientHeight; + const delta = mult * scrollNode.clientHeight * 0.9; scrollNode.scrollBy(0, delta); this._saveScrollState(); }; From 203425c8def330a6d7e304e05c355f9049bd42bb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Apr 2021 08:37:45 +0100 Subject: [PATCH 219/330] Test and fix space store wrongly treating room invites as space invites --- src/stores/SpaceStore.tsx | 41 ++++++++++++++++++++-------------- test/stores/SpaceStore-test.ts | 25 +++++++++++++++++++++ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 61ef1167ae..f80a5f01c3 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -410,32 +410,39 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); }, 100, {trailing: true, leading: true}); - private onRoom = (room: Room, membership?: string, oldMembership?: string) => { - if ((membership || room.getMyMembership()) === "invite") { - this._invitedSpaces.add(room); - this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); - } else if (oldMembership === "invite" && membership !== "join") { - this._invitedSpaces.delete(room); - this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); - } else if (room?.isSpaceRoom()) { - this.onSpaceUpdate(); - this.emit(room.roomId); - } else { + private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => { + const membership = newMembership || room.getMyMembership(); + + if (!room.isSpaceRoom()) { // this.onRoomUpdate(room); this.onRoomsUpdate(); - } - if (room.getMyMembership() === "join") { - if (!room.isSpaceRoom()) { + if (membership === "join") { + // the user just joined a room, remove it from the suggested list if it was there const numSuggestedRooms = this._suggestedRooms.length; this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId); if (numSuggestedRooms !== this._suggestedRooms.length) { this.emit(SUGGESTED_ROOMS, this._suggestedRooms); } - } else if (room.roomId === RoomViewStore.getRoomId()) { - // if the user was looking at the space and then joined: select that space - this.setActiveSpace(room); } + return; + } + + // Space + if (membership === "invite") { + this._invitedSpaces.add(room); + this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); + } else if (oldMembership === "invite" && membership !== "join") { + this._invitedSpaces.delete(room); + this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces); + } else { + this.onSpaceUpdate(); + this.emit(room.roomId); + } + + if (membership === "join" && room.roomId === RoomViewStore.getRoomId()) { + // if the user was looking at the space and then joined: select that space + this.setActiveSpace(room); } }; diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 426290256e..aef788647d 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -463,6 +463,31 @@ describe("SpaceStore", () => { expect(store.spacePanelSpaces).toStrictEqual([]); expect(store.invitedSpaces).toStrictEqual([]); }); + + it("room invite gets added to relevant space filters", async () => { + const space = mkSpace(space1, [invite1]); + await run(); + + expect(store.spacePanelSpaces).toStrictEqual([space]); + expect(store.invitedSpaces).toStrictEqual([]); + expect(store.getChildSpaces(space1)).toStrictEqual([]); + expect(store.getChildRooms(space1)).toStrictEqual([]); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeFalsy(); + expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeFalsy(); + + const invite = mkRoom(invite1); + invite.getMyMembership.mockReturnValue("invite"); + const prom = emitPromise(store, space1); + emitter.emit("Room", space); + await prom; + + expect(store.spacePanelSpaces).toStrictEqual([space]); + expect(store.invitedSpaces).toStrictEqual([]); + expect(store.getChildSpaces(space1)).toStrictEqual([]); + expect(store.getChildRooms(space1)).toStrictEqual([invite]); + expect(store.getSpaceFilteredRoomIds(client.getRoom(space1)).has(invite1)).toBeTruthy(); + expect(store.getSpaceFilteredRoomIds(null).has(invite1)).toBeTruthy(); + }); }); describe("active space switching tests", () => { From 123482952a611ad9e560f6222b7d142d21e11024 Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Mon, 26 Apr 2021 15:01:39 +0530 Subject: [PATCH 220/330] Fixed linting errors --- src/components/structures/MessagePanel.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 3376e3988b..adf1874ba0 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -966,7 +966,6 @@ class CreationGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); - const isGrouped=true; const panel = this.panel; const ret = []; const createEvent = this.createEvent; @@ -1052,7 +1051,6 @@ class RedactionGrouper { this.lastShownEvent = lastShownEvent; this.nextEvent = nextEvent; this.nextEventTile = nextEventTile; - } shouldGroup(ev) { @@ -1099,11 +1097,11 @@ class RedactionGrouper { ); const senders = new Set(); - + let eventTiles = this.events.map((e, i) => { senders.add(e.sender); const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1]; - return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent,isGrouped, this.nextEvent, this.nextEventTile); + return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { @@ -1215,7 +1213,7 @@ class MemberGrouper { // of MemberEventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeparator is inserted. - return panel._getTilesForEvent(e, e, e === lastShownEvent,isGrouped); + return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { From 9319dd54009e658f57305806acfec1f46e77a7f7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Apr 2021 11:24:28 +0100 Subject: [PATCH 221/330] Autofocus search box in the add existing to space dialog --- src/components/views/dialogs/AddExistingToSpaceDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 0f58a624f3..c3092e6674 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -130,6 +130,7 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, placeholder={ _t("Filter your rooms and spaces") } onSearch={setQuery} autoComplete={true} + autoFocus={true} /> { rooms.length > 0 ? ( From 1e7eedba023612e65879665782fbc68078e975b7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Apr 2021 11:29:08 +0100 Subject: [PATCH 222/330] Use label element in add existing to space dialog for easier hit target --- src/components/views/dialogs/AddExistingToSpaceDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 0f58a624f3..3a89abdc82 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -41,11 +41,11 @@ interface IProps extends IDialogProps { } const Entry = ({ room, checked, onChange }) => { - return
    + return
    ; + ; }; const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { From c1a4204ad30fd8071665e9524aa2010759bdde27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 13:11:41 +0200 Subject: [PATCH 223/330] Use a ref instead of that ugly thing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sometimes I do really weird things and don't know why :D Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index e5878d5c0e..fd559fd3cc 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -42,7 +42,6 @@ const ZOOM_STEP = 0.10; const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; -const IMAGE_WRAPPER_CLASS = "mx_ImageView_image_wrapper"; interface IProps { src: string, // the source of the image being displayed @@ -91,6 +90,7 @@ export default class ImageView extends React.Component { // XXX: Refs to functional components private contextMenuButton = createRef(); private focusLock = createRef(); + private imageWrapper = createRef(); private initX = 0; private initY = 0; @@ -114,7 +114,7 @@ export default class ImageView extends React.Component { private calculateZoom = () => { // TODO: What if we don't have width and height props? - const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; + const imageWrapper = this.imageWrapper.current; const zoomX = imageWrapper.clientWidth / this.props.width; const zoomY = imageWrapper.clientHeight / this.props.height; @@ -447,7 +447,9 @@ export default class ImageView extends React.Component { {this.renderContextMenu()}
    -
    +
    Date: Mon, 26 Apr 2021 13:30:14 +0200 Subject: [PATCH 224/330] Fall back to natural height and width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index fd559fd3cc..ee89dabc8e 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -91,6 +91,7 @@ export default class ImageView extends React.Component { private contextMenuButton = createRef(); private focusLock = createRef(); private imageWrapper = createRef(); + private image = createRef(); private initX = 0; private initY = 0; @@ -103,8 +104,10 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); + // We want to recalculate zoom whenever the windows size changes window.addEventListener("resize", this.calculateZoom); - this.calculateZoom(); + // After the image loads for the first time we want to calculate the zoom + this.image.current.addEventListener("load", this.calculateZoom); } componentWillUnmount() { @@ -112,12 +115,14 @@ export default class ImageView extends React.Component { } private calculateZoom = () => { - // TODO: What if we don't have width and height props? - + const image = this.image.current; const imageWrapper = this.imageWrapper.current; - const zoomX = imageWrapper.clientWidth / this.props.width; - const zoomY = imageWrapper.clientHeight / this.props.height; + const width = this.props.width || image.naturalWidth; + const height = this.props.height || image.naturalHeight; + + const zoomX = imageWrapper.clientWidth / width; + const zoomY = imageWrapper.clientHeight / height; // We set minZoom to the min of the zoomX and zoomY to avoid overflow in // any direction. We also multiply by MAX_SCALE to get a gap around the // image by default @@ -454,6 +459,7 @@ export default class ImageView extends React.Component { src={this.props.src} title={this.props.name} style={style} + ref={this.image} className="mx_ImageView_image" draggable={true} onMouseDown={this.onStartMoving} From 8656212eb93d2d25f8c74a3ec2e6227e8ae1fbae Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Apr 2021 12:41:04 +0100 Subject: [PATCH 225/330] Space creation prompt user to add existing rooms instead of creating new ones --- res/css/structures/_SpaceRoomView.scss | 10 + .../dialogs/_AddExistingToSpaceDialog.scss | 118 +++++------ src/components/structures/SpaceRoomView.tsx | 71 ++++++- .../dialogs/AddExistingToSpaceDialog.tsx | 184 ++++++++++-------- src/i18n/strings/en_EN.json | 6 +- 5 files changed, 244 insertions(+), 145 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 269f16beb7..553919d862 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -81,6 +81,16 @@ $SpaceRoomViewInnerWidth: 428px; color: $secondary-fg-color; margin-top: 12px; margin-bottom: 24px; + max-width: $SpaceRoomViewInnerWidth; + } + + .mx_AddExistingToSpace { + max-width: $SpaceRoomViewInnerWidth; + + .mx_AddExistingToSpace_content { + height: calc(100vh - 360px); + max-height: 400px; + } } .mx_SpaceRoomView_buttons { diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 247df52b4a..84f965e6bd 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -21,6 +21,66 @@ limitations under the License. } } +.mx_AddExistingToSpace { + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_AddExistingToSpace_content { + flex-grow: 1; + } + + .mx_AddExistingToSpace_noResults { + display: block; + margin-top: 24px; + } + + .mx_AddExistingToSpace_section { + &:not(:first-child) { + margin-top: 24px; + } + + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_AddExistingToSpace_entry { + display: flex; + margin-top: 12px; + + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_AddExistingToSpace_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } + + .mx_Checkbox { + align-items: center; + } + } + } + + .mx_AddExistingToSpace_section_spaces { + .mx_BaseAvatar_image { + border-radius: 8px; + } + } +} + .mx_AddExistingToSpaceDialog { width: 480px; color: $primary-fg-color; @@ -100,12 +160,6 @@ limitations under the License. } } - .mx_SearchBox { - // To match the space around the title - margin: 0 0 15px 0; - flex-grow: 0; - } - .mx_AddExistingToSpaceDialog_errorText { font-weight: $font-semi-bold; font-size: $font-12px; @@ -114,56 +168,8 @@ limitations under the License. margin-bottom: 28px; } - .mx_AddExistingToSpaceDialog_content { - flex-grow: 1; - - .mx_AddExistingToSpaceDialog_noResults { - display: block; - margin-top: 24px; - } - } - - .mx_AddExistingToSpaceDialog_section { - &:not(:first-child) { - margin-top: 24px; - } - - > h3 { - margin: 0; - color: $secondary-fg-color; - font-size: $font-12px; - font-weight: $font-semi-bold; - line-height: $font-15px; - } - - .mx_AddExistingToSpaceDialog_entry { - display: flex; - margin-top: 12px; - - .mx_BaseAvatar { - margin-right: 12px; - } - - .mx_AddExistingToSpaceDialog_entry_name { - font-size: $font-15px; - line-height: 30px; - flex-grow: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin-right: 12px; - } - - .mx_Checkbox { - align-items: center; - } - } - } - - .mx_AddExistingToSpaceDialog_section_spaces { - .mx_BaseAvatar_image { - border-radius: 8px; - } + .mx_AddExistingToSpace { + display: contents; } .mx_AddExistingToSpaceDialog_footer { diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 31358a3731..cdf9dc02d3 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -51,6 +51,9 @@ import MemberAvatar from "../views/avatars/MemberAvatar"; import {useStateToggle} from "../../hooks/useStateToggle"; import SpaceStore from "../../stores/SpaceStore"; import FacePile from "../views/elements/FacePile"; +import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; +import {allSettled} from "../../utils/promise"; +import {calculateRoomVia} from "../../utils/permalinks/Permalinks"; interface IProps { space: Room; @@ -354,7 +357,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { let buttonLabel = _t("Skip for now"); if (roomNames.some(name => name.trim())) { onClick = onNextClick; - buttonLabel = busy ? _t("Creating rooms...") : _t("Continue") + buttonLabel = busy ? _t("Creating rooms...") : _t("Continue"); } return
    @@ -376,6 +379,65 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
    ; }; +const SpaceAddExistingRooms = ({ space, onFinished }) => { + const [selectedToAdd, setSelectedToAdd] = useState(new Set()); + + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + let onClick = onFinished; + let buttonLabel = _t("Skip for now"); + if (selectedToAdd.size > 0) { + onClick = async () => { + // TODO rate limiting + setBusy(true); + try { + await allSettled(Array.from(selectedToAdd).map((room) => + SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room)))); + onFinished(true); + } catch (e) { + console.error("Failed to add rooms to space", e); + setError(_t("Failed to add rooms to space")); + } + setBusy(false); + }; + buttonLabel = busy ? _t("Adding...") : _t("Add"); + } + + return
    +

    { _t("What do you want to organise?") }

    +
    + { _t("Pick rooms or conversations to add. This is just a space for you, " + + "no one will be informed. You can add more later.") } +
    + + { error &&
    { error }
    } + + { + if (checked) { + selectedToAdd.add(room); + } else { + selectedToAdd.delete(room); + } + setSelectedToAdd(new Set(selectedToAdd)); + }} + /> + +
    + + { buttonLabel } + +
    +
    ; +}; + const SpaceSetupPublicShare = ({ space, onFinished }) => { return

    { _t("Share %(name)s", { name: space.name }) }

    @@ -659,7 +721,7 @@ export default class SpaceRoomView extends React.PureComponent { return { - this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms }); + this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms }); }} />; case Phase.PrivateInvite: @@ -675,6 +737,11 @@ export default class SpaceRoomView extends React.PureComponent { "You can add more later too, including already existing ones.")} onFinished={() => this.setState({ phase: Phase.Landing })} />; + case Phase.PrivateExistingRooms: + return this.setState({ phase: Phase.Landing })} + />; } } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 0f58a624f3..e65a709eb0 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState} from "react"; +import React, {useContext, useState} from "react"; import classNames from "classnames"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; @@ -33,6 +33,7 @@ import {allSettled} from "../../../utils/promise"; import DMRoomMap from "../../../utils/DMRoomMap"; import {calculateRoomVia} from "../../../utils/permalinks/Permalinks"; import StyledCheckbox from "../elements/StyledCheckbox"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -41,31 +42,35 @@ interface IProps extends IDialogProps { } const Entry = ({ room, checked, onChange }) => { - return
    + return
    - { room.name } + { room.name } onChange(e.target.checked)} checked={checked} />
    ; }; -const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { +interface IAddExistingToSpaceProps { + space: Room; + selected: Set; + onChange(checked: boolean, room: Room): void; +} + +export const AddExistingToSpace: React.FC = ({ space, selected, onChange }) => { + const cli = useContext(MatrixClientContext); const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); - const [selectedSpace, setSelectedSpace] = useState(space); - const [selectedToAdd, setSelectedToAdd] = useState(new Set()); - const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); const existingSubspacesSet = new Set(existingSubspaces); const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId)); - const joinRule = selectedSpace.getJoinRule(); + const joinRule = space.getJoinRule(); const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => { if (room.getMyMembership() !== "join") return arr; if (!room.name.toLowerCase().includes(lcQuery)) return arr; if (room.isSpaceRoom()) { - if (room !== space && room !== selectedSpace && !existingSubspacesSet.has(room)) { + if (room !== space && !existingSubspacesSet.has(room)) { arr[0].push(room); } } else if (!existingRoomsSet.has(room) && joinRule !== "public") { @@ -75,11 +80,79 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, return arr; }, [[], [], []]); + return
    + + + { rooms.length > 0 ? ( +
    +

    { _t("Rooms") }

    + { rooms.map(room => { + return { + onChange(checked, room); + }} + />; + }) } +
    + ) : undefined } + + { spaces.length > 0 ? ( +
    +

    { _t("Spaces") }

    + { spaces.map(space => { + return { + onChange(checked, space); + }} + />; + }) } +
    + ) : null } + + { dms.length > 0 ? ( +
    +

    { _t("Direct Messages") }

    + { dms.map(room => { + return { + onChange(checked, room); + }} + />; + }) } +
    + ) : null } + + { spaces.length + rooms.length + dms.length < 1 ? + { _t("No results") } + : undefined } +
    +
    ; +}; + +const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { + const [selectedSpace, setSelectedSpace] = useState(space); + const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); + const [selectedToAdd, setSelectedToAdd] = useState(new Set()); + const [busy, setBusy] = useState(false); const [error, setError] = useState(""); let spaceOptionSection; - if (existingSubspacesSet.size > 0) { + if (existingSubspaces.length > 0) { const options = [space, ...existingSubspaces].map((space) => { const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, @@ -119,86 +192,26 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, return { error &&
    { error }
    } - - - { rooms.length > 0 ? ( -
    -

    { _t("Rooms") }

    - { rooms.map(room => { - return { - if (checked) { - selectedToAdd.add(room); - } else { - selectedToAdd.delete(room); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
    - ) : undefined } - - { spaces.length > 0 ? ( -
    -

    { _t("Spaces") }

    - { spaces.map(space => { - return { - if (checked) { - selectedToAdd.add(space); - } else { - selectedToAdd.delete(space); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
    - ) : null } - - { dms.length > 0 ? ( -
    -

    { _t("Direct Messages") }

    - { dms.map(space => { - return { - if (checked) { - selectedToAdd.add(space); - } else { - selectedToAdd.delete(space); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
    - ) : null } - - { spaces.length + rooms.length + dms.length < 1 ? - { _t("No results") } - : undefined } -
    + + { + if (checked) { + selectedToAdd.add(room); + } else { + selectedToAdd.delete(room); + } + setSelectedToAdd(new Set(selectedToAdd)); + }} + /> +
    @@ -212,6 +225,7 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, kind="primary" disabled={busy || selectedToAdd.size < 1} onClick={async () => { + // TODO rate limiting setBusy(true); try { await allSettled(Array.from(selectedToAdd).map((room) => diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f1b700540f..6c9d22cdeb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2016,11 +2016,11 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", - "Space selection": "Space selection", - "Add existing rooms": "Add existing rooms", "Filter your rooms and spaces": "Filter your rooms and spaces", "Spaces": "Spaces", "Direct Messages": "Direct Messages", + "Space selection": "Space selection", + "Add existing rooms": "Add existing rooms", "Don't want to add an existing room?": "Don't want to add an existing room?", "Create a new room": "Create a new room", "Failed to add rooms to space": "Failed to add rooms to space", @@ -2665,6 +2665,8 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", + "What do you want to organise?": "What do you want to organise?", + "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", "Share %(name)s": "Share %(name)s", "It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.", "Go to my first room": "Go to my first room", From b741b3112a7a6801cb9d7fed9f3aa33de8ecfbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 13:47:06 +0200 Subject: [PATCH 226/330] If the image is small don't scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index ee89dabc8e..e815e3be92 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -123,21 +123,26 @@ export default class ImageView extends React.Component { const zoomX = imageWrapper.clientWidth / width; const zoomY = imageWrapper.clientHeight / height; + + // If the image is smaller in both dimensions set its the zoom to 1 to + // display it in its original size + if (zoomX >= 1 && zoomY >= 1) { + this.setState({ + zoom: 1, + minZoom: 1, + maxZoom: 1, + }); + return; + } // We set minZoom to the min of the zoomX and zoomY to avoid overflow in // any direction. We also multiply by MAX_SCALE to get a gap around the // image by default const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; - // If minZoom is bigger or equal to 1, it means we scaling the image up - // to fit the viewport and therefore we want to disable zooming, so we - // set the maxZoom to be the same as the minZoom. Otherwise, we are - // scaling the image down - we want the user to be allowed to zoom to - // 100% - const maxZoom = minZoom >= 1 ? minZoom : 1; if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); this.setState({ minZoom: minZoom, - maxZoom: maxZoom, + maxZoom: 1, }); } From dbca370497ceb7897a26f0869a4bc828512f0e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 13:48:14 +0200 Subject: [PATCH 227/330] Try to precalculate the zoom from width and height props MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index e815e3be92..0ad8435ef5 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -108,6 +108,8 @@ export default class ImageView extends React.Component { window.addEventListener("resize", this.calculateZoom); // After the image loads for the first time we want to calculate the zoom this.image.current.addEventListener("load", this.calculateZoom); + // Try to precalculate the zoom from width and height props + this.calculateZoom(); } componentWillUnmount() { From e374fcfe9113fb77856bd76b7a9c6e993a4aaa1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 13:49:29 +0200 Subject: [PATCH 228/330] Fix spelling --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 0ad8435ef5..459b192b43 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -104,7 +104,7 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); - // We want to recalculate zoom whenever the windows size changes + // We want to recalculate zoom whenever the window's size changes window.addEventListener("resize", this.calculateZoom); // After the image loads for the first time we want to calculate the zoom this.image.current.addEventListener("load", this.calculateZoom); From a43ad8d8810ebf7bb5250092d3943e0631761747 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 13:55:14 +0100 Subject: [PATCH 229/330] Allow for multiple locale and stabilise set language call --- src/BasePlatform.ts | 2 +- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index b7bc502dca..b10513b46b 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -258,7 +258,7 @@ export default abstract class BasePlatform { return null; } - async setLanguage(language: string) { + async setLanguage(preferredLangs: string[]) { throw new Error("Unimplemented"); } diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 861e5cb1f8..a216aeccbe 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -194,8 +194,8 @@ export default class GeneralUserSettingsTab extends React.Component { this.setState({language: newLanguage}); const platform = PlatformPeg.get(); if (platform) { - platform.reload(); platform.setLanguage(newLanguage); + platform.reload(); } }; From 3547d1f93b16d25fb9e0b5ce368138774f76c24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 15:01:06 +0200 Subject: [PATCH 230/330] Change cursor to default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 0ad8435ef5..be5ea72d2c 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -321,7 +321,7 @@ export default class ImageView extends React.Component { if (this.state.moving) { cursor= "grabbing"; } else if (this.state.maxZoom === this.state.minZoom) { - cursor = "pointer"; + cursor = "default"; } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; } else { From b8a915bb763c6848ab0c17a666c03b6eb28ae25c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 14:02:53 +0100 Subject: [PATCH 231/330] Tweak private / underscores for fields and methods --- src/ScalarAuthClient.ts | 22 +-- .../eventindex/ManageEventIndexDialog.tsx | 8 +- src/components/structures/auth/SoftLogout.tsx | 8 +- src/components/views/rooms/EventTile.tsx | 113 ++++++++------- .../views/rooms/MessageComposer.tsx | 18 +-- .../views/settings/EventIndexPanel.tsx | 14 +- src/components/views/settings/SetIdServer.tsx | 42 +++--- .../tabs/room/RolesRoomSettingsTab.tsx | 26 ++-- .../tabs/room/SecurityRoomSettingsTab.tsx | 46 +++--- .../tabs/user/HelpUserSettingsTab.tsx | 24 ++-- .../tabs/user/MjolnirUserSettingsTab.tsx | 40 +++--- .../tabs/user/PreferencesUserSettingsTab.tsx | 38 ++--- src/stores/TypingStore.ts | 10 +- src/stores/WidgetEchoStore.ts | 16 +-- src/utils/Timer.ts | 72 +++++----- .../permalinks/ElementPermalinkConstructor.ts | 20 +-- src/utils/permalinks/Permalinks.ts | 132 +++++++++--------- 17 files changed, 324 insertions(+), 325 deletions(-) diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts index 3c63bf8047..c59136263d 100644 --- a/src/ScalarAuthClient.ts +++ b/src/ScalarAuthClient.ts @@ -51,7 +51,7 @@ export default class ScalarAuthClient { this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl; } - _writeTokenToStore() { + private writeTokenToStore() { window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); if (this.isDefaultManager) { // We remove the old token from storage to migrate upwards. This is safe @@ -61,7 +61,7 @@ export default class ScalarAuthClient { } } - _readTokenFromStore() { + private readTokenFromStore() { let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); if (!token && this.isDefaultManager) { token = window.localStorage.getItem("mx_scalar_token"); @@ -69,9 +69,9 @@ export default class ScalarAuthClient { return token; } - _readToken() { + private readToken() { if (this.scalarToken) return this.scalarToken; - return this._readTokenFromStore(); + return this.readTokenFromStore(); } setTermsInteractionCallback(callback) { @@ -90,12 +90,12 @@ export default class ScalarAuthClient { // Returns a promise that resolves to a scalar_token string getScalarToken() { - const token = this._readToken(); + const token = this.readToken(); if (!token) { return this.registerForToken(); } else { - return this._checkToken(token).catch((e) => { + return this.checkToken(token).catch((e) => { if (e instanceof TermsNotSignedError) { // retrying won't help this throw e; @@ -105,7 +105,7 @@ export default class ScalarAuthClient { } } - _getAccountName(token) { + private getAccountName(token) { const url = this.apiUrl + "/account"; return new Promise(function(resolve, reject) { @@ -130,8 +130,8 @@ export default class ScalarAuthClient { }); } - _checkToken(token) { - return this._getAccountName(token).then(userId => { + private checkToken(token) { + return this.getAccountName(token).then(userId => { const me = MatrixClientPeg.get().getUserId(); if (userId !== me) { throw new Error("Scalar token is owned by someone else: " + me); @@ -177,10 +177,10 @@ export default class ScalarAuthClient { return this.exchangeForScalarToken(tokenObject); }).then((token) => { // Validate it (this mostly checks to see if the IM needs us to agree to some terms) - return this._checkToken(token); + return this.checkToken(token); }).then((token) => { this.scalarToken = token; - this._writeTokenToStore(); + this.writeTokenToStore(); return token; }); } diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index fd592e6357..8864e043aa 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -131,14 +131,14 @@ export default class ManageEventIndexDialog extends React.Component { + private onDisable = async () => { Modal.createTrackedDialogAsync("Disable message search", "Disable message search", import("./DisableEventIndexDialog"), null, null, /* priority = */ false, /* static = */ true, ); }; - _onCrawlerSleepTimeChange = (e) => { + private onCrawlerSleepTimeChange = (e) => { this.setState({crawlerSleepTime: e.target.value}); SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); }; @@ -177,7 +177,7 @@ export default class ManageEventIndexDialog extends React.Component + onChange={this.onCrawlerSleepTimeChange} />
    ); @@ -196,7 +196,7 @@ export default class ManageEventIndexDialog extends React.Component diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 6b24535657..fa9207efdd 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -83,7 +83,7 @@ export default class SoftLogout extends React.Component { return; } - this._initLogin(); + this.initLogin(); const cli = MatrixClientPeg.get(); if (cli.isCryptoEnabled()) { @@ -105,7 +105,7 @@ export default class SoftLogout extends React.Component { }); }; - async _initLogin() { + private async initLogin() { const queryParams = this.props.realQueryParams; const hasAllParams = queryParams && queryParams['loginToken']; if (hasAllParams) { @@ -200,7 +200,7 @@ export default class SoftLogout extends React.Component { }); } - _renderSignInSection() { + private renderSignInSection() { if (this.state.loginView === LOGIN_VIEW.LOADING) { const Spinner = sdk.getComponent("elements.Spinner"); return ; @@ -300,7 +300,7 @@ export default class SoftLogout extends React.Component {

    {_t("Sign in")}

    - {this._renderSignInSection()} + {this.renderSignInSection()}

    {_t("Clear personal data")}

    diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index f2b70fd093..33bc4951a8 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -293,10 +293,10 @@ interface IState { @replaceableComponent("views.rooms.EventTile") export default class EventTile extends React.Component { - private _suppressReadReceiptAnimation: boolean; - private _isListeningForReceipts: boolean; - private _tile = React.createRef(); - private _replyThread = React.createRef(); + private suppressReadReceiptAnimation: boolean; + private isListeningForReceipts: boolean; + private tile = React.createRef(); + private replyThread = React.createRef(); static defaultProps = { // no-op function because onHeightChanged is optional yet some sub-components assume its existence @@ -323,23 +323,22 @@ export default class EventTile extends React.Component { }; // don't do RR animations until we are mounted - this._suppressReadReceiptAnimation = true; + this.suppressReadReceiptAnimation = true; // Throughout the component we manage a read receipt listener to see if our tile still // qualifies for a "sent" or "sending" state (based on their relevant conditions). We // don't want to over-subscribe to the read receipt events being fired, so we use a flag // to determine if we've already subscribed and use a combination of other flags to find // out if we should even be subscribed at all. - this._isListeningForReceipts = false; + this.isListeningForReceipts = false; } /** * When true, the tile qualifies for some sort of special read receipt. This could be a 'sending' * or 'sent' receipt, for example. * @returns {boolean} - * @private */ - get _isEligibleForSpecialReceipt() { + private get isEligibleForSpecialReceipt() { // First, if there are other read receipts then just short-circuit this. if (this.props.readReceipts && this.props.readReceipts.length > 0) return false; if (!this.props.mxEvent) return false; @@ -368,9 +367,9 @@ export default class EventTile extends React.Component { return true; } - get _shouldShowSentReceipt() { + private get shouldShowSentReceipt() { // If we're not even eligible, don't show the receipt. - if (!this._isEligibleForSpecialReceipt) return false; + if (!this.isEligibleForSpecialReceipt) return false; // We only show the 'sent' receipt on the last successful event. if (!this.props.lastSuccessful) return false; @@ -388,9 +387,9 @@ export default class EventTile extends React.Component { return true; } - get _shouldShowSendingReceipt() { + private get shouldShowSendingReceipt() { // If we're not even eligible, don't show the receipt. - if (!this._isEligibleForSpecialReceipt) return false; + if (!this.isEligibleForSpecialReceipt) return false; // Check the event send status to see if we are pending. Null/undefined status means the // message was sent, so check for that and 'sent' explicitly. @@ -404,22 +403,22 @@ export default class EventTile extends React.Component { // TODO: [REACT-WARNING] Move into constructor // eslint-disable-next-line camelcase UNSAFE_componentWillMount() { - this._verifyEvent(this.props.mxEvent); + this.verifyEvent(this.props.mxEvent); } componentDidMount() { - this._suppressReadReceiptAnimation = false; + this.suppressReadReceiptAnimation = false; const client = this.context; client.on("deviceVerificationChanged", this.onDeviceVerificationChanged); client.on("userTrustStatusChanged", this.onUserVerificationChanged); - this.props.mxEvent.on("Event.decrypted", this._onDecrypted); + this.props.mxEvent.on("Event.decrypted", this.onDecrypted); if (this.props.showReactions) { - this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated); + this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated); } - if (this._shouldShowSentReceipt || this._shouldShowSendingReceipt) { - client.on("Room.receipt", this._onRoomReceipt); - this._isListeningForReceipts = true; + if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { + client.on("Room.receipt", this.onRoomReceipt); + this.isListeningForReceipts = true; } } @@ -429,7 +428,7 @@ export default class EventTile extends React.Component { // re-check the sender verification as outgoing events progress through // the send process. if (nextProps.eventSendStatus !== this.props.eventSendStatus) { - this._verifyEvent(nextProps.mxEvent); + this.verifyEvent(nextProps.mxEvent); } } @@ -438,35 +437,35 @@ export default class EventTile extends React.Component { return true; } - return !this._propsEqual(this.props, nextProps); + return !this.propsEqual(this.props, nextProps); } componentWillUnmount() { const client = this.context; client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); - client.removeListener("Room.receipt", this._onRoomReceipt); - this._isListeningForReceipts = false; - this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); + client.removeListener("Room.receipt", this.onRoomReceipt); + this.isListeningForReceipts = false; + this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted); if (this.props.showReactions) { - this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated); + this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated); } } componentDidUpdate(prevProps, prevState, snapshot) { // If we're not listening for receipts and expect to be, register a listener. - if (!this._isListeningForReceipts && (this._shouldShowSentReceipt || this._shouldShowSendingReceipt)) { - this.context.on("Room.receipt", this._onRoomReceipt); - this._isListeningForReceipts = true; + if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) { + this.context.on("Room.receipt", this.onRoomReceipt); + this.isListeningForReceipts = true; } } - _onRoomReceipt = (ev, room) => { + private onRoomReceipt = (ev, room) => { // ignore events for other rooms const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); if (room !== tileRoom) return; - if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt && !this._isListeningForReceipts) { + if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) { return; } @@ -474,36 +473,36 @@ export default class EventTile extends React.Component { // the getters we use here to determine what needs rendering. this.forceUpdate(() => { // Per elsewhere in this file, we can remove the listener once we will have no further purpose for it. - if (!this._shouldShowSentReceipt && !this._shouldShowSendingReceipt) { - this.context.removeListener("Room.receipt", this._onRoomReceipt); - this._isListeningForReceipts = false; + if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt) { + this.context.removeListener("Room.receipt", this.onRoomReceipt); + this.isListeningForReceipts = false; } }); }; /** called when the event is decrypted after we show it. */ - _onDecrypted = () => { + private onDecrypted = () => { // we need to re-verify the sending device. - // (we call onHeightChanged in _verifyEvent to handle the case where decryption + // (we call onHeightChanged in verifyEvent to handle the case where decryption // has caused a change in size of the event tile) - this._verifyEvent(this.props.mxEvent); + this.verifyEvent(this.props.mxEvent); this.forceUpdate(); }; - onDeviceVerificationChanged = (userId, device) => { + private onDeviceVerificationChanged = (userId, device) => { if (userId === this.props.mxEvent.getSender()) { - this._verifyEvent(this.props.mxEvent); + this.verifyEvent(this.props.mxEvent); } }; - onUserVerificationChanged = (userId, _trustStatus) => { + private onUserVerificationChanged = (userId, _trustStatus) => { if (userId === this.props.mxEvent.getSender()) { - this._verifyEvent(this.props.mxEvent); + this.verifyEvent(this.props.mxEvent); } }; - async _verifyEvent(mxEvent) { + private async verifyEvent(mxEvent) { if (!mxEvent.isEncrypted()) { return; } @@ -557,7 +556,7 @@ export default class EventTile extends React.Component { }, this.props.onHeightChanged); // Decryption may have caused a change in size } - _propsEqual(objA, objB) { + private propsEqual(objA, objB) { const keysA = Object.keys(objA); const keysB = Object.keys(objB); @@ -624,7 +623,7 @@ export default class EventTile extends React.Component { }; getReadAvatars() { - if (this._shouldShowSentReceipt || this._shouldShowSendingReceipt) { + if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { return ; } @@ -671,7 +670,7 @@ export default class EventTile extends React.Component { leftOffset={left} hidden={hidden} readReceiptInfo={readReceiptInfo} checkUnmounting={this.props.checkUnmounting} - suppressAnimation={this._suppressReadReceiptAnimation} + suppressAnimation={this.suppressReadReceiptAnimation} onClick={this.toggleAllReadAvatars} timestamp={receipt.ts} showTwelveHour={this.props.isTwelveHour} @@ -728,7 +727,7 @@ export default class EventTile extends React.Component { }); }; - _renderE2EPadlock() { + private renderE2EPadlock() { const ev = this.props.mxEvent; // event could not be decrypted @@ -777,9 +776,9 @@ export default class EventTile extends React.Component { }); }; - getTile = () => this._tile.current; + getTile = () => this.tile.current; - getReplyThread = () => this._replyThread.current; + getReplyThread = () => this.replyThread.current; getReactions = () => { if ( @@ -799,11 +798,11 @@ export default class EventTile extends React.Component { return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); }; - _onReactionsCreated = (relationType, eventType) => { + private onReactionsCreated = (relationType, eventType) => { if (relationType !== "m.annotation" || eventType !== "m.reaction") { return; } - this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated); + this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated); this.setState({ reactions: this.getReactions(), }); @@ -1017,8 +1016,8 @@ export default class EventTile extends React.Component { const useIRCLayout = this.props.layout == Layout.IRC; const groupTimestamp = !useIRCLayout ? linkedTimestamp : null; const ircTimestamp = useIRCLayout ? linkedTimestamp : null; - const groupPadlock = !useIRCLayout && !isBubbleMessage && this._renderE2EPadlock(); - const ircPadlock = useIRCLayout && !isBubbleMessage && this._renderE2EPadlock(); + const groupPadlock = !useIRCLayout && !isBubbleMessage && this.renderE2EPadlock(); + const ircPadlock = useIRCLayout && !isBubbleMessage && this.renderE2EPadlock(); let msgOption; if (this.props.showReadReceipts) { @@ -1049,7 +1048,7 @@ export default class EventTile extends React.Component {
    - { return (
    - { this.props.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, - this._replyThread, + this.replyThread, ); } return ( @@ -1108,7 +1107,7 @@ export default class EventTile extends React.Component { { groupTimestamp } { groupPadlock } { thread } - { this.props.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, - this._replyThread, + this.replyThread, this.props.layout, ); @@ -1139,7 +1138,7 @@ export default class EventTile extends React.Component { { groupTimestamp } { groupPadlock } { thread } - { - private _uploadInput = React.createRef(); - private _dispatcherRef: string; + private uploadInput = React.createRef(); + private dispatcherRef: string; constructor(props) { super(props); - this._dispatcherRef = dis.register(this.onAction); + this.dispatcherRef = dis.register(this.onAction); } componentWillUnmount() { - dis.unregister(this._dispatcherRef); + dis.unregister(this.dispatcherRef); } private onAction = payload => { @@ -132,7 +132,7 @@ class UploadButton extends React.Component { dis.dispatch({action: 'require_registration'}); return; } - this._uploadInput.current.click(); + this.uploadInput.current.click(); } private onUploadFileInputChange = (ev) => { @@ -165,7 +165,7 @@ class UploadButton extends React.Component { title={_t('Upload file')} > { constructor(props) { super(props); - VoiceRecordingStore.instance.on(UPDATE_EVENT, this._onVoiceStoreUpdate); + VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate); this.state = { tombstone: this.getRoomTombstone(), @@ -249,7 +249,7 @@ export default class MessageComposer extends React.Component { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); } - VoiceRecordingStore.instance.off(UPDATE_EVENT, this._onVoiceStoreUpdate); + VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate); dis.unregister(this.dispatcherRef); } @@ -331,7 +331,7 @@ export default class MessageComposer extends React.Component { }); } - _onVoiceStoreUpdate = () => { + private onVoiceStoreUpdate = () => { const recording = VoiceRecordingStore.instance.activeRecording; this.setState({haveRecording: !!recording}); if (recording) { diff --git a/src/components/views/settings/EventIndexPanel.tsx b/src/components/views/settings/EventIndexPanel.tsx index c97b436854..fa84063ee8 100644 --- a/src/components/views/settings/EventIndexPanel.tsx +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -109,7 +109,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> { }); } - _onManage = async () => { + private onManage = async () => { Modal.createTrackedDialogAsync('Message search', 'Message search', // @ts-ignore: TS doesn't seem to like the type of this now that it // has also been converted to TS as well, but I can't figure out why... @@ -120,7 +120,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> { ); } - _onEnable = async () => { + private onEnable = async () => { this.setState({ enabling: true, }); @@ -132,13 +132,13 @@ export default class EventIndexPanel extends React.Component<{}, IState> { await this.updateState(); } - _confirmEventStoreReset = () => { + private confirmEventStoreReset = () => { const { close } = Modal.createDialog(SeshatResetDialog, { onFinished: async (success) => { if (success) { await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); - await this._onEnable(); + await this.onEnable(); close(); } }, @@ -165,7 +165,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> { }, )}
    - + {_t("Manage")}
    @@ -180,7 +180,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> { )}
    + onClick={this.onEnable}> {_t("Enable")} {this.state.enabling ? :
    } @@ -242,7 +242,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> { {EventIndexPeg.error.message}

    - + {_t("Reset")}

    diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index a3dbcc7830..70a4c46f69 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -107,7 +107,7 @@ export default class SetIdServer extends React.Component { dis.unregister(this.dispatcherRef); } - onAction = (payload) => { + private onAction = (payload) => { // We react to changes in the ID server in the event the user is staring at this form // when changing their identity server on another device. if (payload.action !== "id_server_changed") return; @@ -117,13 +117,13 @@ export default class SetIdServer extends React.Component { }); }; - _onIdentityServerChanged = (ev) => { + private onIdentityServerChanged = (ev) => { const u = ev.target.value; this.setState({idServer: u}); }; - _getTooltip = () => { + private getTooltip = () => { if (this.state.checking) { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); return
    @@ -137,11 +137,11 @@ export default class SetIdServer extends React.Component { } }; - _idServerChangeEnabled = () => { + private idServerChangeEnabled = () => { return !!this.state.idServer && !this.state.busy; }; - _saveIdServer = (fullUrl) => { + private saveIdServer = (fullUrl) => { // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { base_url: fullUrl, @@ -154,7 +154,7 @@ export default class SetIdServer extends React.Component { }); }; - _checkIdServer = async (e) => { + private checkIdServer = async (e) => { e.preventDefault(); const { idServer, currentClientIdServer } = this.state; @@ -177,14 +177,14 @@ export default class SetIdServer extends React.Component { // Double check that the identity server even has terms of service. const hasTerms = await doesIdentityServerHaveTerms(fullUrl); if (!hasTerms) { - const [confirmed] = await this._showNoTermsWarning(fullUrl); + const [confirmed] = await this.showNoTermsWarning(fullUrl); save = confirmed; } // Show a general warning, possibly with details about any bound // 3PIDs that would be left behind. if (save && currentClientIdServer && fullUrl !== currentClientIdServer) { - const [confirmed] = await this._showServerChangeWarning({ + const [confirmed] = await this.showServerChangeWarning({ title: _t("Change identity server"), unboundMessage: _t( "Disconnect from the identity server and " + @@ -200,7 +200,7 @@ export default class SetIdServer extends React.Component { } if (save) { - this._saveIdServer(fullUrl); + this.saveIdServer(fullUrl); } } catch (e) { console.error(e); @@ -215,7 +215,7 @@ export default class SetIdServer extends React.Component { }); }; - _showNoTermsWarning(fullUrl) { + private showNoTermsWarning(fullUrl) { const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { title: _t("Identity server has no terms of service"), @@ -234,10 +234,10 @@ export default class SetIdServer extends React.Component { return finished; } - _onDisconnectClicked = async () => { + private onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); try { - const [confirmed] = await this._showServerChangeWarning({ + const [confirmed] = await this.showServerChangeWarning({ title: _t("Disconnect identity server"), unboundMessage: _t( "Disconnect from the identity server ?", {}, @@ -246,14 +246,14 @@ export default class SetIdServer extends React.Component { button: _t("Disconnect"), }); if (confirmed) { - this._disconnectIdServer(); + this.disconnectIdServer(); } } finally { this.setState({disconnectBusy: false}); } }; - async _showServerChangeWarning({ title, unboundMessage, button }) { + private async showServerChangeWarning({ title, unboundMessage, button }) { const { currentClientIdServer } = this.state; let threepids = []; @@ -329,7 +329,7 @@ export default class SetIdServer extends React.Component { return finished; } - _disconnectIdServer = () => { + private disconnectIdServer = () => { // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { base_url: null, // clear @@ -402,14 +402,14 @@ export default class SetIdServer extends React.Component { } discoSection =
    {discoBodyText} - + {discoButtonContent}
    ; } return ( -
    + {sectionTitle} @@ -422,15 +422,15 @@ export default class SetIdServer extends React.Component { autoComplete="off" placeholder={this.state.defaultIdServer} value={this.state.idServer} - onChange={this._onIdentityServerChanged} - tooltipContent={this._getTooltip()} + onChange={this.onIdentityServerChanged} + tooltipContent={this.getTooltip()} tooltipClassName="mx_SetIdServer_tooltip" disabled={this.state.busy} forceValidity={this.state.error ? false : null} /> {_t("Change")} {discoSection} diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index f5fd537918..a945b22d1f 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -71,7 +71,7 @@ interface IBannedUserProps { } export class BannedUser extends React.Component { - _onUnbanClick = (e) => { + private onUnbanClick = (e) => { MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to unban: " + err); @@ -89,7 +89,7 @@ export class BannedUser extends React.Component { unbanButton = ( { _t('Unban') } @@ -116,22 +116,22 @@ interface IProps { @replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab") export default class RolesRoomSettingsTab extends React.Component { componentDidMount(): void { - MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership); + MatrixClientPeg.get().on("RoomState.members", this.onRoomMembership); } componentWillUnmount(): void { const client = MatrixClientPeg.get(); if (client) { - client.removeListener("RoomState.members", this._onRoomMembership); + client.removeListener("RoomState.members", this.onRoomMembership); } } - _onRoomMembership = (event, state, member) => { + private onRoomMembership = (event, state, member) => { if (state.roomId !== this.props.roomId) return; this.forceUpdate(); }; - _populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) { + private populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) { for (const desiredEvent of Object.keys(plEventsToShow)) { if (!(desiredEvent in eventsSection)) { eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel); @@ -139,7 +139,7 @@ export default class RolesRoomSettingsTab extends React.Component { } } - _onPowerLevelsChanged = (value, powerLevelKey) => { + private onPowerLevelsChanged = (value, powerLevelKey) => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); @@ -184,7 +184,7 @@ export default class RolesRoomSettingsTab extends React.Component { }); }; - _onUserPowerLevelChanged = (value, powerLevelKey) => { + private onUserPowerLevelChanged = (value, powerLevelKey) => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); @@ -268,7 +268,7 @@ export default class RolesRoomSettingsTab extends React.Component { currentUserLevel = defaultUserLevel; } - this._populateDefaultPlEvents( + this.populateDefaultPlEvents( eventsLevels, parseIntWithDefault(plContent.state_default, powerLevelDescriptors.state_default.defaultValue), parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue), @@ -290,7 +290,7 @@ export default class RolesRoomSettingsTab extends React.Component { label={user} key={user} powerLevelKey={user} // Will be sent as the second parameter to `onChange` - onChange={this._onUserPowerLevelChanged} + onChange={this.onUserPowerLevelChanged} />, ); } else if (userLevels[user] < defaultUserLevel) { // muted @@ -301,7 +301,7 @@ export default class RolesRoomSettingsTab extends React.Component { label={user} key={user} powerLevelKey={user} // Will be sent as the second parameter to `onChange` - onChange={this._onUserPowerLevelChanged} + onChange={this.onUserPowerLevelChanged} />, ); } @@ -376,7 +376,7 @@ export default class RolesRoomSettingsTab extends React.Component { usersDefault={defaultUserLevel} disabled={!canChangeLevels || currentUserLevel < value} powerLevelKey={key} // Will be sent as the second parameter to `onChange` - onChange={this._onPowerLevelsChanged} + onChange={this.onPowerLevelsChanged} />
    ; }); @@ -401,7 +401,7 @@ export default class RolesRoomSettingsTab extends React.Component { usersDefault={defaultUserLevel} disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]} powerLevelKey={"event_levels_" + eventType} - onChange={this._onPowerLevelsChanged} + onChange={this.onPowerLevelsChanged} />
    ); diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index a8cd920eb2..07776a5a54 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -59,42 +59,42 @@ export default class SecurityRoomSettingsTab extends React.Component { // eslint-disable-line camelcase - MatrixClientPeg.get().on("RoomState.events", this._onStateEvent); + MatrixClientPeg.get().on("RoomState.events", this.onStateEvent); const room = MatrixClientPeg.get().getRoom(this.props.roomId); const state = room.currentState; - const joinRule: JoinRule = this._pullContentPropertyFromEvent( + const joinRule: JoinRule = this.pullContentPropertyFromEvent( state.getStateEvents("m.room.join_rules", ""), 'join_rule', 'invite', ); - const guestAccess: GuestAccess = this._pullContentPropertyFromEvent( + const guestAccess: GuestAccess = this.pullContentPropertyFromEvent( state.getStateEvents("m.room.guest_access", ""), 'guest_access', 'forbidden', ); - const history: History = this._pullContentPropertyFromEvent( + const history: History = this.pullContentPropertyFromEvent( state.getStateEvents("m.room.history_visibility", ""), 'history_visibility', 'shared', ); const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId); this.setState({joinRule, guestAccess, history, encrypted}); - const hasAliases = await this._hasAliases(); + const hasAliases = await this.hasAliases(); this.setState({hasAliases}); } - _pullContentPropertyFromEvent(event, key, defaultValue) { + private pullContentPropertyFromEvent(event, key, defaultValue) { if (!event || !event.getContent()) return defaultValue; return event.getContent()[key] || defaultValue; } componentWillUnmount(): void { - MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent); + MatrixClientPeg.get().removeListener("RoomState.events", this.onStateEvent); } - _onStateEvent = (e) => { + private onStateEvent = (e) => { const refreshWhenTypes = [ 'm.room.join_rules', 'm.room.guest_access', @@ -104,7 +104,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private onEncryptionChange = (e) => { Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, { title: _t('Enable encryption?'), description: _t( @@ -137,7 +137,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private fixGuestAccess = (e) => { e.preventDefault(); e.stopPropagation(); @@ -159,7 +159,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private onRoomAccessRadioToggle = (roomAccess) => { // join_rule // INVITE | PUBLIC // ----------------------+---------------- @@ -205,7 +205,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private onHistoryRadioToggle = (history) => { const beforeHistory = this.state.history; this.setState({history: history}); MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", { @@ -216,11 +216,11 @@ export default class SecurityRoomSettingsTab extends React.Component { + private updateBlacklistDevicesFlag = (checked) => { MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked); }; - async _hasAliases() { + private async hasAliases() { const cli = MatrixClientPeg.get(); if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) { const response = await cli.unstableGetLocalAliases(this.props.roomId); @@ -234,7 +234,7 @@ export default class SecurityRoomSettingsTab extends React.Component {_t("Guests cannot join this room even if explicitly invited.")}  - {_t("Click here to fix")} + {_t("Click here to fix")}
    ); @@ -275,7 +275,7 @@ export default class SecurityRoomSettingsTab extends React.Component; } @@ -366,7 +366,7 @@ export default class SecurityRoomSettingsTab extends React.Component {_t("Who can read history?")}
    - {this._renderHistory()} + {this.renderHistory()}
    ); if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) { @@ -383,7 +383,7 @@ export default class SecurityRoomSettingsTab extends React.Component {_t("Once enabled, encryption cannot be disabled.")}
    -
    @@ -392,7 +392,7 @@ export default class SecurityRoomSettingsTab extends React.Component{_t("Who can access this room?")}
    - {this._renderRoomAccess()} + {this.renderRoomAccess()}
    {historySection} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index b620088096..a009ead064 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -56,7 +56,7 @@ export default class HelpUserSettingsTab extends React.Component }); } - _onClearCacheAndReload = (e) => { + private onClearCacheAndReload = (e) => { if (!PlatformPeg.get()) return; // Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly @@ -68,7 +68,7 @@ export default class HelpUserSettingsTab extends React.Component }); }; - _onBugReport = (e) => { + private onBugReport = (e) => { const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); if (!BugReportDialog) { return; @@ -76,7 +76,7 @@ export default class HelpUserSettingsTab extends React.Component Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }; - _onStartBotChat = (e) => { + private onStartBotChat = (e) => { this.props.closeSettingsFn(); createRoom({ dmUserId: SdkConfig.get().welcomeUserId, @@ -84,7 +84,7 @@ export default class HelpUserSettingsTab extends React.Component }); }; - _showSpoiler = (event) => { + private showSpoiler = (event) => { const target = event.target; target.innerHTML = target.getAttribute('data-spoiler'); @@ -96,7 +96,7 @@ export default class HelpUserSettingsTab extends React.Component selection.addRange(range); }; - _renderLegal() { + private renderLegal() { const tocLinks = SdkConfig.get().terms_and_conditions_links; if (!tocLinks) return null; @@ -117,7 +117,7 @@ export default class HelpUserSettingsTab extends React.Component ); } - _renderCredits() { + private renderCredits() { // Note: This is not translated because it is legal text. // Also,   is ugly but necessary. return ( @@ -191,7 +191,7 @@ export default class HelpUserSettingsTab extends React.Component }, )}
    - + {_t("Chat with %(brand)s Bot", { brand })}
    @@ -223,7 +223,7 @@ export default class HelpUserSettingsTab extends React.Component "other users. They do not contain messages.", )}
    - + {_t("Submit debug logs")}
    @@ -262,21 +262,21 @@ export default class HelpUserSettingsTab extends React.Component {updateButton}
    - {this._renderLegal()} - {this._renderCredits()} + {this.renderLegal()} + {this.renderCredits()}
    {_t("Advanced")}
    {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
    {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
    {_t("Access Token:") + ' '} - <{ _t("click to reveal") }>
    - + {_t("Clear cache and reload")}
    diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index a265016d1a..6997defea9 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -43,15 +43,15 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> }; } - _onPersonalRuleChanged = (e) => { + private onPersonalRuleChanged = (e) => { this.setState({newPersonalRule: e.target.value}); }; - _onNewListChanged = (e) => { + private onNewListChanged = (e) => { this.setState({newList: e.target.value}); }; - _onAddPersonalRule = async (e) => { + private onAddPersonalRule = async (e) => { e.preventDefault(); e.stopPropagation(); @@ -78,7 +78,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } }; - _onSubscribeList = async (e) => { + private onSubscribeList = async (e) => { e.preventDefault(); e.stopPropagation(); @@ -100,7 +100,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } }; - async _removePersonalRule(rule: ListRule) { + private async removePersonalRule(rule: ListRule) { this.setState({busy: true}); try { const list = Mjolnir.sharedInstance().getPersonalList(); @@ -118,7 +118,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } } - async _unsubscribeFromList(list: BanList) { + private async unsubscribeFromList(list: BanList) { this.setState({busy: true}); try { await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId); @@ -136,7 +136,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> } } - _viewListRules(list: BanList) { + private viewListRules(list: BanList) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const room = MatrixClientPeg.get().getRoom(list.roomId); @@ -167,7 +167,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> }); } - _renderPersonalBanListRules() { + private renderPersonalBanListRules() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const list = Mjolnir.sharedInstance().getPersonalList(); @@ -180,7 +180,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
  • this._removePersonalRule(rule)} + onClick={() => this.removePersonalRule(rule)} disabled={this.state.busy} > {_t("Remove")} @@ -198,7 +198,7 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> ); } - _renderSubscribedBanLists() { + private renderSubscribedBanLists() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const personalList = Mjolnir.sharedInstance().getPersonalList(); @@ -215,14 +215,14 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState>
  • this._unsubscribeFromList(list)} + onClick={() => this.unsubscribeFromList(list)} disabled={this.state.busy} > {_t("Unsubscribe")}   this._viewListRules(list)} + onClick={() => this.viewListRules(list)} disabled={this.state.busy} > {_t("View rules")} @@ -277,21 +277,21 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> )}
  • - {this._renderPersonalBanListRules()} + {this.renderPersonalBanListRules()}
    -
    + {_t("Ignore")} @@ -309,20 +309,20 @@ export default class MjolnirUserSettingsTab extends React.Component<{}, IState> )}
    - {this._renderSubscribedBanLists()} + {this.renderSubscribedBanLists()}
    - + {_t("Subscribe")} diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 3e8c5f929c..138bf40b80 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -142,38 +142,38 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta }); } - _onAutoLaunchChange = (checked) => { + private onAutoLaunchChange = (checked) => { PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked})); }; - _onWarnBeforeExitChange = (checked) => { + private onWarnBeforeExitChange = (checked) => { PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({warnBeforeExit: checked})); } - _onAlwaysShowMenuBarChange = (checked) => { + private onAlwaysShowMenuBarChange = (checked) => { PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked})); }; - _onMinimizeToTrayChange = (checked) => { + private onMinimizeToTrayChange = (checked) => { PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked})); }; - _onAutocompleteDelayChange = (e) => { + private onAutocompleteDelayChange = (e) => { this.setState({autocompleteDelay: e.target.value}); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); }; - _onReadMarkerInViewThresholdMs = (e) => { + private onReadMarkerInViewThresholdMs = (e) => { this.setState({readMarkerInViewThresholdMs: e.target.value}); SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); }; - _onReadMarkerOutOfViewThresholdMs = (e) => { + private onReadMarkerOutOfViewThresholdMs = (e) => { this.setState({readMarkerOutOfViewThresholdMs: e.target.value}); SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); }; - _renderGroup(settingIds) { + private renderGroup(settingIds) { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); return settingIds.filter(SettingsStore.isEnabled).map(i => { return ; @@ -185,7 +185,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta if (this.state.autoLaunchSupported) { autoLaunchOption = ; } @@ -193,7 +193,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta if (this.state.warnBeforeExitSupported) { warnBeforeExitOption = ; } @@ -201,7 +201,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta if (this.state.alwaysShowMenuBarSupported) { autoHideMenuOption = ; } @@ -209,7 +209,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta if (this.state.minimizeToTraySupported) { minimizeToTrayOption = ; } @@ -219,22 +219,22 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
    {_t("Room list")} - {this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
    {_t("Composer")} - {this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
    {_t("Timeline")} - {this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
    {_t("General")} - {this._renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)} {minimizeToTrayOption} {autoHideMenuOption} {autoLaunchOption} @@ -243,17 +243,17 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta label={_t('Autocomplete delay (ms)')} type='number' value={this.state.autocompleteDelay} - onChange={this._onAutocompleteDelayChange} /> + onChange={this.onAutocompleteDelayChange} /> + onChange={this.onReadMarkerInViewThresholdMs} /> + onChange={this.onReadMarkerOutOfViewThresholdMs} />
    ); diff --git a/src/stores/TypingStore.ts b/src/stores/TypingStore.ts index 8b96105a7d..d5177a33a0 100644 --- a/src/stores/TypingStore.ts +++ b/src/stores/TypingStore.ts @@ -25,7 +25,7 @@ const TYPING_SERVER_TIMEOUT = 30000; * Tracks typing state for users. */ export default class TypingStore { - private _typingStates: { + private typingStates: { [roomId: string]: { isTyping: boolean, userTimer: Timer, @@ -49,7 +49,7 @@ export default class TypingStore { * MatrixClientPeg client changes. */ reset() { - this._typingStates = { + this.typingStates = { // "roomId": { // isTyping: bool, // Whether the user is typing or not // userTimer: Timer, // Local timeout for "user has stopped typing" @@ -67,14 +67,14 @@ export default class TypingStore { if (!SettingsStore.getValue('sendTypingNotifications')) return; if (SettingsStore.getValue('lowBandwidth')) return; - let currentTyping = this._typingStates[roomId]; + let currentTyping = this.typingStates[roomId]; if ((!isTyping && !currentTyping) || (currentTyping && currentTyping.isTyping === isTyping)) { // No change in state, so don't do anything. We'll let the timer run its course. return; } if (!currentTyping) { - currentTyping = this._typingStates[roomId] = { + currentTyping = this.typingStates[roomId] = { isTyping: isTyping, serverTimer: new Timer(TYPING_SERVER_TIMEOUT), userTimer: new Timer(TYPING_USER_TIMEOUT), @@ -86,7 +86,7 @@ export default class TypingStore { if (isTyping) { if (!currentTyping.serverTimer.isRunning()) { currentTyping.serverTimer.restart().finished().then(() => { - const currentTyping = this._typingStates[roomId]; + const currentTyping = this.typingStates[roomId]; if (currentTyping) currentTyping.isTyping = false; // The server will (should) time us out on typing, so we don't diff --git a/src/stores/WidgetEchoStore.ts b/src/stores/WidgetEchoStore.ts index e312cecac5..e752f3db20 100644 --- a/src/stores/WidgetEchoStore.ts +++ b/src/stores/WidgetEchoStore.ts @@ -23,7 +23,7 @@ import {WidgetType} from "../widgets/WidgetType"; * proxying through state from the js-sdk. */ class WidgetEchoStore extends EventEmitter { - private _roomWidgetEcho: { + private roomWidgetEcho: { [roomId: string]: { [widgetId: string]: IWidget, }, @@ -32,7 +32,7 @@ class WidgetEchoStore extends EventEmitter { constructor() { super(); - this._roomWidgetEcho = { + this.roomWidgetEcho = { // Map as below. Object is the content of the widget state event, // so for widgets that have been deleted locally, the object is empty. // roomId: { @@ -55,7 +55,7 @@ class WidgetEchoStore extends EventEmitter { getEchoedRoomWidgets(roomId, currentRoomWidgets) { const echoedWidgets = []; - const roomEchoState = Object.assign({}, this._roomWidgetEcho[roomId]); + const roomEchoState = Object.assign({}, this.roomWidgetEcho[roomId]); for (const w of currentRoomWidgets) { const widgetId = w.getStateKey(); @@ -72,7 +72,7 @@ class WidgetEchoStore extends EventEmitter { } roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, type?: WidgetType) { - const roomEchoState = Object.assign({}, this._roomWidgetEcho[roomId]); + const roomEchoState = Object.assign({}, this.roomWidgetEcho[roomId]); // any widget IDs that are already in the room are not pending, so // echoes for them don't count as pending. @@ -96,15 +96,15 @@ class WidgetEchoStore extends EventEmitter { } setRoomWidgetEcho(roomId: string, widgetId: string, state: IWidget) { - if (this._roomWidgetEcho[roomId] === undefined) this._roomWidgetEcho[roomId] = {}; + if (this.roomWidgetEcho[roomId] === undefined) this.roomWidgetEcho[roomId] = {}; - this._roomWidgetEcho[roomId][widgetId] = state; + this.roomWidgetEcho[roomId][widgetId] = state; this.emit('update', roomId, widgetId); } removeRoomWidgetEcho(roomId, widgetId) { - delete this._roomWidgetEcho[roomId][widgetId]; - if (Object.keys(this._roomWidgetEcho[roomId]).length === 0) delete this._roomWidgetEcho[roomId]; + delete this.roomWidgetEcho[roomId][widgetId]; + if (Object.keys(this.roomWidgetEcho[roomId]).length === 0) delete this.roomWidgetEcho[roomId]; this.emit('update', roomId, widgetId); } } diff --git a/src/utils/Timer.ts b/src/utils/Timer.ts index 4d0532087e..3e370413e5 100644 --- a/src/utils/Timer.ts +++ b/src/utils/Timer.ts @@ -26,51 +26,51 @@ Once a timer is finished or aborted, it can't be started again a new one through `clone()` or `cloneIfRun()`. */ export default class Timer { - private _timeout: number; - private _timerHandle: NodeJS.Timeout; - private _startTs: number; - private _promise: Promise; - private _resolve: () => void; - private _reject: (Error) => void; + private timeout: number; + private timerHandle: NodeJS.Timeout; + private startTs: number; + private promise: Promise; + private resolve: () => void; + private reject: (Error) => void; constructor(timeout) { - this._timeout = timeout; - this._onTimeout = this._onTimeout.bind(this); - this._setNotStarted(); + this.timeout = timeout; + this.onTimeout = this.onTimeout.bind(this); + this.setNotStarted(); } - _setNotStarted() { - this._timerHandle = null; - this._startTs = null; - this._promise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; + private setNotStarted() { + this.timerHandle = null; + this.startTs = null; + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; }).finally(() => { - this._timerHandle = null; + this.timerHandle = null; }); } - _onTimeout() { + private onTimeout() { const now = Date.now(); - const elapsed = now - this._startTs; - if (elapsed >= this._timeout) { - this._resolve(); - this._setNotStarted(); + const elapsed = now - this.startTs; + if (elapsed >= this.timeout) { + this.resolve(); + this.setNotStarted(); } else { - const delta = this._timeout - elapsed; - this._timerHandle = setTimeout(this._onTimeout, delta); + const delta = this.timeout - elapsed; + this.timerHandle = setTimeout(this.onTimeout, delta); } } changeTimeout(timeout) { - if (timeout === this._timeout) { + if (timeout === this.timeout) { return; } - const isSmallerTimeout = timeout < this._timeout; - this._timeout = timeout; + const isSmallerTimeout = timeout < this.timeout; + this.timeout = timeout; if (this.isRunning() && isSmallerTimeout) { - clearTimeout(this._timerHandle); - this._onTimeout(); + clearTimeout(this.timerHandle); + this.onTimeout(); } } @@ -80,8 +80,8 @@ export default class Timer { */ start() { if (!this.isRunning()) { - this._startTs = Date.now(); - this._timerHandle = setTimeout(this._onTimeout, this._timeout); + this.startTs = Date.now(); + this.timerHandle = setTimeout(this.onTimeout, this.timeout); } return this; } @@ -96,7 +96,7 @@ export default class Timer { // can be called in fast succession, // instead just take note and compare // when the already running timeout expires - this._startTs = Date.now(); + this.startTs = Date.now(); return this; } else { return this.start(); @@ -110,9 +110,9 @@ export default class Timer { */ abort() { if (this.isRunning()) { - clearTimeout(this._timerHandle); - this._reject(new Error("Timer was aborted.")); - this._setNotStarted(); + clearTimeout(this.timerHandle); + this.reject(new Error("Timer was aborted.")); + this.setNotStarted(); } return this; } @@ -123,10 +123,10 @@ export default class Timer { *@return {Promise} */ finished() { - return this._promise; + return this.promise; } isRunning() { - return this._timerHandle !== null; + return this.timerHandle !== null; } } diff --git a/src/utils/permalinks/ElementPermalinkConstructor.ts b/src/utils/permalinks/ElementPermalinkConstructor.ts index 6702217c8e..cd7f2b9d2c 100644 --- a/src/utils/permalinks/ElementPermalinkConstructor.ts +++ b/src/utils/permalinks/ElementPermalinkConstructor.ts @@ -20,31 +20,31 @@ import PermalinkConstructor, {PermalinkParts} from "./PermalinkConstructor"; * Generates permalinks that self-reference the running webapp */ export default class ElementPermalinkConstructor extends PermalinkConstructor { - private _elementUrl: string; + private elementUrl: string; constructor(elementUrl: string) { super(); - this._elementUrl = elementUrl; + this.elementUrl = elementUrl; - if (!this._elementUrl.startsWith("http:") && !this._elementUrl.startsWith("https:")) { + if (!this.elementUrl.startsWith("http:") && !this.elementUrl.startsWith("https:")) { throw new Error("Element prefix URL does not appear to be an HTTP(S) URL"); } } forEvent(roomId: string, eventId: string, serverCandidates: string[]): string { - return `${this._elementUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`; + return `${this.elementUrl}/#/room/${roomId}/${eventId}${this.encodeServerCandidates(serverCandidates)}`; } forRoom(roomIdOrAlias: string, serverCandidates?: string[]): string { - return `${this._elementUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`; + return `${this.elementUrl}/#/room/${roomIdOrAlias}${this.encodeServerCandidates(serverCandidates)}`; } forUser(userId: string): string { - return `${this._elementUrl}/#/user/${userId}`; + return `${this.elementUrl}/#/user/${userId}`; } forGroup(groupId: string): string { - return `${this._elementUrl}/#/group/${groupId}`; + return `${this.elementUrl}/#/group/${groupId}`; } forEntity(entityId: string): string { @@ -58,7 +58,7 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor { } isPermalinkHost(testHost: string): boolean { - const parsedUrl = new URL(this._elementUrl); + const parsedUrl = new URL(this.elementUrl); return testHost === (parsedUrl.host || parsedUrl.hostname); // one of the hosts should match } @@ -71,11 +71,11 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor { // https://github.com/turt2live/matrix-js-bot-sdk/blob/7c4665c9a25c2c8e0fe4e509f2616505b5b66a1c/src/Permalinks.ts#L33-L61 // Adapted for Element's URL format parsePermalink(fullUrl: string): PermalinkParts { - if (!fullUrl || !fullUrl.startsWith(this._elementUrl)) { + if (!fullUrl || !fullUrl.startsWith(this.elementUrl)) { throw new Error("Does not appear to be a permalink"); } - const parts = fullUrl.substring(`${this._elementUrl}/#/`.length); + const parts = fullUrl.substring(`${this.elementUrl}/#/`.length); return ElementPermalinkConstructor.parseAppRoute(parts); } diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index f5eeca86ee..cb463d6781 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -74,29 +74,29 @@ const MAX_SERVER_CANDIDATES = 3; // the list and magically have the link work. export class RoomPermalinkCreator { - private _room: Room; - private _roomId: string; - private _highestPlUserId: string; - private _populationMap: { [serverName: string]: number }; - private _bannedHostsRegexps: RegExp[]; - private _allowedHostsRegexps: RegExp[]; + private room: Room; + private roomId: string; + private highestPlUserId: string; + private populationMap: { [serverName: string]: number }; + private bannedHostsRegexps: RegExp[]; + private allowedHostsRegexps: RegExp[]; private _serverCandidates: string[]; - private _started: boolean; + private started: boolean; // We support being given a roomId as a fallback in the event the `room` object // doesn't exist or is not healthy for us to rely on. For example, loading a // permalink to a room which the MatrixClient doesn't know about. constructor(room: Room, roomId: string = null) { - this._room = room; - this._roomId = room ? room.roomId : roomId; - this._highestPlUserId = null; - this._populationMap = null; - this._bannedHostsRegexps = null; - this._allowedHostsRegexps = null; + this.room = room; + this.roomId = room ? room.roomId : roomId; + this.highestPlUserId = null; + this.populationMap = null; + this.bannedHostsRegexps = null; + this.allowedHostsRegexps = null; this._serverCandidates = null; - this._started = false; + this.started = false; - if (!this._roomId) { + if (!this.roomId) { throw new Error("Failed to resolve a roomId for the permalink creator to use"); } @@ -105,7 +105,7 @@ export class RoomPermalinkCreator { } load() { - if (!this._room || !this._room.currentState) { + if (!this.room || !this.room.currentState) { // Under rare and unknown circumstances it is possible to have a room with no // currentState, at least potentially at the early stages of joining a room. // To avoid breaking everything, we'll just warn rather than throw as well as @@ -113,23 +113,23 @@ export class RoomPermalinkCreator { console.warn("Tried to load a permalink creator with no room state"); return; } - this._updateAllowedServers(); - this._updateHighestPlUser(); - this._updatePopulationMap(); - this._updateServerCandidates(); + this.updateAllowedServers(); + this.updateHighestPlUser(); + this.updatePopulationMap(); + this.updateServerCandidates(); } start() { this.load(); - this._room.on("RoomMember.membership", this.onMembership); - this._room.on("RoomState.events", this.onRoomState); - this._started = true; + this.room.on("RoomMember.membership", this.onMembership); + this.room.on("RoomState.events", this.onRoomState); + this.started = true; } stop() { - this._room.removeListener("RoomMember.membership", this.onMembership); - this._room.removeListener("RoomState.events", this.onRoomState); - this._started = false; + this.room.removeListener("RoomMember.membership", this.onMembership); + this.room.removeListener("RoomState.events", this.onRoomState); + this.started = false; } get serverCandidates() { @@ -137,44 +137,44 @@ export class RoomPermalinkCreator { } isStarted() { - return this._started; + return this.started; } forEvent(eventId) { - return getPermalinkConstructor().forEvent(this._roomId, eventId, this._serverCandidates); + return getPermalinkConstructor().forEvent(this.roomId, eventId, this._serverCandidates); } forShareableRoom() { - if (this._room) { + if (this.room) { // Prefer to use canonical alias for permalink if possible - const alias = this._room.getCanonicalAlias(); + const alias = this.room.getCanonicalAlias(); if (alias) { return getPermalinkConstructor().forRoom(alias, this._serverCandidates); } } - return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates); + return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates); } forRoom() { - return getPermalinkConstructor().forRoom(this._roomId, this._serverCandidates); + return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates); } - onRoomState(event) { + private onRoomState(event) { switch (event.getType()) { case "m.room.server_acl": - this._updateAllowedServers(); - this._updateHighestPlUser(); - this._updatePopulationMap(); - this._updateServerCandidates(); + this.updateAllowedServers(); + this.updateHighestPlUser(); + this.updatePopulationMap(); + this.updateServerCandidates(); return; case "m.room.power_levels": - this._updateHighestPlUser(); - this._updateServerCandidates(); + this.updateHighestPlUser(); + this.updateServerCandidates(); return; } } - onMembership(evt, member, oldMembership) { + private onMembership(evt, member, oldMembership) { const userId = member.userId; const membership = member.membership; const serverName = getServerName(userId); @@ -182,17 +182,17 @@ export class RoomPermalinkCreator { const hasLeft = oldMembership === "join" && membership !== "join"; if (hasLeft) { - this._populationMap[serverName]--; + this.populationMap[serverName]--; } else if (hasJoined) { - this._populationMap[serverName]++; + this.populationMap[serverName]++; } - this._updateHighestPlUser(); - this._updateServerCandidates(); + this.updateHighestPlUser(); + this.updateServerCandidates(); } - _updateHighestPlUser() { - const plEvent = this._room.currentState.getStateEvents("m.room.power_levels", ""); + private updateHighestPlUser() { + const plEvent = this.room.currentState.getStateEvents("m.room.power_levels", ""); if (plEvent) { const content = plEvent.getContent(); if (content) { @@ -200,14 +200,14 @@ export class RoomPermalinkCreator { if (users) { const entries = Object.entries(users); const allowedEntries = entries.filter(([userId]) => { - const member = this._room.getMember(userId); + const member = this.room.getMember(userId); if (!member || member.membership !== "join") { return false; } const serverName = getServerName(userId); return !isHostnameIpAddress(serverName) && - !isHostInRegex(serverName, this._bannedHostsRegexps) && - isHostInRegex(serverName, this._allowedHostsRegexps); + !isHostInRegex(serverName, this.bannedHostsRegexps) && + isHostInRegex(serverName, this.allowedHostsRegexps); }); const maxEntry = allowedEntries.reduce((max, entry) => { return (entry[1] > max[1]) ? entry : max; @@ -215,20 +215,20 @@ export class RoomPermalinkCreator { const [userId, powerLevel] = maxEntry; // object wasn't empty, and max entry wasn't a demotion from the default if (userId !== null && powerLevel >= 50) { - this._highestPlUserId = userId; + this.highestPlUserId = userId; return; } } } } - this._highestPlUserId = null; + this.highestPlUserId = null; } - _updateAllowedServers() { + private updateAllowedServers() { const bannedHostsRegexps = []; let allowedHostsRegexps = [new RegExp(".*")]; // default allow everyone - if (this._room.currentState) { - const aclEvent = this._room.currentState.getStateEvents("m.room.server_acl", ""); + if (this.room.currentState) { + const aclEvent = this.room.currentState.getStateEvents("m.room.server_acl", ""); if (aclEvent && aclEvent.getContent()) { const getRegex = (hostname) => new RegExp("^" + utils.globToRegexp(hostname, false) + "$"); @@ -240,35 +240,35 @@ export class RoomPermalinkCreator { allowed.forEach(h => allowedHostsRegexps.push(getRegex(h))); } } - this._bannedHostsRegexps = bannedHostsRegexps; - this._allowedHostsRegexps = allowedHostsRegexps; + this.bannedHostsRegexps = bannedHostsRegexps; + this.allowedHostsRegexps = allowedHostsRegexps; } - _updatePopulationMap() { + private updatePopulationMap() { const populationMap: { [server: string]: number } = {}; - for (const member of this._room.getJoinedMembers()) { + for (const member of this.room.getJoinedMembers()) { const serverName = getServerName(member.userId); if (!populationMap[serverName]) { populationMap[serverName] = 0; } populationMap[serverName]++; } - this._populationMap = populationMap; + this.populationMap = populationMap; } - _updateServerCandidates() { + private updateServerCandidates() { let candidates = []; - if (this._highestPlUserId) { - candidates.push(getServerName(this._highestPlUserId)); + if (this.highestPlUserId) { + candidates.push(getServerName(this.highestPlUserId)); } - const serversByPopulation = Object.keys(this._populationMap) - .sort((a, b) => this._populationMap[b] - this._populationMap[a]) + const serversByPopulation = Object.keys(this.populationMap) + .sort((a, b) => this.populationMap[b] - this.populationMap[a]) .filter(a => { return !candidates.includes(a) && !isHostnameIpAddress(a) && - !isHostInRegex(a, this._bannedHostsRegexps) && - isHostInRegex(a, this._allowedHostsRegexps); + !isHostInRegex(a, this.bannedHostsRegexps) && + isHostInRegex(a, this.allowedHostsRegexps); }); const remainingServers = serversByPopulation.slice(0, MAX_SERVER_CANDIDATES - candidates.length); From 3b39007a5d7edbdd637a8d823580c92cd9678768 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 14:06:25 +0100 Subject: [PATCH 232/330] Move initialisers to field --- src/indexing/EventIndexPeg.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/indexing/EventIndexPeg.ts b/src/indexing/EventIndexPeg.ts index 050a8c6217..4356d882d5 100644 --- a/src/indexing/EventIndexPeg.ts +++ b/src/indexing/EventIndexPeg.ts @@ -28,16 +28,10 @@ import {SettingLevel} from "../settings/SettingLevel"; const INDEX_VERSION = 1; export class EventIndexPeg { - public index: EventIndex; - public error: Error; + public index: EventIndex = null; + public error: Error = null; - private _supportIsInstalled: boolean; - - constructor() { - this.index = null; - this.error = null; - this._supportIsInstalled = false; - } + private _supportIsInstalled = false; /** * Initialize the EventIndexPeg and if event indexing is enabled initialize From 69fbfdc552a1026797f792e370487dd7cb1df8e3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 14:07:45 +0100 Subject: [PATCH 233/330] Fix interface syntax --- .../views/settings/tabs/user/PreferencesUserSettingsTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 138bf40b80..7e2da2b53b 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -34,9 +34,9 @@ interface IState { alwaysShowMenuBar: boolean; minimizeToTraySupported: boolean; minimizeToTray: boolean; - autocompleteDelay: string, - readMarkerInViewThresholdMs: string, - readMarkerOutOfViewThresholdMs: string, + autocompleteDelay: string; + readMarkerInViewThresholdMs: string; + readMarkerOutOfViewThresholdMs: string; } @replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab") From a3a756fdb26eaf42855454879aab37daa466bb17 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 14:08:45 +0100 Subject: [PATCH 234/330] Rename history visibility type --- .../views/settings/tabs/room/SecurityRoomSettingsTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 07776a5a54..3814a8c1b7 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -29,7 +29,7 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent" type JoinRule = "public" | "knock" | "invite" | "private"; type GuestAccess = "can_join" | "forbidden"; -type History = "invited" | "joined" | "shared" | "world_readable"; +type HistoryVisibility = "invited" | "joined" | "shared" | "world_readable"; interface IProps { roomId: string; @@ -38,7 +38,7 @@ interface IProps { interface IState { joinRule: JoinRule; guestAccess: GuestAccess; - history: History; + history: HistoryVisibility; hasAliases: boolean; encrypted: boolean; } @@ -74,7 +74,7 @@ export default class SecurityRoomSettingsTab extends React.Component Date: Mon, 26 Apr 2021 15:47:58 +0200 Subject: [PATCH 235/330] Show zoom buttons only if zooming is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 34 ++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index f037168b63..fcacae2d39 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -316,11 +316,12 @@ export default class ImageView extends React.Component { render() { const showEventMeta = !!this.props.mxEvent; + const zoomingDisabled = this.state.maxZoom === this.state.minZoom; let cursor; if (this.state.moving) { cursor= "grabbing"; - } else if (this.state.maxZoom === this.state.minZoom) { + } else if (zoomingDisabled) { cursor = "default"; } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; @@ -412,6 +413,25 @@ export default class ImageView extends React.Component { ); } + let zoomOutButton; + let zoomInButton; + if (!zoomingDisabled) { + zoomOutButton = ( + + + ); + zoomInButton = ( + + + ); + } + return ( { title={_t("Rotate Left")} onClick={ this.onRotateCounterClockwiseClick }> - - - - + {zoomOutButton} + {zoomInButton} Date: Mon, 26 Apr 2021 15:51:56 +0200 Subject: [PATCH 236/330] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f1b700540f..4d5a9a4f43 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1923,10 +1923,10 @@ "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", - "Rotate Right": "Rotate Right", - "Rotate Left": "Rotate Left", "Zoom out": "Zoom out", "Zoom in": "Zoom in", + "Rotate Right": "Rotate Right", + "Rotate Left": "Rotate Left", "Download": "Download", "Information": "Information", "View message": "View message", From 83596c7335779d4f374e339c108e65a67107218c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 14:54:36 +0100 Subject: [PATCH 237/330] Use private parameter properties --- src/ScalarAuthClient.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts index c59136263d..c18265be55 100644 --- a/src/ScalarAuthClient.ts +++ b/src/ScalarAuthClient.ts @@ -30,13 +30,11 @@ const imApiVersion = "1.1"; // TODO: Generify the name of this class and all components within - it's not just for Scalar. export default class ScalarAuthClient { - private apiUrl: string; - private uiUrl: string; private scalarToken: string; private termsInteractionCallback: TermsInteractionCallback; private isDefaultManager: boolean; - constructor(apiUrl, uiUrl) { + constructor(private apiUrl: string, private uiUrl: string) { this.apiUrl = apiUrl; this.uiUrl = uiUrl; this.scalarToken = null; From 751568cef20277e3c01976162c447af85a7d7af7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Apr 2021 14:55:11 +0100 Subject: [PATCH 238/330] Disable spaces context switching for when exploring a space --- src/stores/SpaceStore.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index e4b180f3ce..56f0728f87 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -543,7 +543,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // persist last viewed room from a space if (room.isSpaceRoom()) { - this.setActiveSpace(room); + // Don't context switch when navigating to the space room + // as it will cause you to end up in the wrong room + this.setActiveSpace(room, false); } else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(room.roomId)) { // TODO maybe reverse these first 2 clauses once space panel active is fixed let parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId)); From 1b615eebc1859917d9f97b798f001d56aab764a1 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 14:56:43 +0100 Subject: [PATCH 239/330] Fix optional props --- .../views/settings/tabs/room/RolesRoomSettingsTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index a945b22d1f..acf98edc18 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -64,10 +64,10 @@ function parseIntWithDefault(val, def) { } interface IBannedUserProps { - canUnban: boolean; + canUnban?: boolean; member: RoomMember; by: string; - reason: string; + reason?: string; } export class BannedUser extends React.Component { From 8537f0a5a154960ea48a4123453a37e48c49d5a6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 14:59:26 +0100 Subject: [PATCH 240/330] Remove unneeded lint tweak --- src/@types/common.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/@types/common.ts b/src/@types/common.ts index e5503a0b68..b887bd4090 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -17,7 +17,6 @@ limitations under the License. import { JSXElementConstructor } from "react"; // Based on https://stackoverflow.com/a/53229857/3532235 -// eslint-disable-next-line @typescript-eslint/type-annotation-spacing export type Without = {[P in Exclude] ? : never}; export type XOR = (T | U) extends object ? (Without & U) | (Without & T) : T | U; export type Writeable = { -readonly [P in keyof T]: T[P] }; From 889289d46446715d94d4de3f320f36c1069520eb Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:01:05 +0100 Subject: [PATCH 241/330] Tweak Timer constructor --- src/utils/Timer.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/Timer.ts b/src/utils/Timer.ts index 3e370413e5..26170491d8 100644 --- a/src/utils/Timer.ts +++ b/src/utils/Timer.ts @@ -26,16 +26,14 @@ Once a timer is finished or aborted, it can't be started again a new one through `clone()` or `cloneIfRun()`. */ export default class Timer { - private timeout: number; private timerHandle: NodeJS.Timeout; private startTs: number; private promise: Promise; private resolve: () => void; private reject: (Error) => void; - constructor(timeout) { + constructor(private timeout: number) { this.timeout = timeout; - this.onTimeout = this.onTimeout.bind(this); this.setNotStarted(); } @@ -50,7 +48,7 @@ export default class Timer { }); } - private onTimeout() { + private onTimeout = () => { const now = Date.now(); const elapsed = now - this.startTs; if (elapsed >= this.timeout) { From eb9bf9c83f2dd03eaaeee1a7c9df7b2202ab1654 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:03:17 +0100 Subject: [PATCH 242/330] Tweak Terms constructor --- src/Terms.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Terms.ts b/src/Terms.ts index 382e2d9782..31eeb6b29a 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -27,16 +27,12 @@ export class TermsNotSignedError extends Error {} * require agreement from the user before the user can use that service. */ export class Service { - public serviceType: string; - public baseUrl: string; - public accessToken: string; - /** * @param {MatrixClient.SERVICE_TYPES} serviceType The type of service * @param {string} baseUrl The Base URL of the service (ie. before '/_matrix') * @param {string} accessToken The user's access token for the service */ - constructor(serviceType, baseUrl, accessToken) { + constructor(public serviceType: string, public baseUrl: string, public accessToken: string) { this.serviceType = serviceType; this.baseUrl = baseUrl; this.accessToken = accessToken; From 01cfd9361e58f98f815eb4a7471975c6d5a845e7 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:06:10 +0100 Subject: [PATCH 243/330] Fix ManageEventIndexDialog props syntax --- .../views/dialogs/eventindex/ManageEventIndexDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 8864e043aa..78945a96f5 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -26,7 +26,7 @@ import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import {SettingLevel} from "../../../../settings/SettingLevel"; interface IProps { - onFinished: (boolean) => {}, + onFinished: (boolean) => void; } interface IState { From bf43144f6e1b43fa60cf3e9a364ecb466392bf6f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:21:49 +0100 Subject: [PATCH 244/330] Add ActionPayload type --- src/components/views/rooms/MessageComposer.tsx | 5 +++-- src/components/views/settings/SetIdServer.tsx | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 62df0e7d72..ab7c358a2c 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -22,6 +22,7 @@ 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 dis from '../../../dispatcher/dispatcher'; +import { ActionPayload } from "../../../dispatcher/payloads"; import Stickerpicker from './Stickerpicker'; import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import ContentMessages from '../../../ContentMessages'; @@ -121,7 +122,7 @@ class UploadButton extends React.Component { dis.unregister(this.dispatcherRef); } - private onAction = payload => { + private onAction = (payload: ActionPayload) => { if (payload.action === "upload_file") { this.onUploadClick(); } @@ -217,7 +218,7 @@ export default class MessageComposer extends React.Component { this.waitForOwnMember(); } - private onAction = (payload) => { + private onAction = (payload: ActionPayload) => { if (payload.action === 'reply_to_event') { // add a timeout for the reply preview to be rendered, so // that the ScrollPanel listening to the resizeNotifier can diff --git a/src/components/views/settings/SetIdServer.tsx b/src/components/views/settings/SetIdServer.tsx index 70a4c46f69..05d1f83387 100644 --- a/src/components/views/settings/SetIdServer.tsx +++ b/src/components/views/settings/SetIdServer.tsx @@ -27,6 +27,7 @@ import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils'; import {timeout} from "../../../utils/promise"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { ActionPayload } from '../../../dispatcher/payloads'; // We'll wait up to this long when checking for 3PID bindings on the IS. const REACHABILITY_TIMEOUT = 10000; // ms @@ -107,7 +108,7 @@ export default class SetIdServer extends React.Component { dis.unregister(this.dispatcherRef); } - private onAction = (payload) => { + private onAction = (payload: ActionPayload) => { // We react to changes in the ID server in the event the user is staring at this form // when changing their identity server on another device. if (payload.action !== "id_server_changed") return; From 809454e66accc3229c48105486a8182e55df2cc8 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:23:55 +0100 Subject: [PATCH 245/330] Use new managed prop for emoji composer menu --- src/components/views/rooms/MessageComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index ab7c358a2c..ebc8d3711c 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -74,7 +74,7 @@ const EmojiButton = ({addEmoji}) => { if (menuDisplayed) { const buttonRect = button.current.getBoundingClientRect(); const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker'); - contextMenu = + contextMenu = ; } From 4b66082b0f30f7a93af7a8eb9bddefee4e7b590a Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:27:30 +0100 Subject: [PATCH 246/330] Add change event type --- src/components/views/rooms/MessageComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index ebc8d3711c..d126a7b161 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -136,7 +136,7 @@ class UploadButton extends React.Component { this.uploadInput.current.click(); } - private onUploadFileInputChange = (ev) => { + private onUploadFileInputChange = (ev: React.ChangeEvent) => { if (ev.target.files.length === 0) return; // take a copy so we can safely reset the value of the form control From 82caac16c892f6f595b5b406224b869eadb92779 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:30:34 +0100 Subject: [PATCH 247/330] Add types for StorageManager functions --- src/utils/StorageManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/StorageManager.ts b/src/utils/StorageManager.ts index 07a731cd71..883c032771 100644 --- a/src/utils/StorageManager.ts +++ b/src/utils/StorageManager.ts @@ -32,15 +32,15 @@ try { const SYNC_STORE_NAME = "riot-web-sync"; const CRYPTO_STORE_NAME = "matrix-js-sdk:crypto"; -function log(msg) { +function log(msg: string) { console.log(`StorageManager: ${msg}`); } -function error(msg, ...args) { +function error(msg: string, ...args: string[]) { console.error(`StorageManager: ${msg}`, ...args); } -function track(action) { +function track(action: string) { Analytics.trackEvent("StorageManager", action); } From 25e4feeb388eefe8f44ed9d1311703a7711d7698 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:38:43 +0100 Subject: [PATCH 248/330] Add more types in WidgetEchoStore --- src/stores/WidgetEchoStore.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/stores/WidgetEchoStore.ts b/src/stores/WidgetEchoStore.ts index e752f3db20..09120d6108 100644 --- a/src/stores/WidgetEchoStore.ts +++ b/src/stores/WidgetEchoStore.ts @@ -16,6 +16,7 @@ limitations under the License. import EventEmitter from 'events'; import { IWidget } from 'matrix-widget-api'; +import MatrixEvent from "matrix-js-sdk/src/models/event"; import {WidgetType} from "../widgets/WidgetType"; /** @@ -36,7 +37,7 @@ class WidgetEchoStore extends EventEmitter { // Map as below. Object is the content of the widget state event, // so for widgets that have been deleted locally, the object is empty. // roomId: { - // widgetId: [object] + // widgetId: IWidget // } }; } @@ -48,11 +49,11 @@ class WidgetEchoStore extends EventEmitter { * and we don't really need the actual widget events anyway since we just want to * show a spinner / prevent widgets being added twice. * - * @param {Room} roomId The ID of the room to get widgets for + * @param {string} roomId The ID of the room to get widgets for * @param {MatrixEvent[]} currentRoomWidgets Current widgets for the room * @returns {MatrixEvent[]} List of widgets in the room, minus any pending removal */ - getEchoedRoomWidgets(roomId, currentRoomWidgets) { + getEchoedRoomWidgets(roomId: string, currentRoomWidgets: MatrixEvent[]): MatrixEvent[] { const echoedWidgets = []; const roomEchoState = Object.assign({}, this.roomWidgetEcho[roomId]); @@ -71,7 +72,7 @@ class WidgetEchoStore extends EventEmitter { return echoedWidgets; } - roomHasPendingWidgetsOfType(roomId, currentRoomWidgets, type?: WidgetType) { + roomHasPendingWidgetsOfType(roomId: string, currentRoomWidgets: MatrixEvent[], type?: WidgetType): boolean { const roomEchoState = Object.assign({}, this.roomWidgetEcho[roomId]); // any widget IDs that are already in the room are not pending, so @@ -91,7 +92,7 @@ class WidgetEchoStore extends EventEmitter { } } - roomHasPendingWidgets(roomId, currentRoomWidgets) { + roomHasPendingWidgets(roomId: string, currentRoomWidgets: MatrixEvent[]): boolean { return this.roomHasPendingWidgetsOfType(roomId, currentRoomWidgets); } @@ -102,7 +103,7 @@ class WidgetEchoStore extends EventEmitter { this.emit('update', roomId, widgetId); } - removeRoomWidgetEcho(roomId, widgetId) { + removeRoomWidgetEcho(roomId: string, widgetId: string) { delete this.roomWidgetEcho[roomId][widgetId]; if (Object.keys(this.roomWidgetEcho[roomId]).length === 0) delete this.roomWidgetEcho[roomId]; this.emit('update', roomId, widgetId); From ba4e58513d1bb6a7c21041bab837c1d29d1894af Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:41:19 +0100 Subject: [PATCH 249/330] Fix ScalarAuthClient test --- test/ScalarAuthClient-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ScalarAuthClient-test.js b/test/ScalarAuthClient-test.js index 83f357811a..3435f70932 100644 --- a/test/ScalarAuthClient-test.js +++ b/test/ScalarAuthClient-test.js @@ -29,7 +29,7 @@ describe('ScalarAuthClient', function() { it('should request a new token if the old one fails', async function() { const sac = new ScalarAuthClient(); - sac._getAccountName = jest.fn((arg) => { + sac.getAccountName = jest.fn((arg) => { switch (arg) { case "brokentoken": return Promise.reject({ From 489b4be6cfa00408b499423b53ed6000fd12869a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Apr 2021 15:48:33 +0100 Subject: [PATCH 250/330] Fix space hierarchy css not applying as it should --- res/css/structures/_SpaceRoomDirectory.scss | 95 +++++++++++---------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index dcceee6371..c7d087d8e0 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -26,7 +26,10 @@ limitations under the License. word-break: break-word; display: flex; flex-direction: column; +} +.mx_SpaceRoomDirectory, +.mx_SpaceRoomView_landing { .mx_Dialog_title { display: flex; @@ -56,65 +59,63 @@ limitations under the License. } } - .mx_Dialog_content { - .mx_AccessibleButton_kind_link { - padding: 0; - } + .mx_AccessibleButton_kind_link { + padding: 0; + } - .mx_SearchBox { - margin: 24px 0 16px; - } + .mx_SearchBox { + margin: 24px 0 16px; + } - .mx_SpaceRoomDirectory_noResults { - text-align: center; + .mx_SpaceRoomDirectory_noResults { + text-align: center; - > div { - font-size: $font-15px; - line-height: $font-24px; - color: $secondary-fg-color; - } - } - - .mx_SpaceRoomDirectory_listHeader { - display: flex; - min-height: 32px; - align-items: center; + > div { font-size: $font-15px; line-height: $font-24px; - color: $primary-fg-color; + color: $secondary-fg-color; + } + } - .mx_AccessibleButton { - padding: 2px 8px; - font-weight: normal; + .mx_SpaceRoomDirectory_listHeader { + display: flex; + min-height: 32px; + align-items: center; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; - & + .mx_AccessibleButton { - margin-left: 16px; - } - } + .mx_AccessibleButton { + padding: 2px 8px; + font-weight: normal; - > span { - margin-left: auto; + & + .mx_AccessibleButton { + margin-left: 16px; } } - .mx_SpaceRoomDirectory_error { - position: relative; - font-weight: $font-semi-bold; - color: $notice-primary-color; - font-size: $font-15px; - line-height: $font-18px; - margin: 20px auto 12px; - padding-left: 24px; - width: max-content; + > span { + margin-left: auto; + } + } - &::before { - content: ""; - position: absolute; - height: 16px; - width: 16px; - left: 0; - background-image: url("$(res)/img/element-icons/warning-badge.svg"); - } + .mx_SpaceRoomDirectory_error { + position: relative; + font-weight: $font-semi-bold; + color: $notice-primary-color; + font-size: $font-15px; + line-height: $font-18px; + margin: 20px auto 12px; + padding-left: 24px; + width: max-content; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 0; + background-image: url("$(res)/img/element-icons/warning-badge.svg"); } } } From 8659c98c448eb2633aa6e896d4c779c95593e1c2 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 15:55:04 +0100 Subject: [PATCH 251/330] Add tile shape string type --- src/components/views/rooms/EventTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 33bc4951a8..7de8578ae9 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -245,7 +245,7 @@ interface IProps { // It could also be done by subclassing EventTile, but that'd be quite // boiilerplatey. So just make the necessary render decisions conditional // for now. - tileShape?: string; + tileShape?: 'notif' | 'file_grid' | 'reply' | 'reply_preview'; // show twelve hour timestamps isTwelveHour?: boolean; From a8711dcce9100bc17a86262316a4a09dd778f812 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Apr 2021 16:06:42 +0100 Subject: [PATCH 252/330] useSpaceSummary return error for incompatible server notice --- .../structures/SpaceRoomDirectory.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 930cfa15a9..99ff5c1d47 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -312,11 +312,12 @@ export const HierarchyLevel = ({ // mutate argument refreshToken to force a reload export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [ + null, ISpaceSummaryRoom[], - Map>, - Map>, - Map>, -] | [] => { + Map>?, + Map>?, + Map>?, +] | [Error] => { // TODO pagination return useAsyncMemo(async () => { try { @@ -336,13 +337,12 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a } }); - return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; + return [null, data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; } catch (e) { console.error(e); // TODO + return [e]; } - - return []; - }, [space, refreshToken], []); + }, [space, refreshToken], [undefined]); }; export const SpaceHierarchy: React.FC = ({ @@ -358,7 +358,7 @@ export const SpaceHierarchy: React.FC = ({ const [selected, setSelected] = useState(new Map>()); // Map> - const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); + const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); const roomsMap = useMemo(() => { if (!rooms) return null; @@ -538,10 +538,10 @@ export const SpaceHierarchy: React.FC = ({ { children } ; - } else if (!rooms) { - content = ; - } else { + } else if (summaryError) { content =

    {_t("Your server does not support showing space hierarchies.")}

    ; + } else { + content = ; } // TODO loading state/error state From 43b43dc685da0a0b9a0e19e0d3b4cb5e89032169 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Apr 2021 16:11:07 +0100 Subject: [PATCH 253/330] tidy up code --- src/components/structures/SpaceRoomDirectory.tsx | 6 ++++-- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 99ff5c1d47..cb9063faf5 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -397,6 +397,10 @@ export const SpaceHierarchy: React.FC = ({ const [removing, setRemoving] = useState(false); const [saving, setSaving] = useState(false); + if (summaryError) { + return

    {_t("Your server does not support showing space hierarchies.")}

    ; + } + let content; if (roomsMap) { const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length; @@ -538,8 +542,6 @@ export const SpaceHierarchy: React.FC = ({ { children } ; - } else if (summaryError) { - content =

    {_t("Your server does not support showing space hierarchies.")}

    ; } else { content = ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index db8760e4f3..b377d91349 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2638,6 +2638,7 @@ "%(count)s rooms|one": "%(count)s room", "This room is suggested as a good one to join": "This room is suggested as a good one to join", "Suggested": "Suggested", + "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces", "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces", "%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space", @@ -2648,7 +2649,6 @@ "Mark as suggested": "Mark as suggested", "No results found": "No results found", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", - "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "Search names and description": "Search names and description", "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", "Create room": "Create room", From d497d62db36d00d46bcaf76c91bf6868ced9c632 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 16:14:21 +0100 Subject: [PATCH 254/330] Use enums in SecurityRoomSettingsTab --- .../tabs/room/SecurityRoomSettingsTab.tsx | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 3814a8c1b7..cd40761150 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -27,9 +27,24 @@ import SettingsStore from "../../../../../settings/SettingsStore"; import {UIFeature} from "../../../../../settings/UIFeature"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; -type JoinRule = "public" | "knock" | "invite" | "private"; -type GuestAccess = "can_join" | "forbidden"; -type HistoryVisibility = "invited" | "joined" | "shared" | "world_readable"; +enum JoinRule { + Public = "public", + Knock = "knock", + Invite = "invite", + Private = "private", +} + +enum GuestAccess { + CanJoin = "can_join", + Forbidden = "forbidden", +} + +enum HistoryVisibility { + Invited = "invited", + Joined = "joined", + Shared = "shared", + WorldReadable = "world_readable", +} interface IProps { roomId: string; @@ -49,9 +64,9 @@ export default class SecurityRoomSettingsTab extends React.Component Date: Mon, 26 Apr 2021 16:16:43 +0100 Subject: [PATCH 255/330] Tweak interface syntax --- src/components/views/rooms/EventTile.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 67eaa40611..19c5a7acaa 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -222,20 +222,20 @@ interface IProps { isSelectedEvent?: boolean; // callback called when dynamic content in events are loaded - onHeightChanged?: () => void, + onHeightChanged?: () => void; // a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'. - readReceipts?: IReadReceiptProps[], + readReceipts?: IReadReceiptProps[]; // opaque readreceipt info for each userId; used by ReadReceiptMarker // to manage its animations. Should be an empty object when the room // first loads - readReceiptMap?: any, + readReceiptMap?: any; // A function which is used to check if the parent panel is being // unmounted, to avoid unnecessary work. Should return true if we // are being unmounted. - checkUnmounting?: () => boolean, + checkUnmounting?: () => boolean; // the status of this event - ie, mxEvent.status. Denormalised to here so // that we can tell when it changes. @@ -253,13 +253,13 @@ interface IProps { isTwelveHour?: boolean; // helper function to access relations for this event - getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations, + getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations; // whether to show reactions for this event showReactions?: boolean; // which layout to use - layout: Layout, + layout: Layout; // whether or not to show flair at all enableFlair?: boolean; From 26bb7c08c253c059636e441eb28b0cf072fab056 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 16:20:16 +0100 Subject: [PATCH 256/330] Add join rule comment --- .../views/settings/tabs/room/SecurityRoomSettingsTab.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index cd40761150..3beb329dc1 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -27,6 +27,7 @@ import SettingsStore from "../../../../../settings/SettingsStore"; import {UIFeature} from "../../../../../settings/UIFeature"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +// Knock and private are reserved keywords which are not yet implemented. enum JoinRule { Public = "public", Knock = "knock", From a1906be3498e0f47a9b8b4f265029464a35a0cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 08:03:39 +0200 Subject: [PATCH 257/330] Initial code for dynamic minZoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/elements/_ImageView.scss | 3 +-- src/components/views/elements/ImageView.tsx | 29 ++++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 93ebcc2d56..71035dadc3 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -31,8 +31,7 @@ limitations under the License. .mx_ImageView_image { pointer-events: all; - max-width: 95%; - max-height: 95%; + flex-shrink: 0; } .mx_ImageView_panel { diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index cbced07bfe..208a6d995b 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -36,13 +36,15 @@ import {normalizeWheelEvent} from "../../../utils/Mouse"; const MIN_ZOOM = 100; const MAX_ZOOM = 300; +// Max scale to keep gaps around the image +const MAX_SCALE = 0.95; // This is used for the buttons const ZOOM_STEP = 10; // This is used for mouse wheel events const ZOOM_COEFFICIENT = 0.5; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; - +const IMAGE_WRAPPER_CLASS = "mx_ImageView_image_wrapper"; interface IProps { src: string, // the source of the image being displayed @@ -62,8 +64,9 @@ interface IProps { } interface IState { - rotation: number, zoom: number, + minZoom: number, + rotation: number, translationX: number, translationY: number, moving: boolean, @@ -75,8 +78,9 @@ export default class ImageView extends React.Component { constructor(props) { super(props); this.state = { + zoom: 0, + minZoom: 100, rotation: 0, - zoom: MIN_ZOOM, translationX: 0, translationY: 0, moving: false, @@ -99,12 +103,29 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); + window.addEventListener("resize", this.onWindowResize); + this.calculateMinZoom(); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); } + private onWindowResize = (ev) => { + this.calculateMinZoom(); + } + + private calculateMinZoom() { + // TODO: What if we don't have width and height props? + const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; + const zoomX = (imageWrapper.clientWidth / this.props.width) * 100; + const zoomY = (imageWrapper.clientHeight / this.props.height) * 100; + const zoom = Math.min(zoomX, zoomY) * MAX_SCALE; + + if (this.state.zoom <= this.state.minZoom) this.setState({zoom: zoom}); + this.setState({minZoom: zoom}); + } + private onKeyDown = (ev: KeyboardEvent) => { if (ev.key === Key.ESCAPE) { ev.stopPropagation(); @@ -427,7 +448,7 @@ export default class ImageView extends React.Component { {this.renderContextMenu()}
    -
    +
    Date: Sat, 24 Apr 2021 08:32:28 +0200 Subject: [PATCH 258/330] Add dynamic maxZoom and wire it all up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 50 ++++++++++++--------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 208a6d995b..ecc4303764 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -34,8 +34,6 @@ import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import {normalizeWheelEvent} from "../../../utils/Mouse"; -const MIN_ZOOM = 100; -const MAX_ZOOM = 300; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; // This is used for the buttons @@ -66,6 +64,7 @@ interface IProps { interface IState { zoom: number, minZoom: number, + maxZoom: number, rotation: number, translationX: number, translationY: number, @@ -79,7 +78,8 @@ export default class ImageView extends React.Component { super(props); this.state = { zoom: 0, - minZoom: 100, + minZoom: MAX_SCALE, + maxZoom: 100, rotation: 0, translationX: 0, translationY: 0, @@ -100,11 +100,12 @@ export default class ImageView extends React.Component { private previousY = 0; componentDidMount() { + console.log("LOG calculating", this.props.width, this.props.height); // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); window.addEventListener("resize", this.onWindowResize); - this.calculateMinZoom(); + this.calculateZoom(); } componentWillUnmount() { @@ -112,18 +113,23 @@ export default class ImageView extends React.Component { } private onWindowResize = (ev) => { - this.calculateMinZoom(); + this.calculateZoom(); } - private calculateMinZoom() { - // TODO: What if we don't have width and height props? + private calculateZoom() { + // TODO: What if we don't have width and height props? + const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; const zoomX = (imageWrapper.clientWidth / this.props.width) * 100; const zoomY = (imageWrapper.clientHeight / this.props.height) * 100; - const zoom = Math.min(zoomX, zoomY) * MAX_SCALE; + const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; + const maxZoom = minZoom >= 100 ? minZoom : 100; - if (this.state.zoom <= this.state.minZoom) this.setState({zoom: zoom}); - this.setState({minZoom: zoom}); + if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); + this.setState({ + minZoom: minZoom, + maxZoom: maxZoom, + }); } private onKeyDown = (ev: KeyboardEvent) => { @@ -141,16 +147,16 @@ export default class ImageView extends React.Component { const {deltaY} = normalizeWheelEvent(ev); const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT); - if (newZoom <= MIN_ZOOM) { + if (newZoom <= this.state.minZoom) { this.setState({ - zoom: MIN_ZOOM, + zoom: this.state.minZoom, translationX: 0, translationY: 0, }); return; } - if (newZoom >= MAX_ZOOM) { - this.setState({zoom: MAX_ZOOM}); + if (newZoom >= this.state.maxZoom) { + this.setState({zoom: this.state.maxZoom}); return; } @@ -172,8 +178,8 @@ export default class ImageView extends React.Component { }; private onZoomInClick = () => { - if (this.state.zoom >= MAX_ZOOM) { - this.setState({zoom: MAX_ZOOM}); + if (this.state.zoom >= this.state.maxZoom) { + this.setState({zoom: this.state.maxZoom}); return; } @@ -183,9 +189,9 @@ export default class ImageView extends React.Component { }; private onZoomOutClick = () => { - if (this.state.zoom <= MIN_ZOOM) { + if (this.state.zoom <= this.state.minZoom) { this.setState({ - zoom: MIN_ZOOM, + zoom: this.state.minZoom, translationX: 0, translationY: 0, }); @@ -238,8 +244,8 @@ export default class ImageView extends React.Component { if (ev.button !== 0) return; // Zoom in if we are completely zoomed out - if (this.state.zoom === MIN_ZOOM) { - this.setState({zoom: MAX_ZOOM}); + if (this.state.zoom === this.state.minZoom) { + this.setState({zoom: this.state.maxZoom}); return; } @@ -272,7 +278,7 @@ export default class ImageView extends React.Component { Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE ) { this.setState({ - zoom: MIN_ZOOM, + zoom: this.state.minZoom, translationX: 0, translationY: 0, }); @@ -311,7 +317,7 @@ export default class ImageView extends React.Component { let cursor; if (this.state.moving) { cursor= "grabbing"; - } else if (this.state.zoom === MIN_ZOOM) { + } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; } else { cursor = "zoom-out"; From 6a405fa8e8d76a0f47f9d5bfe144ec7880700ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 08:35:45 +0200 Subject: [PATCH 259/330] Don't use percanteages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I was an idiot to use them in the first place Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index ecc4303764..68567257f7 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -120,10 +120,10 @@ export default class ImageView extends React.Component { // TODO: What if we don't have width and height props? const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; - const zoomX = (imageWrapper.clientWidth / this.props.width) * 100; - const zoomY = (imageWrapper.clientHeight / this.props.height) * 100; + const zoomX = imageWrapper.clientWidth / this.props.width; + const zoomY = imageWrapper.clientHeight / this.props.height; const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; - const maxZoom = minZoom >= 100 ? minZoom : 100; + const maxZoom = minZoom >= 1 ? minZoom : 1; if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); this.setState({ @@ -323,7 +323,7 @@ export default class ImageView extends React.Component { cursor = "zoom-out"; } const rotationDegrees = this.state.rotation + "deg"; - const zoomPercentage = this.state.zoom/100; + const zoom = this.state.zoom; const translatePixelsX = this.state.translationX + "px"; const translatePixelsY = this.state.translationY + "px"; // The order of the values is important! @@ -335,7 +335,7 @@ export default class ImageView extends React.Component { transition: this.state.moving ? null : "transform 200ms ease 0s", transform: `translateX(${translatePixelsX}) translateY(${translatePixelsY}) - scale(${zoomPercentage}) + scale(${zoom}) rotate(${rotationDegrees})`, }; From 464ebe900dd0504a88f429a10f4ad1d71b5e38f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 08:37:51 +0200 Subject: [PATCH 260/330] Get rid of onWindowResize() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 68567257f7..cca4b34ad6 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -104,7 +104,7 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); - window.addEventListener("resize", this.onWindowResize); + window.addEventListener("resize", this.calculateZoom); this.calculateZoom(); } @@ -112,11 +112,7 @@ export default class ImageView extends React.Component { this.focusLock.current.removeEventListener('wheel', this.onWheel); } - private onWindowResize = (ev) => { - this.calculateZoom(); - } - - private calculateZoom() { + private calculateZoom = () => { // TODO: What if we don't have width and height props? const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; From 3ae0bc307c4d2896ea714eed9a0cfe6b1b4cef7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 08:38:13 +0200 Subject: [PATCH 261/330] Remove logline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index cca4b34ad6..543828dc55 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -100,7 +100,6 @@ export default class ImageView extends React.Component { private previousY = 0; componentDidMount() { - console.log("LOG calculating", this.props.width, this.props.height); // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); From 52e2c136d79b71897dd2ca11492b6b152e895436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 09:00:15 +0200 Subject: [PATCH 262/330] Use correct cursor when we can't zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 543828dc55..0db7d9401c 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -312,6 +312,8 @@ export default class ImageView extends React.Component { let cursor; if (this.state.moving) { cursor= "grabbing"; + } else if (this.state.maxZoom === this.state.minZoom) { + cursor = "pointer"; } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; } else { From 7e2a3e3c317bff1d64b51246997467c224c76cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 09:24:25 +0200 Subject: [PATCH 263/330] Use MAX_SCALE for maxZoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 0db7d9401c..a836409d4d 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -79,7 +79,7 @@ export default class ImageView extends React.Component { this.state = { zoom: 0, minZoom: MAX_SCALE, - maxZoom: 100, + maxZoom: MAX_SCALE, rotation: 0, translationX: 0, translationY: 0, From d0ba142b729c7c7588741a9009cb20285d4f6138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 09:41:46 +0200 Subject: [PATCH 264/330] Add some comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index a836409d4d..1679c40e76 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -115,9 +115,18 @@ export default class ImageView extends React.Component { // TODO: What if we don't have width and height props? const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; + const zoomX = imageWrapper.clientWidth / this.props.width; const zoomY = imageWrapper.clientHeight / this.props.height; + // We set minZoom to the min of the zoomX and zoomY to avoid overflow in + // any direction. We also multiply by MAX_SCALE to get a gap around the + // image by default const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; + // If minZoom is bigger or equal to 1, it means we scaling the image up + // to fit the viewport and therefore we want to disable zooming, so we + // set the maxZoom to be the same as the minZoom. Otherwise, we are + // scaling the image down - we want the user to be allowed to zoom to + // 100% const maxZoom = minZoom >= 1 ? minZoom : 1; if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); From bc62c6bec9de891f21bb4ec7d33ec5cf66b421cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 10:35:25 +0200 Subject: [PATCH 265/330] Fix zoom step and coeficient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 1679c40e76..af379a08e1 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -37,9 +37,9 @@ import {normalizeWheelEvent} from "../../../utils/Mouse"; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; // This is used for the buttons -const ZOOM_STEP = 10; +const ZOOM_STEP = 0.10; // This is used for mouse wheel events -const ZOOM_COEFFICIENT = 0.5; +const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; const IMAGE_WRAPPER_CLASS = "mx_ImageView_image_wrapper"; From dcd625c7e3ec9e2f619f22965504ae72680c77b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 24 Apr 2021 10:36:53 +0200 Subject: [PATCH 266/330] Rework zooming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 65 +++++++++------------ 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index af379a08e1..e5878d5c0e 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -136,20 +136,8 @@ export default class ImageView extends React.Component { }); } - private onKeyDown = (ev: KeyboardEvent) => { - if (ev.key === Key.ESCAPE) { - ev.stopPropagation(); - ev.preventDefault(); - this.props.onFinished(); - } - }; - - private onWheel = (ev: WheelEvent) => { - ev.stopPropagation(); - ev.preventDefault(); - - const {deltaY} = normalizeWheelEvent(ev); - const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT); + private zoom(delta: number) { + const newZoom = this.state.zoom + delta; if (newZoom <= this.state.minZoom) { this.setState({ @@ -167,6 +155,30 @@ export default class ImageView extends React.Component { this.setState({ zoom: newZoom, }); + } + + private onWheel = (ev: WheelEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + + const {deltaY} = normalizeWheelEvent(ev); + this.zoom(-(deltaY * ZOOM_COEFFICIENT)); + }; + + private onZoomInClick = () => { + this.zoom(ZOOM_STEP); + }; + + private onZoomOutClick = () => { + this.zoom(-ZOOM_STEP); + }; + + private onKeyDown = (ev: KeyboardEvent) => { + if (ev.key === Key.ESCAPE) { + ev.stopPropagation(); + ev.preventDefault(); + this.props.onFinished(); + } }; private onRotateCounterClockwiseClick = () => { @@ -181,31 +193,6 @@ export default class ImageView extends React.Component { this.setState({ rotation: rotationDegrees }); }; - private onZoomInClick = () => { - if (this.state.zoom >= this.state.maxZoom) { - this.setState({zoom: this.state.maxZoom}); - return; - } - - this.setState({ - zoom: this.state.zoom + ZOOM_STEP, - }); - }; - - private onZoomOutClick = () => { - if (this.state.zoom <= this.state.minZoom) { - this.setState({ - zoom: this.state.minZoom, - translationX: 0, - translationY: 0, - }); - return; - } - this.setState({ - zoom: this.state.zoom - ZOOM_STEP, - }); - }; - private onDownloadClick = () => { const a = document.createElement("a"); a.href = this.props.src; From 95ea71a23ad108f660ab93175c7fc95d0827e701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 13:11:41 +0200 Subject: [PATCH 267/330] Use a ref instead of that ugly thing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sometimes I do really weird things and don't know why :D Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index e5878d5c0e..fd559fd3cc 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -42,7 +42,6 @@ const ZOOM_STEP = 0.10; const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; -const IMAGE_WRAPPER_CLASS = "mx_ImageView_image_wrapper"; interface IProps { src: string, // the source of the image being displayed @@ -91,6 +90,7 @@ export default class ImageView extends React.Component { // XXX: Refs to functional components private contextMenuButton = createRef(); private focusLock = createRef(); + private imageWrapper = createRef(); private initX = 0; private initY = 0; @@ -114,7 +114,7 @@ export default class ImageView extends React.Component { private calculateZoom = () => { // TODO: What if we don't have width and height props? - const imageWrapper = document.getElementsByClassName(IMAGE_WRAPPER_CLASS)[0]; + const imageWrapper = this.imageWrapper.current; const zoomX = imageWrapper.clientWidth / this.props.width; const zoomY = imageWrapper.clientHeight / this.props.height; @@ -447,7 +447,9 @@ export default class ImageView extends React.Component { {this.renderContextMenu()}
    -
    +
    Date: Mon, 26 Apr 2021 13:30:14 +0200 Subject: [PATCH 268/330] Fall back to natural height and width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index fd559fd3cc..ee89dabc8e 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -91,6 +91,7 @@ export default class ImageView extends React.Component { private contextMenuButton = createRef(); private focusLock = createRef(); private imageWrapper = createRef(); + private image = createRef(); private initX = 0; private initY = 0; @@ -103,8 +104,10 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); + // We want to recalculate zoom whenever the windows size changes window.addEventListener("resize", this.calculateZoom); - this.calculateZoom(); + // After the image loads for the first time we want to calculate the zoom + this.image.current.addEventListener("load", this.calculateZoom); } componentWillUnmount() { @@ -112,12 +115,14 @@ export default class ImageView extends React.Component { } private calculateZoom = () => { - // TODO: What if we don't have width and height props? - + const image = this.image.current; const imageWrapper = this.imageWrapper.current; - const zoomX = imageWrapper.clientWidth / this.props.width; - const zoomY = imageWrapper.clientHeight / this.props.height; + const width = this.props.width || image.naturalWidth; + const height = this.props.height || image.naturalHeight; + + const zoomX = imageWrapper.clientWidth / width; + const zoomY = imageWrapper.clientHeight / height; // We set minZoom to the min of the zoomX and zoomY to avoid overflow in // any direction. We also multiply by MAX_SCALE to get a gap around the // image by default @@ -454,6 +459,7 @@ export default class ImageView extends React.Component { src={this.props.src} title={this.props.name} style={style} + ref={this.image} className="mx_ImageView_image" draggable={true} onMouseDown={this.onStartMoving} From ebe3b365281ccc330241e42ea6990f6e28af1cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 13:47:06 +0200 Subject: [PATCH 269/330] If the image is small don't scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index ee89dabc8e..e815e3be92 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -123,21 +123,26 @@ export default class ImageView extends React.Component { const zoomX = imageWrapper.clientWidth / width; const zoomY = imageWrapper.clientHeight / height; + + // If the image is smaller in both dimensions set its the zoom to 1 to + // display it in its original size + if (zoomX >= 1 && zoomY >= 1) { + this.setState({ + zoom: 1, + minZoom: 1, + maxZoom: 1, + }); + return; + } // We set minZoom to the min of the zoomX and zoomY to avoid overflow in // any direction. We also multiply by MAX_SCALE to get a gap around the // image by default const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; - // If minZoom is bigger or equal to 1, it means we scaling the image up - // to fit the viewport and therefore we want to disable zooming, so we - // set the maxZoom to be the same as the minZoom. Otherwise, we are - // scaling the image down - we want the user to be allowed to zoom to - // 100% - const maxZoom = minZoom >= 1 ? minZoom : 1; if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); this.setState({ minZoom: minZoom, - maxZoom: maxZoom, + maxZoom: 1, }); } From 3716ec4a25ee011915688ca6f8f3da8b6a32e8b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 13:48:14 +0200 Subject: [PATCH 270/330] Try to precalculate the zoom from width and height props MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index e815e3be92..0ad8435ef5 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -108,6 +108,8 @@ export default class ImageView extends React.Component { window.addEventListener("resize", this.calculateZoom); // After the image loads for the first time we want to calculate the zoom this.image.current.addEventListener("load", this.calculateZoom); + // Try to precalculate the zoom from width and height props + this.calculateZoom(); } componentWillUnmount() { From 2f147c2e98d67685cbce69fa3f7ed587b92fe855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 15:01:06 +0200 Subject: [PATCH 271/330] Change cursor to default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 0ad8435ef5..be5ea72d2c 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -321,7 +321,7 @@ export default class ImageView extends React.Component { if (this.state.moving) { cursor= "grabbing"; } else if (this.state.maxZoom === this.state.minZoom) { - cursor = "pointer"; + cursor = "default"; } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; } else { From 9a04f029aaacca489580271ca39b707641384cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 13:49:29 +0200 Subject: [PATCH 272/330] Fix spelling --- src/components/views/elements/ImageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index be5ea72d2c..f037168b63 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -104,7 +104,7 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); - // We want to recalculate zoom whenever the windows size changes + // We want to recalculate zoom whenever the window's size changes window.addEventListener("resize", this.calculateZoom); // After the image loads for the first time we want to calculate the zoom this.image.current.addEventListener("load", this.calculateZoom); From e820d60cd4efcbf3441d0e81d9aaa0ffe6ded2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 26 Apr 2021 15:47:58 +0200 Subject: [PATCH 273/330] Show zoom buttons only if zooming is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 34 ++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index f037168b63..fcacae2d39 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -316,11 +316,12 @@ export default class ImageView extends React.Component { render() { const showEventMeta = !!this.props.mxEvent; + const zoomingDisabled = this.state.maxZoom === this.state.minZoom; let cursor; if (this.state.moving) { cursor= "grabbing"; - } else if (this.state.maxZoom === this.state.minZoom) { + } else if (zoomingDisabled) { cursor = "default"; } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; @@ -412,6 +413,25 @@ export default class ImageView extends React.Component { ); } + let zoomOutButton; + let zoomInButton; + if (!zoomingDisabled) { + zoomOutButton = ( + + + ); + zoomInButton = ( + + + ); + } + return ( { title={_t("Rotate Left")} onClick={ this.onRotateCounterClockwiseClick }> - - - - + {zoomOutButton} + {zoomInButton} Date: Mon, 26 Apr 2021 15:51:56 +0200 Subject: [PATCH 274/330] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 133d24e3c8..0e43df5e5c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1917,10 +1917,10 @@ "collapse": "collapse", "expand": "expand", "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", - "Rotate Right": "Rotate Right", - "Rotate Left": "Rotate Left", "Zoom out": "Zoom out", "Zoom in": "Zoom in", + "Rotate Right": "Rotate Right", + "Rotate Left": "Rotate Left", "Download": "Download", "Information": "Information", "View message": "View message", From 4123406785d7171e627b886fc1a77fe206b3de85 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 26 Apr 2021 16:55:12 +0100 Subject: [PATCH 275/330] Move i18n utils to its own module --- package.json | 11 +- scripts/compare-file.js | 10 -- scripts/gen-i18n.js | 304 ---------------------------------------- scripts/prune-i18n.js | 68 --------- yarn.lock | 92 ++++++++++++ 5 files changed, 97 insertions(+), 388 deletions(-) delete mode 100644 scripts/compare-file.js delete mode 100755 scripts/gen-i18n.js delete mode 100755 scripts/prune-i18n.js diff --git a/package.json b/package.json index 7c190c68bf..a357678eaa 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,7 @@ "package.json" ], "bin": { - "reskindex": "scripts/reskindex.js", - "matrix-gen-i18n": "scripts/gen-i18n.js", - "matrix-prune-i18n": "scripts/prune-i18n.js" + "reskindex": "scripts/reskindex.js" }, "main": "./src/index.js", "matrix_src_main": "./src/index.js", @@ -33,9 +31,9 @@ "matrix_lib_typings": "./lib/index.d.ts", "scripts": { "prepublishOnly": "yarn build", - "i18n": "matrix-gen-i18n", - "prunei18n": "matrix-prune-i18n", - "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", + "i18n": "gen-i18n", + "prunei18n": "prune-i18n", + "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && gen-i18n && compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "reskindex": "node scripts/reskindex.js -h header", "reskindex:watch": "node scripts/reskindex.js -h header -w", "rethemendex": "res/css/rethemendex.sh", @@ -160,6 +158,7 @@ "jest-fetch-mock": "^3.0.3", "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", + "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "react-test-renderer": "^16.14.0", "rimraf": "^3.0.2", diff --git a/scripts/compare-file.js b/scripts/compare-file.js deleted file mode 100644 index f53275ebfa..0000000000 --- a/scripts/compare-file.js +++ /dev/null @@ -1,10 +0,0 @@ -const fs = require("fs"); - -if (process.argv.length < 4) throw new Error("Missing source and target file arguments"); - -const sourceFile = fs.readFileSync(process.argv[2], 'utf8'); -const targetFile = fs.readFileSync(process.argv[3], 'utf8'); - -if (sourceFile !== targetFile) { - throw new Error("Files do not match"); -} diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js deleted file mode 100755 index 91733469f7..0000000000 --- a/scripts/gen-i18n.js +++ /dev/null @@ -1,304 +0,0 @@ -#!/usr/bin/env node - -/* -Copyright 2017 New Vector Ltd - -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. -*/ - -/** - * Regenerates the translations en_EN file by walking the source tree and - * parsing each file with the appropriate parser. Emits a JSON file with the - * translatable strings mapped to themselves in the order they appeared - * in the files and grouped by the file they appeared in. - * - * Usage: node scripts/gen-i18n.js - */ -const fs = require('fs'); -const path = require('path'); - -const walk = require('walk'); - -const parser = require("@babel/parser"); -const traverse = require("@babel/traverse"); - -const TRANSLATIONS_FUNCS = ['_t', '_td']; - -const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; -const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; - -// NB. The sync version of walk is broken for single files so we walk -// all of res rather than just res/home.html. -// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it, -// or if we get bored waiting for it to be merged, we could switch -// to a project that's actively maintained. -const SEARCH_PATHS = ['src', 'res']; - -function getObjectValue(obj, key) { - for (const prop of obj.properties) { - if (prop.key.type === 'Identifier' && prop.key.name === key) { - return prop.value; - } - } - return null; -} - -function getTKey(arg) { - if (arg.type === 'Literal' || arg.type === "StringLiteral") { - return arg.value; - } else if (arg.type === 'BinaryExpression' && arg.operator === '+') { - return getTKey(arg.left) + getTKey(arg.right); - } else if (arg.type === 'TemplateLiteral') { - return arg.quasis.map((q) => { - return q.value.raw; - }).join(''); - } - return null; -} - -function getFormatStrings(str) { - // Match anything that starts with % - // We could make a regex that matched the full placeholder, but this - // would just not match invalid placeholders and so wouldn't help us - // detect the invalid ones. - // Also note that for simplicity, this just matches a % character and then - // anything up to the next % character (or a single %, or end of string). - const formatStringRe = /%([^%]+|%|$)/g; - const formatStrings = new Set(); - - let match; - while ( (match = formatStringRe.exec(str)) !== null ) { - const placeholder = match[1]; // Minus the leading '%' - if (placeholder === '%') continue; // Literal % is %% - - const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/); - if (placeholderMatch === null) { - throw new Error("Invalid format specifier: '"+match[0]+"'"); - } - if (placeholderMatch.length < 3) { - throw new Error("Malformed format specifier"); - } - const placeholderName = placeholderMatch[1]; - const placeholderFormat = placeholderMatch[2]; - - if (placeholderFormat !== 's') { - throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`); - } - - formatStrings.add(placeholderName); - } - - return formatStrings; -} - -function getTranslationsJs(file) { - const contents = fs.readFileSync(file, { encoding: 'utf8' }); - - const trs = new Set(); - - try { - const plugins = [ - // https://babeljs.io/docs/en/babel-parser#plugins - "classProperties", - "objectRestSpread", - "throwExpressions", - "exportDefaultFrom", - "decorators-legacy", - ]; - - if (file.endsWith(".js") || file.endsWith(".jsx")) { - // all JS is assumed to be flow or react - plugins.push("flow", "jsx"); - } else if (file.endsWith(".ts")) { - // TS can't use JSX unless it's a TSX file (otherwise angle casts fail) - plugins.push("typescript"); - } else if (file.endsWith(".tsx")) { - // When the file is a TSX file though, enable JSX parsing - plugins.push("typescript", "jsx"); - } - - const babelParsed = parser.parse(contents, { - allowImportExportEverywhere: true, - errorRecovery: true, - sourceFilename: file, - tokens: true, - plugins, - }); - traverse.default(babelParsed, { - enter: (p) => { - const node = p.node; - if (p.isCallExpression() && node.callee && TRANSLATIONS_FUNCS.includes(node.callee.name)) { - const tKey = getTKey(node.arguments[0]); - - // This happens whenever we call _t with non-literals (ie. whenever we've - // had to use a _td to compensate) so is expected. - if (tKey === null) return; - - // check the format string against the args - // We only check _t: _td has no args - if (node.callee.name === '_t') { - try { - const placeholders = getFormatStrings(tKey); - for (const placeholder of placeholders) { - if (node.arguments.length < 2) { - throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`); - } - const value = getObjectValue(node.arguments[1], placeholder); - if (value === null) { - throw new Error(`No value found for placeholder '${placeholder}'`); - } - } - - // Validate tag replacements - if (node.arguments.length > 2) { - const tagMap = node.arguments[2]; - for (const prop of tagMap.properties || []) { - if (prop.key.type === 'Literal') { - const tag = prop.key.value; - // RegExp same as in src/languageHandler.js - const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); - if (!tKey.match(regexp)) { - throw new Error(`No match for ${regexp} in ${tKey}`); - } - } - } - } - - } catch (e) { - console.log(); - console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); - console.error(e); - process.exit(1); - } - } - - let isPlural = false; - if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') { - const countVal = getObjectValue(node.arguments[1], 'count'); - if (countVal) { - isPlural = true; - } - } - - if (isPlural) { - trs.add(tKey + "|other"); - const plurals = enPlurals[tKey]; - if (plurals) { - for (const pluralType of Object.keys(plurals)) { - trs.add(tKey + "|" + pluralType); - } - } - } else { - trs.add(tKey); - } - } - }, - }); - } catch (e) { - console.error(e); - process.exit(1); - } - - return trs; -} - -function getTranslationsOther(file) { - const contents = fs.readFileSync(file, { encoding: 'utf8' }); - - const trs = new Set(); - - // Taken from element-web src/components/structures/HomePage.js - const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg; - let matches; - while (matches = translationsRegex.exec(contents)) { - trs.add(matches[1]); - } - return trs; -} - -// gather en_EN plural strings from the input translations file: -// the en_EN strings are all in the source with the exception of -// pluralised strings, which we need to pull in from elsewhere. -const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' })); -const enPlurals = {}; - -for (const key of Object.keys(inputTranslationsRaw)) { - const parts = key.split("|"); - if (parts.length > 1) { - const plurals = enPlurals[parts[0]] || {}; - plurals[parts[1]] = inputTranslationsRaw[key]; - enPlurals[parts[0]] = plurals; - } -} - -const translatables = new Set(); - -const walkOpts = { - listeners: { - names: function(root, nodeNamesArray) { - // Sort the names case insensitively and alphabetically to - // maintain some sense of order between the different strings. - nodeNamesArray.sort((a, b) => { - a = a.toLowerCase(); - b = b.toLowerCase(); - if (a > b) return 1; - if (a < b) return -1; - return 0; - }); - }, - file: function(root, fileStats, next) { - const fullPath = path.join(root, fileStats.name); - - let trs; - if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) { - trs = getTranslationsJs(fullPath); - } else if (fileStats.name.endsWith('.html')) { - trs = getTranslationsOther(fullPath); - } else { - return; - } - console.log(`${fullPath} (${trs.size} strings)`); - for (const tr of trs.values()) { - // Convert DOS line endings to unix - translatables.add(tr.replace(/\r\n/g, "\n")); - } - }, - } -}; - -for (const path of SEARCH_PATHS) { - if (fs.existsSync(path)) { - walk.walkSync(path, walkOpts); - } -} - -const trObj = {}; -for (const tr of translatables) { - if (tr.includes("|")) { - if (inputTranslationsRaw[tr]) { - trObj[tr] = inputTranslationsRaw[tr]; - } else { - trObj[tr] = tr.split("|")[0]; - } - } else { - trObj[tr] = tr; - } -} - -fs.writeFileSync( - OUTPUT_FILE, - JSON.stringify(trObj, translatables.values(), 4) + "\n" -); - -console.log(); -console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`); diff --git a/scripts/prune-i18n.js b/scripts/prune-i18n.js deleted file mode 100755 index b4fe8d69f5..0000000000 --- a/scripts/prune-i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node - -/* -Copyright 2017 New Vector Ltd - -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. -*/ - -/* - * Looks through all the translation files and removes any strings - * which don't appear in en_EN.json. - * Use this if you remove a translation, but merge any outstanding changes - * from weblate first or you'll need to resolve the conflict in weblate. - */ - -const fs = require('fs'); -const path = require('path'); - -const I18NDIR = 'src/i18n/strings'; - -const enStringsRaw = JSON.parse(fs.readFileSync(path.join(I18NDIR, 'en_EN.json'))); - -const enStrings = new Set(); -for (const str of Object.keys(enStringsRaw)) { - const parts = str.split('|'); - if (parts.length > 1) { - enStrings.add(parts[0]); - } else { - enStrings.add(str); - } -} - -for (const filename of fs.readdirSync(I18NDIR)) { - if (filename === 'en_EN.json') continue; - if (filename === 'basefile.json') continue; - if (!filename.endsWith('.json')) continue; - - const trs = JSON.parse(fs.readFileSync(path.join(I18NDIR, filename))); - const oldLen = Object.keys(trs).length; - for (const tr of Object.keys(trs)) { - const parts = tr.split('|'); - const trKey = parts.length > 1 ? parts[0] : tr; - if (!enStrings.has(trKey)) { - delete trs[tr]; - } - } - - const removed = oldLen - Object.keys(trs).length; - if (removed > 0) { - console.log(`${filename}: removed ${removed} translations`); - // XXX: This is totally relying on the impl serialising the JSON object in the - // same order as they were parsed from the file. JSON.stringify() has a specific argument - // that can be used to control the order, but JSON.parse() lacks any kind of equivalent. - // Empirically this does maintain the order on my system, so I'm going to leave it like - // this for now. - fs.writeFileSync(path.join(I18NDIR, filename), JSON.stringify(trs, undefined, 4) + "\n"); - } -} diff --git a/yarn.lock b/yarn.lock index 66329cfa89..f3f58fd8b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,6 +26,13 @@ dependencies: "@babel/highlight" "^7.10.4" +"@babel/code-frame@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" + integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== + dependencies: + "@babel/highlight" "^7.12.13" + "@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41" @@ -61,6 +68,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.13.16": + version "7.13.16" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.16.tgz#0befc287031a201d84cdfc173b46b320ae472d14" + integrity sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg== + dependencies: + "@babel/types" "^7.13.16" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d" @@ -130,6 +146,15 @@ "@babel/template" "^7.12.7" "@babel/types" "^7.12.11" +"@babel/helper-function-name@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" + integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA== + dependencies: + "@babel/helper-get-function-arity" "^7.12.13" + "@babel/template" "^7.12.13" + "@babel/types" "^7.12.13" + "@babel/helper-get-function-arity@^7.12.10": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" @@ -137,6 +162,13 @@ dependencies: "@babel/types" "^7.12.10" +"@babel/helper-get-function-arity@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" + integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg== + dependencies: + "@babel/types" "^7.12.13" + "@babel/helper-hoist-variables@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" @@ -225,6 +257,13 @@ dependencies: "@babel/types" "^7.12.11" +"@babel/helper-split-export-declaration@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" + integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== + dependencies: + "@babel/types" "^7.12.13" + "@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" @@ -263,11 +302,25 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.12.13": + version "7.13.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" + integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== +"@babel/parser@^7.12.13", "@babel/parser@^7.13.16": + version "7.13.16" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.16.tgz#0f18179b0448e6939b1f3f5c4c355a3a9bcdfd37" + integrity sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw== + "@babel/plugin-proposal-async-generator-functions@^7.12.1": version "7.12.12" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566" @@ -980,6 +1033,15 @@ "@babel/parser" "^7.12.7" "@babel/types" "^7.12.7" +"@babel/template@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" + integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/parser" "^7.12.13" + "@babel/types" "^7.12.13" + "@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.12", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.4": version "7.12.12" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" @@ -995,6 +1057,20 @@ globals "^11.1.0" lodash "^4.17.19" +"@babel/traverse@^7.13.17": + version "7.13.17" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.17.tgz#c85415e0c7d50ac053d758baec98b28b2ecfeea3" + integrity sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.13.16" + "@babel/helper-function-name" "^7.12.13" + "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/parser" "^7.13.16" + "@babel/types" "^7.13.17" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0": version "7.12.12" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" @@ -1004,6 +1080,14 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.12.13", "@babel/types@^7.13.16", "@babel/types@^7.13.17": + version "7.13.17" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.17.tgz#48010a115c9fba7588b4437dd68c9469012b38b4" + integrity sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -5614,6 +5698,14 @@ matrix-react-test-utils@^0.2.2: resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853" integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== +"matrix-web-i18n@github:matrix-org/matrix-web-i18n": + version "1.1.1" + resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/68ea0c57b6c74c40df6419eb5ac0fa8945ff8a75" + dependencies: + "@babel/parser" "^7.13.16" + "@babel/traverse" "^7.13.17" + walk "^2.3.14" + matrix-widget-api@^0.1.0-beta.13: version "0.1.0-beta.13" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.13.tgz#ebddc83eaef39bbb87b621a02a35902e1a29b9ef" From 417f662ea783a143ebcfca3ac79199d1b3d3137f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 17:12:31 +0100 Subject: [PATCH 276/330] Remove redundant parameter initialisers --- src/ScalarAuthClient.ts | 2 -- src/Terms.ts | 3 --- src/utils/Timer.ts | 1 - 3 files changed, 6 deletions(-) diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts index c18265be55..c8695eb80f 100644 --- a/src/ScalarAuthClient.ts +++ b/src/ScalarAuthClient.ts @@ -35,8 +35,6 @@ export default class ScalarAuthClient { private isDefaultManager: boolean; constructor(private apiUrl: string, private uiUrl: string) { - this.apiUrl = apiUrl; - this.uiUrl = uiUrl; this.scalarToken = null; // `undefined` to allow `startTermsFlow` to fallback to a default // callback if this is unset. diff --git a/src/Terms.ts b/src/Terms.ts index 31eeb6b29a..1bdff36cbc 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -33,9 +33,6 @@ export class Service { * @param {string} accessToken The user's access token for the service */ constructor(public serviceType: string, public baseUrl: string, public accessToken: string) { - this.serviceType = serviceType; - this.baseUrl = baseUrl; - this.accessToken = accessToken; } } diff --git a/src/utils/Timer.ts b/src/utils/Timer.ts index 26170491d8..0b846e02ab 100644 --- a/src/utils/Timer.ts +++ b/src/utils/Timer.ts @@ -33,7 +33,6 @@ export default class Timer { private reject: (Error) => void; constructor(private timeout: number) { - this.timeout = timeout; this.setNotStarted(); } From a79b0e93273020b4e0e5950354ef1890ec2ecbf9 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 26 Apr 2021 17:42:04 +0100 Subject: [PATCH 277/330] Upgrade matrix-js-sdk to 10.0.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index dc9a9057d6..20fc9ea23e 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "10.0.0-rc.1", + "matrix-js-sdk": "10.0.0", "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 845a7b8a34..728b185a29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5587,10 +5587,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@10.0.0-rc.1: - version "10.0.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-10.0.0-rc.1.tgz#e99ff19fa02ad6526cd62a20767104591b4e0720" - integrity sha512-3dwM9BFFAW1RC55+XHUpSfV4lQmyrx8peLW+3p+uIbZNgtPV/+h2X0ja281SVipdePJ50gYF9Iif+UkLkXXuug== +matrix-js-sdk@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-10.0.0.tgz#571e97c8d8351715ac609ccedd38cad79d0b752e" + integrity sha512-40QN9HITqWBSYi/e8QQidDL6UOhWBpst437i+lHIqQ8a7SQtbcquDSRXWR22BjM2qbssR+02zfrLI/Kez7IoBQ== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From a6e790aa1de5b4fc07e6b65277c9d5f20f90c290 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 26 Apr 2021 17:47:09 +0100 Subject: [PATCH 278/330] Prepare changelog for v3.19.0 --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0158e305bb..d459b4e94a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0) + + * Upgrade to JS SDK 10.0.0 + * [Release] Dynamic max and min zoom in the new ImageView + [\#5927](https://github.com/matrix-org/matrix-react-sdk/pull/5927) + * [Release] Add a WheelEvent normalization function + [\#5911](https://github.com/matrix-org/matrix-react-sdk/pull/5911) + * Add a WheelEvent normalization function + [\#5904](https://github.com/matrix-org/matrix-react-sdk/pull/5904) + * [Release] Use floats for image background opacity + [\#5907](https://github.com/matrix-org/matrix-react-sdk/pull/5907) + Changes in [3.19.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0-rc.1) (2021-04-21) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0...v3.19.0-rc.1) From 87e3ad303f93f770f6adccc8bbd4a9d305d612cb Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 26 Apr 2021 17:47:10 +0100 Subject: [PATCH 279/330] v3.19.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 20fc9ea23e..0bb37a267c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.19.0-rc.1", + "version": "3.19.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From c3f9bce016f306e0fd2b58364f5f90efbe8db194 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 26 Apr 2021 17:50:19 +0100 Subject: [PATCH 280/330] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0bb37a267c..9f4c16d674 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "matrix-gen-i18n": "scripts/gen-i18n.js", "matrix-prune-i18n": "scripts/prune-i18n.js" }, - "main": "./lib/index.js", + "main": "./src/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -190,6 +190,5 @@ "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" ] - }, - "typings": "./lib/index.d.ts" + } } From 3817aaeaca9622c56894df4092b5ce727b943f64 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 26 Apr 2021 17:50:40 +0100 Subject: [PATCH 281/330] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9f4c16d674..b8f0db800a 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "10.0.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 728b185a29..b658a73b60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5587,10 +5587,9 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@10.0.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "10.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-10.0.0.tgz#571e97c8d8351715ac609ccedd38cad79d0b752e" - integrity sha512-40QN9HITqWBSYi/e8QQidDL6UOhWBpst437i+lHIqQ8a7SQtbcquDSRXWR22BjM2qbssR+02zfrLI/Kez7IoBQ== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c8f69c0b7937b9064938c134d708c4d064b71315" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 915f8b3c9ca7fbfa65eea9ba91f739fc9c2377c0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 26 Apr 2021 18:25:49 +0100 Subject: [PATCH 282/330] Scale all mxc thumbs using device pixel ratio for hidpi as we are notoriously bad at doing it everywhere we ought to, like the TopLeftMenu avatar --- src/Avatar.ts | 12 ++---------- src/components/structures/SpaceRoomDirectory.tsx | 2 +- src/components/views/avatars/MemberAvatar.tsx | 4 ++-- src/components/views/avatars/RoomAvatar.tsx | 11 +++-------- src/components/views/dialogs/IncomingSasDialog.js | 2 +- src/components/views/messages/MImageBody.js | 5 ++--- src/customisations/Media.ts | 7 +++++++ src/editor/parts.ts | 12 ++---------- 8 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/Avatar.ts b/src/Avatar.ts index 76c88faa1c..d218ae8b46 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -27,11 +27,7 @@ export type ResizeMethod = "crop" | "scale"; export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) { let url: string; if (member?.getMxcAvatarUrl()) { - url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp( - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - ); + url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); } if (!url) { // member can be null here currently since on invites, the JS SDK @@ -44,11 +40,7 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { if (!user.avatarUrl) return null; - return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp( - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - ); + return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); } function isValidHexColor(color: string): boolean { diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 930cfa15a9..513aa25849 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -136,7 +136,7 @@ const Tile: React.FC = ({ let url: string; if (room.avatar_url) { - url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(20 * window.devicePixelRatio)); + url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20); } let description = _t("%(count)s members", { count: room.num_joined_members }); diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index c79cbc0d32..3205ca372c 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -68,8 +68,8 @@ export default class MemberAvatar extends React.Component { let imageUrl = null; if (props.member.getMxcAvatarUrl()) { imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp( - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), + props.width, + props.height, props.resizeMethod, ); } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index ad0eb45a52..4693d907ba 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -93,8 +93,8 @@ export default class RoomAvatar extends React.Component { let oobAvatar = null; if (props.oobData.avatarUrl) { oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp( - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), + props.width, + props.height, props.resizeMethod, ); } @@ -109,12 +109,7 @@ export default class RoomAvatar extends React.Component { private static getRoomAvatarUrl(props: IProps): string { if (!props.room) return null; - return Avatar.avatarUrlForRoom( - props.room, - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), - props.resizeMethod, - ); + return Avatar.avatarUrlForRoom(props.room, props.width, props.height, props.resizeMethod); } private onRoomAvatarClick = () => { diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index f18b7a9d0c..5df02d7a6f 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -130,7 +130,7 @@ export default class IncomingSasDialog extends React.Component { const oppProfile = this.state.opponentProfile; if (oppProfile) { const url = oppProfile.avatar_url - ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio)) + ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48) : null; profile =
    Date: Tue, 27 Apr 2021 09:56:28 +0100 Subject: [PATCH 283/330] fix removed pixelRatio --- src/components/views/messages/MImageBody.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 06aae03e90..07a12d70a8 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -217,7 +217,7 @@ export default class MImageBody extends React.Component { const info = content.info; if ( this._isGif() || - pixelRatio === 1.0 || + window.devicePixelRatio === 1.0 || (!info || !info.w || !info.h || !info.size) ) { return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight); From 5107ce7f4087b2b4f0325b8538589d576ab123a6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 27 Apr 2021 11:22:17 +0100 Subject: [PATCH 284/330] Add types to ScalarAuthClient --- src/ScalarAuthClient.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/ScalarAuthClient.ts b/src/ScalarAuthClient.ts index c8695eb80f..a09c3494a8 100644 --- a/src/ScalarAuthClient.ts +++ b/src/ScalarAuthClient.ts @@ -23,6 +23,7 @@ import request from "browser-request"; import SdkConfig from "./SdkConfig"; import {WidgetType} from "./widgets/WidgetType"; import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; +import { Room } from "matrix-js-sdk/src/models/room"; // The version of the integration manager API we're intending to work with const imApiVersion = "1.1"; @@ -57,7 +58,7 @@ export default class ScalarAuthClient { } } - private readTokenFromStore() { + private readTokenFromStore(): string { let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); if (!token && this.isDefaultManager) { token = window.localStorage.getItem("mx_scalar_token"); @@ -65,7 +66,7 @@ export default class ScalarAuthClient { return token; } - private readToken() { + private readToken(): string { if (this.scalarToken) return this.scalarToken; return this.readTokenFromStore(); } @@ -74,18 +75,18 @@ export default class ScalarAuthClient { this.termsInteractionCallback = callback; } - connect() { + connect(): Promise { return this.getScalarToken().then((tok) => { this.scalarToken = tok; }); } - hasCredentials() { + hasCredentials(): boolean { return this.scalarToken != null; // undef or null } // Returns a promise that resolves to a scalar_token string - getScalarToken() { + getScalarToken(): Promise { const token = this.readToken(); if (!token) { @@ -101,7 +102,7 @@ export default class ScalarAuthClient { } } - private getAccountName(token) { + private getAccountName(token: string): Promise { const url = this.apiUrl + "/account"; return new Promise(function(resolve, reject) { @@ -126,7 +127,7 @@ export default class ScalarAuthClient { }); } - private checkToken(token) { + private checkToken(token: string): Promise { return this.getAccountName(token).then(userId => { const me = MatrixClientPeg.get().getUserId(); if (userId !== me) { @@ -166,7 +167,7 @@ export default class ScalarAuthClient { }); } - registerForToken() { + registerForToken(): Promise { // Get openid bearer token from the HS as the first part of our dance return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { // Now we can send that to scalar and exchange it for a scalar token @@ -181,7 +182,7 @@ export default class ScalarAuthClient { }); } - exchangeForScalarToken(openidTokenObject) { + exchangeForScalarToken(openidTokenObject: any): Promise { const scalarRestUrl = this.apiUrl; return new Promise(function(resolve, reject) { @@ -205,7 +206,7 @@ export default class ScalarAuthClient { }); } - getScalarPageTitle(url) { + getScalarPageTitle(url: string): Promise { let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup'; scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); @@ -241,7 +242,7 @@ export default class ScalarAuthClient { * @param {string} widgetId The widget ID to disable assets for * @return {Promise} Resolves on completion */ - disableWidgetAssets(widgetType: WidgetType, widgetId) { + disableWidgetAssets(widgetType: WidgetType, widgetId: string): Promise { let url = this.apiUrl + '/widgets/set_assets_state'; url = this.getStarterLink(url); return new Promise((resolve, reject) => { @@ -268,7 +269,7 @@ export default class ScalarAuthClient { }); } - getScalarInterfaceUrlForRoom(room, screen, id) { + getScalarInterfaceUrlForRoom(room: Room, screen: string, id: string): string { const roomId = room.roomId; const roomName = room.name; let url = this.uiUrl; @@ -285,7 +286,7 @@ export default class ScalarAuthClient { return url; } - getStarterLink(starterLinkUrl) { + getStarterLink(starterLinkUrl: string): string { return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); } } From 2be8f0c9c78ea7a1247f9a4e79b7eb296421033f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 27 Apr 2021 11:26:37 +0100 Subject: [PATCH 285/330] Fix onFinished type --- .../views/dialogs/eventindex/ManageEventIndexDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 78945a96f5..0710c513da 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -26,7 +26,7 @@ import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import {SettingLevel} from "../../../../settings/SettingLevel"; interface IProps { - onFinished: (boolean) => void; + onFinished: (confirmed: boolean) => void; } interface IState { From 2ebd25659052a1fc7c8207e4ba846cd00ab9906c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 27 Apr 2021 11:42:11 +0100 Subject: [PATCH 286/330] Add types to RolesRoomSettingsTab --- .../settings/tabs/room/RolesRoomSettingsTab.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index acf98edc18..4fa521f598 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -23,6 +23,8 @@ import Modal from "../../../../../Modal"; import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import {EventType} from "matrix-js-sdk/src/@types/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { RoomState } from "matrix-js-sdk/src/models/room-state"; const plEventsToLabels = { // These will be translated for us later. @@ -115,23 +117,23 @@ interface IProps { @replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab") export default class RolesRoomSettingsTab extends React.Component { - componentDidMount(): void { + componentDidMount() { MatrixClientPeg.get().on("RoomState.members", this.onRoomMembership); } - componentWillUnmount(): void { + componentWillUnmount() { const client = MatrixClientPeg.get(); if (client) { client.removeListener("RoomState.members", this.onRoomMembership); } } - private onRoomMembership = (event, state, member) => { + private onRoomMembership = (event: MatrixEvent, state: RoomState, member: RoomMember) => { if (state.roomId !== this.props.roomId) return; this.forceUpdate(); }; - private populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) { + private populateDefaultPlEvents(eventsSection: Record, stateLevel: number, eventsLevel: number) { for (const desiredEvent of Object.keys(plEventsToShow)) { if (!(desiredEvent in eventsSection)) { eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel); @@ -139,7 +141,7 @@ export default class RolesRoomSettingsTab extends React.Component { } } - private onPowerLevelsChanged = (value, powerLevelKey) => { + private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); @@ -150,7 +152,7 @@ export default class RolesRoomSettingsTab extends React.Component { const eventsLevelPrefix = "event_levels_"; - value = parseInt(value); + const value = parseInt(inputValue); if (powerLevelKey.startsWith(eventsLevelPrefix)) { // deep copy "events" object, Object.assign itself won't deep copy @@ -184,7 +186,7 @@ export default class RolesRoomSettingsTab extends React.Component { }); }; - private onUserPowerLevelChanged = (value, powerLevelKey) => { + private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); From 4e7240ebc9bef608e1de8f94875409b179029319 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 27 Apr 2021 11:56:45 +0100 Subject: [PATCH 287/330] Add types to SecurityRoomSettingsTab --- .../tabs/room/SecurityRoomSettingsTab.tsx | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 3beb329dc1..02bbcfb751 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import {_t} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../.."; @@ -74,7 +75,7 @@ export default class SecurityRoomSettingsTab extends React.Component { // eslint-disable-line camelcase + async UNSAFE_componentWillMount() { // eslint-disable-line camelcase MatrixClientPeg.get().on("RoomState.events", this.onStateEvent); const room = MatrixClientPeg.get().getRoom(this.props.roomId); @@ -83,17 +84,17 @@ export default class SecurityRoomSettingsTab extends React.Component(event: MatrixEvent, key: string, defaultValue: T): T { if (!event || !event.getContent()) return defaultValue; return event.getContent()[key] || defaultValue; } - componentWillUnmount(): void { + componentWillUnmount() { MatrixClientPeg.get().removeListener("RoomState.events", this.onStateEvent); } - private onStateEvent = (e) => { + private onStateEvent = (e: MatrixEvent) => { const refreshWhenTypes = [ 'm.room.join_rules', 'm.room.guest_access', @@ -120,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private onEncryptionChange = (e: React.ChangeEvent) => { Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, { title: _t('Enable encryption?'), description: _t( @@ -153,7 +154,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private fixGuestAccess = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -175,7 +176,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private onRoomAccessRadioToggle = (roomAccess: string) => { // join_rule // INVITE | PUBLIC // ----------------------+---------------- @@ -221,7 +222,7 @@ export default class SecurityRoomSettingsTab extends React.Component { + private onHistoryRadioToggle = (history: HistoryVisibility) => { const beforeHistory = this.state.history; this.setState({history: history}); MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", { @@ -232,11 +233,11 @@ export default class SecurityRoomSettingsTab extends React.Component { + private updateBlacklistDevicesFlag = (checked: boolean) => { MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked); }; - private async hasAliases() { + private async hasAliases(): Promise { const cli = MatrixClientPeg.get(); if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) { const response = await cli.unstableGetLocalAliases(this.props.roomId); @@ -335,22 +336,22 @@ export default class SecurityRoomSettingsTab extends React.Component Date: Tue, 27 Apr 2021 12:00:36 +0100 Subject: [PATCH 288/330] Add types to PreferencesUserSettingsTab --- .../tabs/user/PreferencesUserSettingsTab.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 7e2da2b53b..f02c5c9ce0 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -103,7 +103,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta }; } - async componentDidMount(): Promise { + async componentDidMount() { const platform = PlatformPeg.get(); const autoLaunchSupported = await platform.supportsAutoLaunch(); @@ -142,38 +142,38 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta }); } - private onAutoLaunchChange = (checked) => { + private onAutoLaunchChange = (checked: boolean) => { PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked})); }; - private onWarnBeforeExitChange = (checked) => { + private onWarnBeforeExitChange = (checked: boolean) => { PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({warnBeforeExit: checked})); } - private onAlwaysShowMenuBarChange = (checked) => { + private onAlwaysShowMenuBarChange = (checked: boolean) => { PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked})); }; - private onMinimizeToTrayChange = (checked) => { + private onMinimizeToTrayChange = (checked: boolean) => { PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked})); }; - private onAutocompleteDelayChange = (e) => { + private onAutocompleteDelayChange = (e: React.ChangeEvent) => { this.setState({autocompleteDelay: e.target.value}); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); }; - private onReadMarkerInViewThresholdMs = (e) => { + private onReadMarkerInViewThresholdMs = (e: React.ChangeEvent) => { this.setState({readMarkerInViewThresholdMs: e.target.value}); SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); }; - private onReadMarkerOutOfViewThresholdMs = (e) => { + private onReadMarkerOutOfViewThresholdMs = (e: React.ChangeEvent) => { this.setState({readMarkerOutOfViewThresholdMs: e.target.value}); SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); }; - private renderGroup(settingIds) { + private renderGroup(settingIds: string[]): React.ReactNodeArray { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); return settingIds.filter(SettingsStore.isEnabled).map(i => { return ; From bca45a1ad42a391696298fa1652d952a38fdb3a5 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 27 Apr 2021 12:02:20 +0100 Subject: [PATCH 289/330] Add types to Timer --- src/utils/Timer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Timer.ts b/src/utils/Timer.ts index 0b846e02ab..9760631d09 100644 --- a/src/utils/Timer.ts +++ b/src/utils/Timer.ts @@ -59,7 +59,7 @@ export default class Timer { } } - changeTimeout(timeout) { + changeTimeout(timeout: number) { if (timeout === this.timeout) { return; } From b8203043be45f4c55cc3039c3f9c475e2dc7600c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 27 Apr 2021 12:11:23 +0100 Subject: [PATCH 290/330] Add types to Permalinks --- src/utils/permalinks/Permalinks.ts | 36 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/utils/permalinks/Permalinks.ts b/src/utils/permalinks/Permalinks.ts index cb463d6781..2ef955c358 100644 --- a/src/utils/permalinks/Permalinks.ts +++ b/src/utils/permalinks/Permalinks.ts @@ -17,6 +17,9 @@ limitations under the License. import isIp from "is-ip"; import * as utils from "matrix-js-sdk/src/utils"; import {Room} from "matrix-js-sdk/src/models/room"; +import {EventType} from "matrix-js-sdk/src/@types/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import {MatrixClientPeg} from "../../MatrixClientPeg"; import SpecPermalinkConstructor, {baseUrl as matrixtoBaseUrl} from "./SpecPermalinkConstructor"; @@ -99,9 +102,6 @@ export class RoomPermalinkCreator { if (!this.roomId) { throw new Error("Failed to resolve a roomId for the permalink creator to use"); } - - this.onMembership = this.onMembership.bind(this); - this.onRoomState = this.onRoomState.bind(this); } load() { @@ -140,11 +140,11 @@ export class RoomPermalinkCreator { return this.started; } - forEvent(eventId) { + forEvent(eventId: string): string { return getPermalinkConstructor().forEvent(this.roomId, eventId, this._serverCandidates); } - forShareableRoom() { + forShareableRoom(): string { if (this.room) { // Prefer to use canonical alias for permalink if possible const alias = this.room.getCanonicalAlias(); @@ -155,26 +155,26 @@ export class RoomPermalinkCreator { return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates); } - forRoom() { + forRoom(): string { return getPermalinkConstructor().forRoom(this.roomId, this._serverCandidates); } - private onRoomState(event) { + private onRoomState = (event: MatrixEvent) => { switch (event.getType()) { - case "m.room.server_acl": + case EventType.RoomServerAcl: this.updateAllowedServers(); this.updateHighestPlUser(); this.updatePopulationMap(); this.updateServerCandidates(); return; - case "m.room.power_levels": + case EventType.RoomPowerLevels: this.updateHighestPlUser(); this.updateServerCandidates(); return; } } - private onMembership(evt, member, oldMembership) { + private onMembership = (evt: MatrixEvent, member: RoomMember, oldMembership: string) => { const userId = member.userId; const membership = member.membership; const serverName = getServerName(userId); @@ -282,11 +282,11 @@ export function makeGenericPermalink(entityId: string): string { return getPermalinkConstructor().forEntity(entityId); } -export function makeUserPermalink(userId) { +export function makeUserPermalink(userId: string): string { return getPermalinkConstructor().forUser(userId); } -export function makeRoomPermalink(roomId) { +export function makeRoomPermalink(roomId: string): string { if (!roomId) { throw new Error("can't permalink a falsey roomId"); } @@ -305,7 +305,7 @@ export function makeRoomPermalink(roomId) { return permalinkCreator.forRoom(); } -export function makeGroupPermalink(groupId) { +export function makeGroupPermalink(groupId: string): string { return getPermalinkConstructor().forGroup(groupId); } @@ -437,24 +437,24 @@ export function parseAppLocalLink(localLink: string): PermalinkParts { return null; } -function getServerName(userId) { +function getServerName(userId: string): string { return userId.split(":").splice(1).join(":"); } -function getHostnameFromMatrixDomain(domain) { +function getHostnameFromMatrixDomain(domain: string): string { if (!domain) return null; return new URL(`https://${domain}`).hostname; } -function isHostInRegex(hostname, regexps) { +function isHostInRegex(hostname: string, regexps: RegExp[]) { hostname = getHostnameFromMatrixDomain(hostname); if (!hostname) return true; // assumed - if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0]); + if (regexps.length > 0 && !regexps[0].test) throw new Error(regexps[0].toString()); return regexps.filter(h => h.test(hostname)).length > 0; } -function isHostnameIpAddress(hostname) { +function isHostnameIpAddress(hostname: string): boolean { hostname = getHostnameFromMatrixDomain(hostname); if (!hostname) return false; From 9b2eb8ebc03000e3ee30618c59e0cbdf8d5717bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 27 Apr 2021 15:27:11 +0200 Subject: [PATCH 291/330] Set box-shadow opacity to 20% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallView.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 80fa985e5d..7292e325df 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -41,7 +41,7 @@ limitations under the License. padding-bottom: 8px; margin-top: 10px; background-color: $voipcall-plinth-color; - box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5); + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; .mx_CallView_voice { From c08fe6aa22bdf6aa3d60599e978b942933f8ecc8 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 27 Apr 2021 15:09:26 +0100 Subject: [PATCH 292/330] Upgrade matrix-web-i18n to use matrix prefixed binaries --- package.json | 6 +++--- yarn.lock | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a357678eaa..c09046bbb8 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "matrix_lib_typings": "./lib/index.d.ts", "scripts": { "prepublishOnly": "yarn build", - "i18n": "gen-i18n", - "prunei18n": "prune-i18n", - "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && gen-i18n && compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", + "i18n": "matrix-gen-i18n", + "prunei18n": "matrix-prune-i18n", + "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "reskindex": "node scripts/reskindex.js -h header", "reskindex:watch": "node scripts/reskindex.js -h header -w", "rethemendex": "res/css/rethemendex.sh", diff --git a/yarn.lock b/yarn.lock index f3f58fd8b2..a451714904 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5699,8 +5699,8 @@ matrix-react-test-utils@^0.2.2: integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== "matrix-web-i18n@github:matrix-org/matrix-web-i18n": - version "1.1.1" - resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/68ea0c57b6c74c40df6419eb5ac0fa8945ff8a75" + version "1.1.2" + resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/63f9119bc0bc304e83d4e8e22364caa7850e7671" dependencies: "@babel/parser" "^7.13.16" "@babel/traverse" "^7.13.17" From 2e44d7e17f6050bd60413e17fac189551cb6ba76 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 27 Apr 2021 15:11:49 +0100 Subject: [PATCH 293/330] Do not throw on setLanguage --- src/BasePlatform.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index b10513b46b..5483ea6874 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -258,9 +258,7 @@ export default abstract class BasePlatform { return null; } - async setLanguage(preferredLangs: string[]) { - throw new Error("Unimplemented"); - } + async setLanguage(preferredLangs: string[]) {} setSpellCheckLanguages(preferredLangs: string[]) {} From b9bd83ad41f4b63b539e027413dce94a706be647 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 08:58:18 -0600 Subject: [PATCH 294/330] Handle possible edge case with getting stuck in "unsent messages" bar Just in case we're not cleaning up the isResending state properly, here's a catch all. Unrelated to https://github.com/vector-im/element-web/issues/17078 (this code doesn't affect the js-sdk error the author is seeing) --- src/components/structures/RoomStatusBar.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index ab4f524faf..38e3cd97e8 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -128,7 +128,11 @@ export default class RoomStatusBar extends React.Component { _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => { if (room.roomId !== this.props.room.roomId) return; - this.setState({unsentMessages: getUnsentMessages(this.props.room)}); + const messages = getUnsentMessages(this.props.room); + this.setState({ + unsentMessages: messages, + isResending: messages.length > 0 && this.state.isResending, + }); }; // Check whether current size is greater than 0, if yes call props.onVisible From 46bfbbadf9e448b795610c5bbe28da4d5bc21e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 27 Apr 2021 17:23:27 +0200 Subject: [PATCH 295/330] Enable indent rule and fix indent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .eslintrc.js | 1 - src/GroupAddressPicker.js | 4 +- src/IdentityAuthClient.js | 2 +- src/PasswordReset.js | 2 +- src/TextForEvent.js | 6 +- .../dialogs/security/CreateKeyBackupDialog.js | 6 +- .../security/CreateSecretStorageDialog.js | 6 +- .../dialogs/security/ExportE2eKeysDialog.js | 7 +- .../dialogs/security/ImportE2eKeysDialog.js | 56 +++++------ src/components/structures/FilePanel.js | 8 +- src/components/structures/GroupFilterPanel.js | 20 ++-- src/components/structures/GroupView.js | 44 +++++---- src/components/structures/MessagePanel.js | 26 ++--- src/components/structures/RoomStatusBar.js | 32 +++--- src/components/structures/ScrollPanel.js | 25 ++--- src/components/structures/TimelinePanel.js | 14 +-- .../auth/InteractiveAuthEntryComponents.js | 4 +- .../views/context_menus/MessageContextMenu.js | 2 +- .../views/dialogs/BugReportDialog.js | 2 +- .../views/dialogs/ChangelogDialog.js | 2 +- .../views/dialogs/ConfirmWipeDeviceDialog.js | 9 +- .../views/dialogs/DevtoolsDialog.js | 94 ++++++++++-------- .../dialogs/IntegrationsDisabledDialog.js | 9 +- .../dialogs/IntegrationsImpossibleDialog.js | 9 +- .../dialogs/KeySignatureUploadFailedDialog.js | 10 +- .../views/dialogs/MessageEditHistoryDialog.js | 8 +- .../views/dialogs/RoomSettingsDialog.js | 2 +- .../dialogs/SessionRestoreErrorDialog.js | 2 +- .../views/dialogs/StorageEvictedDialog.js | 5 +- .../views/dialogs/UserSettingsDialog.js | 2 +- .../dialogs/VerificationRequestDialog.js | 12 ++- .../dialogs/WidgetOpenIDPermissionsDialog.js | 4 +- .../ConfirmDestroyCrossSigningDialog.js | 8 +- .../security/RestoreKeyBackupDialog.js | 14 +-- src/components/views/elements/ActionButton.js | 4 +- src/components/views/elements/AppTile.js | 2 +- .../views/elements/EditableItemList.js | 20 ++-- src/components/views/elements/EditableText.js | 16 +-- .../views/elements/LabelledToggleSwitch.js | 8 +- .../views/elements/LanguageDropdown.js | 6 +- src/components/views/elements/Pill.js | 24 ++--- .../views/elements/PowerSelector.js | 15 ++- .../views/elements/RoomAliasField.js | 23 ++--- src/components/views/elements/TintableSvg.js | 14 +-- .../views/groups/GroupMemberList.js | 12 ++- .../views/groups/GroupPublicityToggle.js | 6 +- src/components/views/groups/GroupRoomList.js | 13 ++- src/components/views/messages/MImageBody.js | 28 +++--- .../messages/MKeyVerificationConclusion.js | 4 +- src/components/views/messages/TextualBody.js | 11 ++- .../views/room_settings/AliasSettings.js | 17 ++-- .../room_settings/RoomProfileSettings.js | 34 +++++-- .../views/room_settings/UrlPreviewSettings.js | 14 +-- src/components/views/rooms/EntityTile.js | 7 +- .../views/rooms/LinkPreviewWidget.js | 4 +- src/components/views/rooms/PinnedEventTile.js | 13 ++- .../views/rooms/PinnedEventsPanel.js | 20 ++-- .../views/rooms/ReadReceiptMarker.js | 2 +- src/components/views/rooms/ReplyPreview.js | 9 +- src/components/views/rooms/RoomDetailRow.js | 8 +- src/components/views/rooms/Stickerpicker.js | 38 +++---- src/components/views/settings/ChangeAvatar.js | 2 +- .../views/settings/ChangePassword.js | 4 +- .../views/settings/CrossSigningPanel.js | 2 +- src/components/views/settings/DevicesPanel.js | 2 +- .../views/settings/Notifications.js | 34 +++---- .../views/settings/ProfileSettings.js | 8 +- .../views/settings/account/EmailAddresses.js | 39 +++++--- .../views/settings/account/PhoneNumbers.js | 21 ++-- .../tabs/room/GeneralRoomSettingsTab.js | 12 ++- .../tabs/room/NotificationSettingsTab.js | 6 +- .../tabs/user/GeneralUserSettingsTab.js | 13 ++- .../tabs/user/SecurityUserSettingsTab.js | 13 ++- .../tabs/user/VoiceUserSettingsTab.js | 12 +-- .../verification/VerificationCancelled.js | 16 +-- src/indexing/EventIndex.js | 29 ++++-- src/stores/CustomRoomTagStore.js | 4 +- src/stores/GroupFilterOrderStore.js | 8 +- src/utils/MegolmExportEncryption.js | 2 +- test/autocomplete/QueryMatcher-test.js | 2 +- .../dialogs/AccessSecretStorageDialog-test.js | 24 ++--- .../elements/MemberEventListSummary-test.js | 99 ++++++++++--------- .../components/views/rooms/MemberList-test.js | 2 +- test/components/views/rooms/RoomList-test.js | 5 +- test/end-to-end-tests/src/session.js | 8 +- test/utils/MegolmExportEncryption-test.js | 30 +++--- test/utils/ShieldUtils-test.js | 4 +- 87 files changed, 693 insertions(+), 537 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 99695b7a03..4959b133a0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,6 @@ module.exports = { "prefer-promise-reject-errors": "off", "no-async-promise-executor": "off", "quotes": "off", - "indent": "off", }, overrides: [{ diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index e7ae3217bb..58b65769ae 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -149,12 +149,12 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { Modal.createTrackedDialog( 'Failed to add the following room to the group', '', ErrorDialog, - { + { title: _t( "Failed to add the following rooms to %(groupId)s:", {groupId}, ), description: errorList.join(", "), - }); + }); }); } diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 1687adf13b..9239c1bc75 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -163,7 +163,7 @@ export default class IdentityAuthClient {
    ), button: _t("Trust"), - }); + }); const [confirmed] = await finished; if (confirmed) { // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 6fe6ca82cc..88ae00d088 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -54,7 +54,7 @@ export default class PasswordReset { return res; }, function(err) { if (err.errcode === 'M_THREEPID_NOT_FOUND') { - err.message = _t('This email address was not found'); + err.message = _t('This email address was not found'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } diff --git a/src/TextForEvent.js b/src/TextForEvent.js index a6787c647d..fd7b4fd18f 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -549,15 +549,15 @@ function textForMjolnirEvent(event) { if (USER_RULE_TYPES.includes(event.getType())) { return _t("%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}); } else if (ROOM_RULE_TYPES.includes(event.getType())) { return _t("%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}); } else if (SERVER_RULE_TYPES.includes(event.getType())) { return _t("%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}); } // Unknown type. We'll say something but we shouldn't end up here. diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js index 863ee2b427..ab4f605c83 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js @@ -498,9 +498,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent { title={this._titleForPhase(this.state.phase)} hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)} > -
    - {content} -
    +
    + {content} +
    ); } diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index 84cb58536a..7b7d552c43 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -856,9 +856,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} fixedWidth={false} > -
    - {content} -
    +
    + {content} +
    ); } diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js index eeb68b94bd..60f2ca9168 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js @@ -170,8 +170,11 @@ export default class ExportE2eKeysDialog extends React.Component {
    -
    -
    - -
    -
    - -
    +
    + +
    +
    + +
    -
    - -
    -
    - -
    +
    + +
    +
    + +
    diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 32db5c251c..d5e4b092e2 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -200,10 +200,10 @@ class FilePanel extends React.Component { previousPhase={RightPanelPhases.RoomSummary} >
    - { _t("You must register to use this functionality", - {}, - { 'a': (sub) => { sub } }) - } + { _t("You must register to use this functionality", + {}, + { 'a': (sub) => { sub } }) + }
    ; } else if (this.noRoom) { diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js index 976b2d81a5..7c050e7433 100644 --- a/src/components/structures/GroupFilterPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -153,17 +153,17 @@ class GroupFilterPanel extends React.Component { type="draggable-TagTile" > { (provided, snapshot) => ( -
    - { this.renderGlobalIcon() } - { tags } -
    - {createButton} -
    - { provided.placeholder } +
    + { this.renderGlobalIcon() } + { tags } +
    + {createButton}
    + { provided.placeholder } +
    ) } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index ed6167cbe7..ef74499473 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -43,7 +43,7 @@ import {mediaFromMxc} from "../../customisations/Media"; import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( -`

    HTML for your community's page

    + `

    HTML for your community's page

    Use the long description to introduce new members to the community, or distribute some important links @@ -111,13 +111,14 @@ class CategoryRoomList extends React.Component { Modal.createTrackedDialog( 'Failed to add the following room to the group summary', '', ErrorDialog, - { + { title: _t( "Failed to add the following rooms to the summary of %(groupId)s:", {groupId: this.props.groupId}, ), description: errorList.join(", "), - }); + }, + ); }); }, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -145,9 +146,10 @@ class CategoryRoomList extends React.Component { let catHeader =

    ; if (this.props.category && this.props.category.profile) { - catHeader =
    - { this.props.category.profile.name } -
    ; + catHeader =
    + { this.props.category.profile.name } +
    ; } return
    { catHeader } @@ -190,13 +192,14 @@ class FeaturedRoom extends React.Component { Modal.createTrackedDialog( 'Failed to remove room from group summary', '', ErrorDialog, - { + { title: _t( "Failed to remove the room from the summary of %(groupId)s", {groupId: this.props.groupId}, ), description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}), - }); + }, + ); }); }; @@ -283,13 +286,13 @@ class RoleUserList extends React.Component { Modal.createTrackedDialog( 'Failed to add the following users to the community summary', '', ErrorDialog, - { + { title: _t( "Failed to add the following users to the summary of %(groupId)s:", {groupId: this.props.groupId}, ), description: errorList.join(", "), - }); + }); }); }, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -299,11 +302,11 @@ class RoleUserList extends React.Component { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const addButton = this.props.editing ? ( - -
    - { _t('Add a User') } -
    -
    ) :
    ; + +
    + { _t('Add a User') } +
    + ) :
    ; const userNodes = this.props.users.map((u) => { return - { _t("Leave %(groupName)s?", {groupName: this.props.groupId}) } - { warnings } + { _t("Leave %(groupName)s?", {groupName: this.props.groupId}) } + { warnings } ), button: _t("Leave"), @@ -1059,7 +1063,7 @@ export default class GroupView extends React.Component { 'mx_RoomHeader_textButton', 'mx_GroupView_textButton', ], - membershipButtonExtraClasses, + membershipButtonExtraClasses, ); const membershipContainerClasses = classnames( diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 132d9ab4c3..c93f07fa0f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -427,8 +427,10 @@ export default class MessagePanel extends React.Component { // we get a new DOM node (restarting the animation) when the ghost // moves to a different event. return ( -
  • +
  • { hr }
  • ); @@ -1014,13 +1016,13 @@ class CreationGrouper { ret.push( - { eventTiles } + { eventTiles } , ); @@ -1222,11 +1224,11 @@ class MemberGrouper { ret.push( - { eventTiles } + { eventTiles } , ); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index ab4f524faf..ed8efab3b5 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -196,20 +196,22 @@ export default class RoomStatusBar extends React.Component { } else if (resourceLimitError) { title = messageForResourceLimitError( resourceLimitError.data.limit_type, - resourceLimitError.data.admin_contact, { - 'monthly_active_user': _td( - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + - "Please contact your service administrator to continue using the service.", - ), - 'hs_disabled': _td( - "Your message wasn't sent because this homeserver has been blocked by it's administrator. " + - "Please contact your service administrator to continue using the service.", - ), - '': _td( - "Your message wasn't sent because this homeserver has exceeded a resource limit. " + - "Please contact your service administrator to continue using the service.", - ), - }); + resourceLimitError.data.admin_contact, + { + 'monthly_active_user': _td( + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + + "Please contact your service administrator to continue using the service.", + ), + 'hs_disabled': _td( + "Your message wasn't sent because this homeserver has been blocked by it's administrator. " + + "Please contact your service administrator to continue using the service.", + ), + '': _td( + "Your message wasn't sent because this homeserver has exceeded a resource limit. " + + "Please contact your service administrator to continue using the service.", + ), + }, + ); } else { title = _t('Some of your messages have not been sent'); } @@ -261,7 +263,7 @@ export default class RoomStatusBar extends React.Component {
    /!\ + height="24" title="/!\ " alt="/!\ " />
    {_t('Connectivity to the server has been lost.')} diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 976734680c..93a4c29b81 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -884,16 +884,19 @@ export default class ScrollPanel extends React.Component { // give the
      an explicit role=list because Safari+VoiceOver seems to think an ordered-list with // list-style-type: none; is no longer a list - return ( - { this.props.fixedChildren } -
      -
        - { this.props.children } -
      -
      -
      - ); + return ( + { this.props.fixedChildren } +
      +
        + { this.props.children } +
      +
      +
      + ); } } diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 12f5d6e890..bf7794f0b0 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -785,8 +785,10 @@ class TimelinePanel extends React.Component { return; } const lastDisplayedEvent = this.state.events[lastDisplayedIndex]; - this._setReadMarker(lastDisplayedEvent.getId(), - lastDisplayedEvent.getTs()); + this._setReadMarker( + lastDisplayedEvent.getId(), + lastDisplayedEvent.getTs(), + ); // the read-marker should become invisible, so that if the user scrolls // down, they don't see it. @@ -872,7 +874,7 @@ class TimelinePanel extends React.Component { // The messagepanel knows where the RM is, so we must have loaded // the relevant event. this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId, - 0, 1/3); + 0, 1/3); return; } @@ -1044,7 +1046,7 @@ class TimelinePanel extends React.Component { } if (eventId) { this._messagePanel.current.scrollToEvent(eventId, pixelOffset, - offsetBase); + offsetBase); } else { this._messagePanel.current.scrollToBottom(); } @@ -1418,8 +1420,8 @@ class TimelinePanel extends React.Component { ['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState) ); const events = this.state.firstVisibleEventIndex - ? this.state.events.slice(this.state.firstVisibleEventIndex) - : this.state.events; + ? this.state.events.slice(this.state.firstVisibleEventIndex) + : this.state.events; return ( - { errorSection } + { errorSection }
    ); } @@ -375,7 +375,7 @@ export class TermsAuthEntry extends React.Component { if (this.props.showContinue !== false) { // XXX: button classes submitButton = ; + onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}; } return ( diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 142b8c80a8..35efd12c9c 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -350,7 +350,7 @@ export default class MessageContextMenu extends React.Component { > { _t('Source URL') } - ); + ); } if (this.props.collapseReplyThread) { diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js index 8948c14c7c..cbe0130649 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.js @@ -184,7 +184,7 @@ export default class BugReportDialog extends React.Component { return (
    diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js index 50bc13cff5..efbeba3977 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.js @@ -95,7 +95,7 @@ export default class ChangelogDialog extends React.Component { description={content} button={_t("Update")} onFinished={this.props.onFinished} - /> + /> ); } } diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js index 4faaad0f7e..333e1522f1 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js @@ -39,9 +39,12 @@ export default class ConfirmWipeDeviceDialog extends React.Component { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( - +

    {_t( diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 9f5513e0a3..8a035263cc 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -70,8 +70,16 @@ class GenericEditor extends React.PureComponent { } textInput(id, label) { - return ; + return ; } } @@ -155,7 +163,7 @@ export class SendCustomEvent extends GenericEditor {
    + autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />

    @@ -239,7 +247,7 @@ class SendAccountData extends GenericEditor {
    + autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
    @@ -315,15 +323,15 @@ class FilteredList extends React.PureComponent { const TruncatedList = sdk.getComponent("elements.TruncatedList"); return
    + type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery} + className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" + // force re-render so that autoFocus is applied when this component is re-used + key={this.props.children[0] ? this.props.children[0].key : ''} /> + getChildCount={this.getChildCount} + truncateAt={this.state.truncateAt} + createOverflowElement={this.createOverflowElement} />
    ; } } @@ -647,7 +655,7 @@ function VerificationRequest({txnId, request}) { /* Note that request.timeout is a getter, so its value changes */ const id = setInterval(() => { - setRequestTimeout(request.timeout); + setRequestTimeout(request.timeout); }, 500); return () => { clearInterval(id); }; @@ -941,35 +949,35 @@ class SettingsExplorer extends React.Component { /> - - - - - + + + + + - {allSettings.map(i => ( - - + - - - - ))} + + + + + + ))}
    {_t("Setting ID")}{_t("Value")}{_t("Value in this room")}
    {_t("Setting ID")}{_t("Value")}{_t("Value in this room")}
    - this.onViewClick(e, i)}> - {i} - - this.onEditClick(e, i)} - className='mx_DevTools_SettingsExplorer_edit' - > + {allSettings.map(i => ( +
    + this.onViewClick(e, i)}> + {i} + + this.onEditClick(e, i)} + className='mx_DevTools_SettingsExplorer_edit' + > ✏ - - - {this.renderSettingValue(SettingsStore.getValue(i))} - - - {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} - -
    + {this.renderSettingValue(SettingsStore.getValue(i))} + + + {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} + +
    @@ -998,11 +1006,11 @@ class SettingsExplorer extends React.Component {
    - - - - - + + + + + {LEVEL_ORDER.map(lvl => ( diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.js b/src/components/views/dialogs/IntegrationsDisabledDialog.js index 0e9878f4bc..dd7a51420e 100644 --- a/src/components/views/dialogs/IntegrationsDisabledDialog.js +++ b/src/components/views/dialogs/IntegrationsDisabledDialog.js @@ -42,9 +42,12 @@ export default class IntegrationsDisabledDialog extends React.Component { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( - +

    {_t("Enable 'Manage Integrations' in Settings to do this.")}

    diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js index 9bc9d02ba6..e14d40aaef 100644 --- a/src/components/views/dialogs/IntegrationsImpossibleDialog.js +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js @@ -37,9 +37,12 @@ export default class IntegrationsImpossibleDialog extends React.Component { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( - +

    {_t( diff --git a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js index 25eb7a90d2..bcb4d4f9b9 100644 --- a/src/components/views/dialogs/KeySignatureUploadFailedDialog.js +++ b/src/components/views/dialogs/KeySignatureUploadFailedDialog.js @@ -24,7 +24,7 @@ export default function KeySignatureUploadFailedDialog({ source, continuation, onFinished, - }) { +}) { const RETRIES = 2; const BaseDialog = sdk.getComponent('dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -84,10 +84,10 @@ export default function KeySignatureUploadFailedDialog({ } else { body = (

    {success ? - {_t("Upload completed")} : - cancelled ? - {_t("Cancelled signature upload")} : - {_t("Unable to upload")}} + {_t("Upload completed")} : + cancelled ? + {_t("Cancelled signature upload")} : + {_t("Unable to upload")}} + {content} ); diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 9c2f23ef22..c052b5c5bb 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -117,7 +117,7 @@ export default class RoomSettingsDialog extends React.Component { const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name; return ( + onFinished={this.props.onFinished} title={_t("Room Settings - %(roomName)s", {roomName})}>
    diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index 50d7fbea09..43e73a2f83 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -98,7 +98,7 @@ export default class SessionRestoreErrorDialog extends React.Component { "may be incompatible with this version. Close this window and return " + "to the more recent version.", { brand }, - ) }

    + ) }

    { _t( "Clearing your browser's storage may fix the problem, but will sign you " + diff --git a/src/components/views/dialogs/StorageEvictedDialog.js b/src/components/views/dialogs/StorageEvictedDialog.js index 15c5347644..629990032f 100644 --- a/src/components/views/dialogs/StorageEvictedDialog.js +++ b/src/components/views/dialogs/StorageEvictedDialog.js @@ -46,9 +46,10 @@ export default class StorageEvictedDialog extends React.Component { if (SdkConfig.get().bug_report_endpoint_url) { logRequest = _t( "To help us prevent this in future, please send us logs.", {}, - { + { a: text => {text}, - }); + }, + ); } return ( diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index eb9eaeb5dd..8d99ffb5cd 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -156,7 +156,7 @@ export default class UserSettingsDialog extends React.Component { return ( + onFinished={this.props.onFinished} title={_t("Settings")}>

    diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.js index 205597a1c4..9281275e6a 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.js +++ b/src/components/views/dialogs/VerificationRequestDialog.js @@ -52,11 +52,13 @@ export default class VerificationRequestDialog extends React.Component { const title = request && request.isSelfVerification ? _t("Verify other login") : _t("Verification Request"); - return + return + onFinished={this.props.onFinished} + title={_t("Allow this widget to verify your identity")}>

    {_t("The widget will verify your user ID, but won't be able to perform actions for you:")} diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js index 43fb25f152..dabd7950b4 100644 --- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js +++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js @@ -40,10 +40,10 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component { return ( + className='mx_ConfirmDestroyCrossSigningDialog' + hasCancel={true} + onFinished={this.props.onFinished} + title={_t("Destroy cross-signing keys?")}>

    {_t( diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js index 1fafe03d95..faabbacb81 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js @@ -374,7 +374,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { "If you've forgotten your Security Phrase you can "+ "use your Security Key or " + "set up new recovery options" - , {}, { + , {}, { button1: s => {s} , - })} + })}

    ; } else { title = _t("Enter Security Key"); @@ -436,14 +436,14 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { {_t( "If you've forgotten your Security Key you can "+ "" - , {}, { + , {}, { button: s => {s} , - })} + })}
    ; } @@ -452,9 +452,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { onFinished={this.props.onFinished} title={title} > -
    - {content} -
    +
    + {content} +
    ); } diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index 1714891cb5..9903c631b2 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -70,8 +70,8 @@ export default class ActionButton extends React.Component { } const icon = this.props.iconPath ? - () : - undefined; + () : + undefined; const classNames = ["mx_RoleButton"]; if (this.props.className) { diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index e206fda797..b898ad2ebc 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -109,7 +109,7 @@ export default class AppTile extends React.Component { const childContentProtocol = u.protocol; if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') { console.warn("Refusing to load mixed-content app:", - parentContentProtocol, childContentProtocol, window.location, this.props.app.url); + parentContentProtocol, childContentProtocol, window.location, this.props.app.url); return true; } return false; diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js index ff62f169fa..e3fe54333a 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.js @@ -65,12 +65,18 @@ export class EditableItem extends React.Component { {_t("Are you sure?")} - + {_t("Yes")} - + {_t("No")}
    @@ -122,10 +128,10 @@ export default class EditableItemList extends React.Component { _renderNewItemField() { return (
    + noValidate={true} className="mx_EditableItemList_newItem"> + autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} + list={this.props.suggestionsListId} /> {_t("Add")} diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 638fd02553..7c38ac1777 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -221,13 +221,15 @@ export default class EditableText extends React.Component {
    ; } else { // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together - editableEl =
    ; + editableEl =
    ; } return editableEl; diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.js index e6378f0e6a..ef60eeed7b 100644 --- a/src/components/views/elements/LabelledToggleSwitch.js +++ b/src/components/views/elements/LabelledToggleSwitch.js @@ -46,8 +46,12 @@ export default class LabelledToggleSwitch extends React.Component { // This is a minimal version of a SettingsFlag let firstPart = {this.props.label}; - let secondPart = ; + let secondPart = ; if (this.props.toggleInFront) { const temp = firstPart; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 2e961be700..b8734c5afb 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -60,10 +60,10 @@ export default class LanguageDropdown extends React.Component { // doesn't know this, therefore we do this. const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); if (language) { - this.props.onOptionChange(language); + this.props.onOptionChange(language); } else { - const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); - this.props.onOptionChange(language); + const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); + this.props.onOptionChange(language); } } } diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index a8e16813e6..ace41db39d 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -225,19 +225,19 @@ class Pill extends React.Component { } break; case Pill.TYPE_USER_MENTION: { - // If this user is not a member of this room, default to the empty member - const member = this.state.member; - if (member) { - userId = member.userId; - member.rawDisplayName = member.rawDisplayName || ''; - linkText = member.rawDisplayName; - if (this.props.shouldShowPillAvatar) { - avatar =
    -
    {_t("Level")}{_t("Settable at global")}{_t("Settable at room")}
    {_t("Level")}{_t("Settable at global")}{_t("Settable at room")}
    {_t("Homeserver feature support:")} {homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}
    + {errorSection} {actionRow} diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index e7d300b0f8..b1ad605a37 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -214,7 +214,7 @@ export default class DevicesPanel extends React.Component { const deleteButton = this.state.deleting ? : - { _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length}) } + { _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length})} ; const classes = classNames(this.props.className, "mx_DevicesPanel"); diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 25fe434994..5756536085 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -100,7 +100,7 @@ export default class Notifications extends React.Component { MatrixClientPeg.get().setPushRuleEnabled( 'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked, ).then(function() { - self._refreshFromServer(); + self._refreshFromServer(); }); }; @@ -580,12 +580,12 @@ export default class Notifications extends React.Component { "vectorRuleId": "_keywords", "description": ( - { _t('Messages containing keywords', - {}, - { 'span': (sub) => - {sub}, - }, - )} + { _t('Messages containing keywords', + {}, + { 'span': (sub) => + {sub}, + }, + )} ), "vectorState": self.state.vectorContentRules.vectorState, @@ -743,8 +743,8 @@ export default class Notifications extends React.Component { emailNotificationsRow(address, label) { return ; + onChange={this.onEnableEmailNotificationsChange.bind(this, address)} + label={label} key={`emailNotif_${label}`} />; } render() { @@ -757,8 +757,8 @@ export default class Notifications extends React.Component { let masterPushRuleDiv; if (this.state.masterPushRule) { masterPushRuleDiv = ; + onChange={this.onEnableNotificationsChange} + label={_t('Enable notifications for this account')} />; } let clearNotificationsButton; @@ -874,16 +874,16 @@ export default class Notifications extends React.Component { { spinner } + onChange={this.onEnableDesktopNotificationsChange} + label={_t('Enable desktop notifications for this session')} /> + onChange={this.onEnableDesktopNotificationBodyChange} + label={_t('Show message in desktop notification')} /> + onChange={this.onEnableAudioNotificationsChange} + label={_t('Enable audible notifications for this session')} /> { emailNotificationsRows } diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js index 971b868751..9ecf369eba 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.js @@ -170,8 +170,12 @@ export default class ProfileSettings extends React.Component { noValidate={true} className="mx_ProfileSettings_profileForm" > - +
    {_t("Profile")} diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js index 1ebd374173..a36369cf88 100644 --- a/src/components/views/settings/account/EmailAddresses.js +++ b/src/components/views/settings/account/EmailAddresses.js @@ -90,12 +90,18 @@ export class ExistingEmailAddress extends React.Component { {_t("Remove %(email)s?", {email: this.props.email.address} )} - + {_t("Remove")} - + {_t("Cancel")}
    @@ -228,21 +234,28 @@ export default class EmailAddresses extends React.Component { ); if (this.state.verifying) { addButton = ( -
    -
    {_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}
    - - {_t("Continue")} - -
    +
    +
    {_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}
    + + {_t("Continue")} + +
    ); } return (
    {existingEmailElements} - + {_t("Remove %(phone)s?", {phone: this.props.msisdn.address})} - + {_t("Remove")} - + {_t("Cancel")}
    @@ -246,8 +252,11 @@ export default class PhoneNumbers extends React.Component { value={this.state.newPhoneNumberCode} onChange={this._onChangeNewPhoneNumberCode} /> - + {_t("Continue")} diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js index cd4a043622..139cfd5fbd 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js @@ -80,9 +80,11 @@ export default class GeneralRoomSettingsTab extends React.Component { flairSection = <> {_t("Flair")}
    - +
    ; } @@ -97,8 +99,8 @@ export default class GeneralRoomSettingsTab extends React.Component {
    {_t("Room Addresses")}
    + canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases} + canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
    {_t("Other")}
    { flairSection } diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index baefb5ae20..fa56fa2cb6 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -155,7 +155,7 @@ export default class NotificationsSettingsTab extends React.Component {
    {_t("Notification sound")}: {this.state.currentSound}
    - {_t("Reset")} + {_t("Reset")}
    @@ -167,11 +167,11 @@ export default class NotificationsSettingsTab extends React.Component { {currentUploadedFile} - {_t("Browse")} + {_t("Browse")} - {_t("Save")} + {_t("Save")}
    diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index b1ad9f3d23..fe2c9ad0fa 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -319,8 +319,11 @@ export default class GeneralUserSettingsTab extends React.Component { return (
    {_t("Language and region")} - +
    ); } @@ -329,8 +332,10 @@ export default class GeneralUserSettingsTab extends React.Component { return (
    {_t("Spell check dictionaries")} - +
    ); } diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 8a70811399..9e1e9b03a9 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -257,13 +257,12 @@ export default class SecurityUserSettingsTab extends React.Component { if (!ignoredUserIds || ignoredUserIds.length === 0) return null; - const userIds = ignoredUserIds - .map((u) => ); + const userIds = ignoredUserIds.map((u) => ); return (
    diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index d8adab55f6..362059f8ed 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -176,8 +176,8 @@ export default class VoiceUserSettingsTab extends React.Component { const defaultDevice = getDefaultDevice(audioOutputs); speakerDropdown = ( + value={this.state.activeAudioOutput || defaultDevice} + onChange={this._setAudioOutput}> {this._renderDeviceOptions(audioOutputs, 'audioOutput')} ); @@ -188,8 +188,8 @@ export default class VoiceUserSettingsTab extends React.Component { const defaultDevice = getDefaultDevice(audioInputs); microphoneDropdown = ( + value={this.state.activeAudioInput || defaultDevice} + onChange={this._setAudioInput}> {this._renderDeviceOptions(audioInputs, 'audioInput')} ); @@ -200,8 +200,8 @@ export default class VoiceUserSettingsTab extends React.Component { const defaultDevice = getDefaultDevice(videoInputs); webcamDropdown = ( + value={this.state.activeVideoInput || defaultDevice} + onChange={this._setVideoInput}> {this._renderDeviceOptions(videoInputs, 'videoInput')} ); diff --git a/src/components/views/verification/VerificationCancelled.js b/src/components/views/verification/VerificationCancelled.js index 0bbaea1804..c57094d9b5 100644 --- a/src/components/views/verification/VerificationCancelled.js +++ b/src/components/views/verification/VerificationCancelled.js @@ -29,14 +29,14 @@ export default class VerificationCancelled extends React.Component { render() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
    -

    {_t( - "The other party cancelled the verification.", - )}

    - +

    {_t( + "The other party cancelled the verification.", + )}

    +
    ; } } diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 2dcdb9e3a3..2584ee9172 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -127,8 +127,13 @@ export default class EventIndex extends EventEmitter { this.crawlerCheckpoints.push(forwardCheckpoint); } } catch (e) { - console.log("EventIndex: Error adding initial checkpoints for room", - room.roomId, backCheckpoint, forwardCheckpoint, e); + console.log( + "EventIndex: Error adding initial checkpoints for room", + room.roomId, + backCheckpoint, + forwardCheckpoint, + e, + ); } })); } @@ -379,8 +384,12 @@ export default class EventIndex extends EventEmitter { try { await indexManager.addCrawlerCheckpoint(checkpoint); } catch (e) { - console.log("EventIndex: Error adding new checkpoint for room", - room.roomId, checkpoint, e); + console.log( + "EventIndex: Error adding new checkpoint for room", + room.roomId, + checkpoint, + e, + ); } this.crawlerCheckpoints.push(checkpoint); @@ -459,7 +468,7 @@ export default class EventIndex extends EventEmitter { } catch (e) { if (e.httpStatus === 403) { console.log("EventIndex: Removing checkpoint as we don't have ", - "permissions to fetch messages from this room.", checkpoint); + "permissions to fetch messages from this room.", checkpoint); try { await indexManager.removeCrawlerCheckpoint(checkpoint); } catch (e) { @@ -589,7 +598,7 @@ export default class EventIndex extends EventEmitter { // to do here anymore. if (!newCheckpoint) { console.log("EventIndex: The server didn't return a valid ", - "new checkpoint, not continuing the crawl.", checkpoint); + "new checkpoint, not continuing the crawl.", checkpoint); continue; } @@ -599,12 +608,12 @@ export default class EventIndex extends EventEmitter { // the new checkpoint to be used by the crawler. if (eventsAlreadyAdded === true && newCheckpoint.fullCrawl !== true) { console.log("EventIndex: Checkpoint had already all events", - "added, stopping the crawl", checkpoint); + "added, stopping the crawl", checkpoint); await indexManager.removeCrawlerCheckpoint(newCheckpoint); } else { if (eventsAlreadyAdded === true) { console.log("EventIndex: Checkpoint had already all events", - "added, but continuing due to a full crawl", checkpoint); + "added, but continuing due to a full crawl", checkpoint); } this.crawlerCheckpoints.push(newCheckpoint); } @@ -777,7 +786,7 @@ export default class EventIndex extends EventEmitter { * timeline, false otherwise. */ async populateFileTimeline(timelineSet, timeline, room, limit = 10, - fromEvent = null, direction = EventTimeline.BACKWARDS) { + fromEvent = null, direction = EventTimeline.BACKWARDS) { const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction); // If this is a normal fill request, not a pagination request, we need @@ -807,7 +816,7 @@ export default class EventIndex extends EventEmitter { } console.log("EventIndex: Populating file panel with", matrixEvents.length, - "events and setting the pagination token to", paginationToken); + "events and setting the pagination token to", paginationToken); timeline.setPaginationToken(paginationToken, EventTimeline.BACKWARDS); return ret; diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index edfc0003cf..060f1f3749 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -125,14 +125,14 @@ class CustomRoomTagStore extends EventEmitter { this._setState({tags}); } } - break; + break; case 'on_client_not_viable': case 'on_logged_out': { // we assume to always have a tags object in the state this._state = {tags: {}}; RoomListStore.instance.off(LISTS_UPDATE_EVENT, this._onListsUpdated); } - break; + break; } } diff --git a/src/stores/GroupFilterOrderStore.js b/src/stores/GroupFilterOrderStore.js index 492322146e..b18abaa001 100644 --- a/src/stores/GroupFilterOrderStore.js +++ b/src/stores/GroupFilterOrderStore.js @@ -168,7 +168,7 @@ class GroupFilterOrderStore extends Store { Analytics.trackEvent('FilterStore', 'select_tag'); } - break; + break; case 'deselect_tags': if (payload.tag) { // if a tag is passed, only deselect that tag @@ -181,7 +181,7 @@ class GroupFilterOrderStore extends Store { }); } Analytics.trackEvent('FilterStore', 'deselect_tags'); - break; + break; case 'on_client_not_viable': case 'on_logged_out': { // Reset state without pushing an update to the view, which generally assumes that @@ -207,8 +207,8 @@ class GroupFilterOrderStore extends Store { groupIds.forEach(groupId => { const rooms = GroupStore.getGroupRooms(groupId) - .map(r => client.getRoom(r.roomId)) // to Room objects - .filter(r => r !== null && r !== undefined); // filter out rooms we haven't joined from the group + .map(r => client.getRoom(r.roomId)) // to Room objects + .filter(r => r !== null && r !== undefined); // filter out rooms we haven't joined from the group const badge = rooms && RoomNotifs.aggregateNotificationCount(rooms); changedBadges[groupId] = (badge && badge.count !== 0) ? badge : undefined; }); diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index be7472901a..20f3cd6cb6 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -311,7 +311,7 @@ function unpackMegolmKeyFile(data) { while (1) { const lineEnd = fileStr.indexOf('\n', lineStart); const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd) - .trim(); + .trim(); if (line === TRAILER_LINE) { break; } diff --git a/test/autocomplete/QueryMatcher-test.js b/test/autocomplete/QueryMatcher-test.js index 3d383f08d7..cae71841d4 100644 --- a/test/autocomplete/QueryMatcher-test.js +++ b/test/autocomplete/QueryMatcher-test.js @@ -177,7 +177,7 @@ describe('QueryMatcher', function() { const qm = new QueryMatcher(NONWORDOBJECTS, { keys: ["name"], shouldMatchWordsOnly: false, - }); + }); const results = qm.match('bob'); expect(results.length).toBe(1); diff --git a/test/components/views/dialogs/AccessSecretStorageDialog-test.js b/test/components/views/dialogs/AccessSecretStorageDialog-test.js index 13b39ab0d0..d9e07a2d74 100644 --- a/test/components/views/dialogs/AccessSecretStorageDialog-test.js +++ b/test/components/views/dialogs/AccessSecretStorageDialog-test.js @@ -26,9 +26,9 @@ describe("AccessSecretStorageDialog", function() { it("Closes the dialog if _onRecoveryKeyNext is called with a valid key", (done) => { const testInstance = TestRenderer.create( p && p.recoveryKey && p.recoveryKey == "a"} - onFinished={(v) => { - if (v) { done(); } + checkPrivateKey={(p) => p && p.recoveryKey && p.recoveryKey == "a"} + onFinished={(v) => { + if (v) { done(); } }} />, ); @@ -43,7 +43,7 @@ describe("AccessSecretStorageDialog", function() { it("Considers a valid key to be valid", async function() { const testInstance = TestRenderer.create( true} + checkPrivateKey={() => true} />, ); const v = "asdf"; @@ -61,7 +61,7 @@ describe("AccessSecretStorageDialog", function() { it("Notifies the user if they input an invalid Security Key", async function(done) { const testInstance = TestRenderer.create( false} + checkPrivateKey={async () => false} />, ); const e = { target: { value: "a" } }; @@ -87,12 +87,14 @@ describe("AccessSecretStorageDialog", function() { it("Notifies the user if they input an invalid passphrase", async function(done) { const testInstance = TestRenderer.create( false} - onFinished={() => {}} - keyInfo={ { passphrase: { - salt: 'nonempty', - iterations: 2, - } } } + checkPrivateKey={() => false} + onFinished={() => {}} + keyInfo={{ + passphrase: { + salt: 'nonempty', + iterations: 2, + }, + }} />, ); const e = { target: { value: "a" } }; diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index dd6febc7d7..9386d8cf4a 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -246,8 +246,8 @@ describe('MemberEventListSummary', function() { }); it('truncates multiple sequences of repetitions with other events between', - function() { - const events = generateEvents([ + function() { + const events = generateEvents([ { userId: "@user_1:some.domain", prevMembership: "ban", @@ -276,28 +276,29 @@ describe('MemberEventListSummary', function() { membership: "invite", senderId: "@some_other_user:some.domain", }, - ]); - const props = { + ]); + const props = { events: events, children: generateTiles(events), summaryLength: 1, avatarsMaxLength: 5, threshold: 3, - }; + }; - const instance = ReactTestUtils.renderIntoDocument( - , - ); - const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_EventListSummary_summary", - ); - const summaryText = summary.textContent; + const instance = ReactTestUtils.renderIntoDocument( + , + ); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, "mx_EventListSummary_summary", + ); + const summaryText = summary.textContent; - expect(summaryText).toBe( - "user_1 was unbanned, joined and left 2 times, was banned, " + + expect(summaryText).toBe( + "user_1 was unbanned, joined and left 2 times, was banned, " + "joined and left 3 times and was invited", - ); - }); + ); + }, + ); it('handles multiple users following the same sequence of memberships', function() { const events = generateEvents([ @@ -396,8 +397,8 @@ describe('MemberEventListSummary', function() { }); it('correctly orders sequences of transitions by the order of their first event', - function() { - const events = generateEvents([ + function() { + const events = generateEvents([ { userId: "@user_2:some.domain", prevMembership: "ban", @@ -424,28 +425,29 @@ describe('MemberEventListSummary', function() { {userId: "@user_2:some.domain", prevMembership: "join", membership: "leave"}, {userId: "@user_2:some.domain", prevMembership: "leave", membership: "join"}, {userId: "@user_2:some.domain", prevMembership: "join", membership: "leave"}, - ]); - const props = { + ]); + const props = { events: events, children: generateTiles(events), summaryLength: 1, avatarsMaxLength: 5, threshold: 3, - }; + }; - const instance = ReactTestUtils.renderIntoDocument( - , - ); - const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_EventListSummary_summary", - ); - const summaryText = summary.textContent; + const instance = ReactTestUtils.renderIntoDocument( + , + ); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, "mx_EventListSummary_summary", + ); + const summaryText = summary.textContent; - expect(summaryText).toBe( - "user_2 was unbanned and joined and left 2 times, user_1 was unbanned, " + + expect(summaryText).toBe( + "user_2 was unbanned and joined and left 2 times, user_1 was unbanned, " + "joined and left 2 times and was banned", - ); - }); + ); + }, + ); it('correctly identifies transitions', function() { const events = generateEvents([ @@ -569,8 +571,8 @@ describe('MemberEventListSummary', function() { }); it('handles invitation plurals correctly when there are multiple invites', - function() { - const events = generateEvents([ + function() { + const events = generateEvents([ { userId: "@user_1:some.domain", prevMembership: "invite", @@ -581,27 +583,28 @@ describe('MemberEventListSummary', function() { prevMembership: "invite", membership: "leave", }, - ]); - const props = { + ]); + const props = { events: events, children: generateTiles(events), summaryLength: 1, avatarsMaxLength: 5, threshold: 1, // threshold = 1 to force collapse - }; + }; - const instance = ReactTestUtils.renderIntoDocument( - , - ); - const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_EventListSummary_summary", - ); - const summaryText = summary.textContent; + const instance = ReactTestUtils.renderIntoDocument( + , + ); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, "mx_EventListSummary_summary", + ); + const summaryText = summary.textContent; - expect(summaryText).toBe( - "user_1 rejected their invitation 2 times", - ); - }); + expect(summaryText).toBe( + "user_1 rejected their invitation 2 times", + ); + }, + ); it('handles a summary length = 2, with no "others"', function() { const events = generateEvents([ diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.js index 068d358dcd..093e5588d0 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.js @@ -100,7 +100,7 @@ describe('MemberList', () => { memberList = r; }; root = ReactDOM.render(, parentDiv); + wrappedRef={gatherWrappedRef} />, parentDiv); }); afterEach((done) => { diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index d3211f564c..bfb8e1afd4 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -70,8 +70,9 @@ describe('RoomList', () => { root = ReactDOM.render( {}} /> - - , parentDiv); + , + parentDiv, + ); ReactTestUtils.findRenderedComponentWithType(root, RoomList); movingRoom = createRoom({name: 'Moving room'}); diff --git a/test/end-to-end-tests/src/session.js b/test/end-to-end-tests/src/session.js index 433baa5e48..4c611ef877 100644 --- a/test/end-to-end-tests/src/session.js +++ b/test/end-to-end-tests/src/session.js @@ -93,10 +93,10 @@ module.exports = class ElementSession { const type = req.resourceType(); const response = await req.response(); //if (type === 'xhr' || type === 'fetch') { - buffer += `${type} ${response.status()} ${req.method()} ${req.url()} \n`; - // if (req.method() === "POST") { - // buffer += " Post data: " + req.postData(); - // } + buffer += `${type} ${response.status()} ${req.method()} ${req.url()} \n`; + // if (req.method() === "POST") { + // buffer += " Post data: " + req.postData(); + // } //} }); return { diff --git a/test/utils/MegolmExportEncryption-test.js b/test/utils/MegolmExportEncryption-test.js index e0ed5ba26a..07ec03860b 100644 --- a/test/utils/MegolmExportEncryption-test.js +++ b/test/utils/MegolmExportEncryption-test.js @@ -84,22 +84,22 @@ describe('MegolmExportEncryption', function() { it('should handle missing header', function() { const input=stringToArray(`-----`); return MegolmExportEncryption.decryptMegolmKeyFile(input, '') - .then((res) => { - throw new Error('expected to throw'); - }, (error) => { - expect(error.message).toEqual('Header line not found'); - }); + .then((res) => { + throw new Error('expected to throw'); + }, (error) => { + expect(error.message).toEqual('Header line not found'); + }); }); it('should handle missing trailer', function() { const input=stringToArray(`-----BEGIN MEGOLM SESSION DATA----- -----`); return MegolmExportEncryption.decryptMegolmKeyFile(input, '') - .then((res) => { - throw new Error('expected to throw'); - }, (error) => { - expect(error.message).toEqual('Trailer line not found'); - }); + .then((res) => { + throw new Error('expected to throw'); + }, (error) => { + expect(error.message).toEqual('Trailer line not found'); + }); }); it('should handle a too-short body', function() { @@ -109,11 +109,11 @@ cissyYBxjsfsAn -----END MEGOLM SESSION DATA----- `); return MegolmExportEncryption.decryptMegolmKeyFile(input, '') - .then((res) => { - throw new Error('expected to throw'); - }, (error) => { - expect(error.message).toEqual('Invalid file: too short'); - }); + .then((res) => { + throw new Error('expected to throw'); + }, (error) => { + expect(error.message).toEqual('Invalid file: too short'); + }); }); // TODO find a subtlecrypto shim which doesn't break this test diff --git a/test/utils/ShieldUtils-test.js b/test/utils/ShieldUtils-test.js index bea3d26565..fdf4f527ee 100644 --- a/test/utils/ShieldUtils-test.js +++ b/test/utils/ShieldUtils-test.js @@ -26,7 +26,7 @@ describe("mkClient self-test", function() { ["@TF:h", true], ["@FT:h", false], ["@FF:h", false]], - )("behaves well for user trust %s", (userId, trust) => { + )("behaves well for user trust %s", (userId, trust) => { expect(mkClient().checkUserTrust(userId).isCrossSigningVerified()).toBe(trust); }); @@ -35,7 +35,7 @@ describe("mkClient self-test", function() { ["@TF:h", false], ["@FT:h", true], ["@FF:h", false]], - )("behaves well for device trust %s", (userId, trust) => { + )("behaves well for device trust %s", (userId, trust) => { expect(mkClient().checkDeviceTrust(userId, "device").isVerified()).toBe(trust); }); }); From 35799c213e1e9d03bf33b1b484045bddd113f7ce Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 27 Apr 2021 16:30:54 +0100 Subject: [PATCH 296/330] Fix suggested rooms not showing up regression from room list optimisation --- src/components/views/rooms/RoomSublist.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index a726ab99fc..fb367349a2 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -763,7 +763,9 @@ export default class RoomSublist extends React.Component { 'mx_RoomSublist': true, 'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition, 'mx_RoomSublist_minimized': this.props.isMinimized, - 'mx_RoomSublist_hidden': !this.state.rooms.length && this.props.alwaysVisible !== true, + 'mx_RoomSublist_hidden': ( + !this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true + ), }); let content = null; From b6762c68afd8cec1e06c9544d046e9126c8bf6b4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 27 Apr 2021 18:55:53 +0100 Subject: [PATCH 297/330] typo Co-authored-by: J. Ryan Stinnett --- src/CallHandler.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 605e5a4a89..f2a2e71854 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -168,7 +168,7 @@ export default class CallHandler { private invitedRoomsAreVirtual = new Map(); private invitedRoomCheckInProgress = false; - // Map of the asserted identiy users after we've looked them up using the API. + // Map of the asserted identity users after we've looked them up using the API. // We need to be be able to determine the mapped room synchronously, so we // do the async lookup when we get new information and then store these mappings here private assertedIdentityNativeUsers = new Map(); From 705505fe85a85e2a14f725d5301ea3c78f19cfbb Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 27 Apr 2021 18:56:22 +0100 Subject: [PATCH 298/330] make copyright not lie --- test/CallHandler-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 04dec8979f..cb801b4936 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -1,5 +1,5 @@ /* -Copyright 2015-2021 The Matrix.org Foundation C.I.C. +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. From be7d4d020bd9713a92e17bf268fc0e382815763a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 27 Apr 2021 19:33:53 +0100 Subject: [PATCH 299/330] Put asserted identity option under a 'voip' section --- src/CallHandler.tsx | 4 +++- test/CallHandler-test.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index f2a2e71854..16bc837aa2 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -188,7 +188,9 @@ export default class CallHandler { public roomIdForCall(call: MatrixCall): string { if (!call) return null; - if (SdkConfig.get()['voipObeyAssertedIdentity']) { + const voipConfig = SdkConfig.get()['voip']; + + if (voipConfig && voipConfig.obeyAssertedIdentity) { const nativeUser = this.assertedIdentityNativeUsers[call.callId]; if (nativeUser) { const room = findDMForUser(MatrixClientPeg.get(), nativeUser); diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index cb801b4936..754610b223 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -189,7 +189,9 @@ describe('CallHandler', () => { // Now set the config option SdkConfig.put({ - voipObeyAssertedIdentity: true, + voip: { + obeyAssertedIdentity: true, + }, }); // ...and send another asserted identity event for a different user From 32e3ce3dea7c86b1098ac07bb5c9e430f847f34d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 20 Apr 2021 21:30:21 -0600 Subject: [PATCH 300/330] Handle basic state machine of recordings --- .../views/rooms/_VoiceRecordComposerTile.scss | 7 +- .../views/rooms/VoiceRecordComposerTile.tsx | 160 +++++++++++------- .../IRecordingWaveformStateProps.ts | 26 +++ .../voice_messages/LiveRecordingWaveform.tsx | 13 +- .../views/voice_messages/PlaybackWaveform.tsx | 43 +++++ src/i18n/strings/en_EN.json | 2 +- src/utils/arrays.ts | 20 +++ src/voice/VoiceRecording.ts | 11 ++ test/utils/arrays-test.ts | 33 ++++ 9 files changed, 241 insertions(+), 74 deletions(-) create mode 100644 src/components/views/voice_messages/IRecordingWaveformStateProps.ts create mode 100644 src/components/views/voice_messages/PlaybackWaveform.tsx diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index 8100a03890..dc4835c4dd 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -36,11 +36,12 @@ limitations under the License. } .mx_VoiceRecordComposerTile_waveformContainer { - padding: 5px; + padding: 8px; // makes us 4px taller than the send/stop button padding-right: 4px; // there's 1px from the waveform itself, so account for that padding-left: 15px; // +10px for the live circle, +5px for regular padding background-color: $voice-record-waveform-bg-color; border-radius: 12px; + margin: 6px; // force the composer area to put a gutter around us margin-right: 12px; // isolate from stop button // Cheat at alignment a bit @@ -52,7 +53,7 @@ limitations under the License. color: $voice-record-waveform-fg-color; font-size: $font-14px; - &::before { + &.mx_VoiceRecordComposerTile_recording::before { animation: recording-pulse 2s infinite; content: ''; @@ -61,7 +62,7 @@ limitations under the License. height: 10px; position: absolute; left: 8px; - top: 16px; // vertically center + top: 18px; // vertically center border-radius: 10px; } diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 9b7f0da472..3fcafafd05 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -17,7 +17,7 @@ limitations under the License. import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {_t} from "../../../languageHandler"; import React from "react"; -import {VoiceRecording} from "../../../voice/VoiceRecording"; +import {RecordingState, VoiceRecording} from "../../../voice/VoiceRecording"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import classNames from "classnames"; @@ -25,6 +25,8 @@ import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import PlaybackWaveform from "../voice_messages/PlaybackWaveform"; interface IProps { room: Room; @@ -32,6 +34,7 @@ interface IProps { interface IState { recorder?: VoiceRecording; + recordingPhase?: RecordingState; } /** @@ -43,87 +46,126 @@ export default class VoiceRecordComposerTile extends React.PureComponent { - // TODO: @@ TravisR: We do not want to auto-send on stop. + public async componentWillUnmount() { + await VoiceRecordingStore.instance.disposeRecording(); + } + + // called by composer + public async send() { + if (!this.state.recorder) { + throw new Error("No recording started - cannot send anything"); + } + + await this.state.recorder.stop(); + const mxc = await this.state.recorder.upload(); + MatrixClientPeg.get().sendMessage(this.props.room.roomId, { + "body": "Voice message", + "msgtype": "org.matrix.msc2516.voice", + //"msgtype": MsgType.Audio, + "url": mxc, + "info": { + duration: Math.round(this.state.recorder.durationSeconds * 1000), + mimetype: this.state.recorder.contentType, + size: this.state.recorder.contentLength, + }, + + // MSC1767 experiment + "org.matrix.msc1767.text": "Voice message", + "org.matrix.msc1767.file": { + url: mxc, + name: "Voice message.ogg", + mimetype: this.state.recorder.contentType, + size: this.state.recorder.contentLength, + }, + "org.matrix.msc1767.audio": { + duration: Math.round(this.state.recorder.durationSeconds * 1000), + // TODO: @@ TravisR: Waveform? (MSC1767 decision) + }, + "org.matrix.experimental.msc2516.voice": { // MSC2516+MSC1767 experiment + duration: Math.round(this.state.recorder.durationSeconds * 1000), + + // Events can't have floats, so we try to maintain resolution by using 1024 + // as a maximum value. The waveform contains values between zero and 1, so this + // should come out largely sane. + // + // We're expecting about one data point per second of audio. + waveform: this.state.recorder.finalWaveform.map(v => Math.round(v * 1024)), + }, + }); + await VoiceRecordingStore.instance.disposeRecording(); + this.setState({recorder: null}); + } + + private onRecordStartEndClick = async () => { if (this.state.recorder) { await this.state.recorder.stop(); - const mxc = await this.state.recorder.upload(); - MatrixClientPeg.get().sendMessage(this.props.room.roomId, { - "body": "Voice message", - "msgtype": "org.matrix.msc2516.voice", - //"msgtype": MsgType.Audio, - "url": mxc, - "info": { - duration: Math.round(this.state.recorder.durationSeconds * 1000), - mimetype: this.state.recorder.contentType, - size: this.state.recorder.contentLength, - }, - - // MSC1767 experiment - "org.matrix.msc1767.text": "Voice message", - "org.matrix.msc1767.file": { - url: mxc, - name: "Voice message.ogg", - mimetype: this.state.recorder.contentType, - size: this.state.recorder.contentLength, - }, - "org.matrix.msc1767.audio": { - duration: Math.round(this.state.recorder.durationSeconds * 1000), - // TODO: @@ TravisR: Waveform? (MSC1767 decision) - }, - "org.matrix.experimental.msc2516.voice": { // MSC2516+MSC1767 experiment - duration: Math.round(this.state.recorder.durationSeconds * 1000), - - // Events can't have floats, so we try to maintain resolution by using 1024 - // as a maximum value. The waveform contains values between zero and 1, so this - // should come out largely sane. - // - // We're expecting about one data point per second of audio. - waveform: this.state.recorder.finalWaveform.map(v => Math.round(v * 1024)), - }, - }); - await VoiceRecordingStore.instance.disposeRecording(); - this.setState({recorder: null}); return; } const recorder = VoiceRecordingStore.instance.startRecording(); await recorder.start(); - this.setState({recorder}); + + // We don't need to remove the listener: the recorder will clean that up for us. + recorder.on(UPDATE_EVENT, (ev: RecordingState) => { + if (ev === RecordingState.EndingSoon) return; // ignore this state: it has no UI purpose here + this.setState({recordingPhase: ev}); + }); + + this.setState({recorder, recordingPhase: RecordingState.Started}); }; private renderWaveformArea() { if (!this.state.recorder) return null; - return
    - - + const classes = classNames({ + 'mx_VoiceRecordComposerTile_waveformContainer': true, + 'mx_VoiceRecordComposerTile_recording': this.state.recordingPhase === RecordingState.Started, + }); + + const clock = ; + let waveform = ; + if (this.state.recordingPhase !== RecordingState.Started) { + waveform = ; + } + + return
    + {clock} + {waveform}
    ; } public render() { - const classes = classNames({ - 'mx_MessageComposer_button': !this.state.recorder, - 'mx_MessageComposer_voiceMessage': !this.state.recorder, - 'mx_VoiceRecordComposerTile_stop': !!this.state.recorder, - }); + let recordingInfo; + if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) { + const classes = classNames({ + 'mx_MessageComposer_button': !this.state.recorder, + 'mx_MessageComposer_voiceMessage': !this.state.recorder, + 'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording, + }); - let tooltip = _t("Record a voice message"); - if (!!this.state.recorder) { - // TODO: @@ TravisR: Change to match behaviour - tooltip = _t("Stop & send recording"); + let tooltip = _t("Record a voice message"); + if (!!this.state.recorder) { + tooltip = _t("Stop the recording"); + } + + let stopOrRecordBtn = ; + if (this.state.recorder && !this.state.recorder?.isRecording) { + stopOrRecordBtn = null; + } + + recordingInfo = stopOrRecordBtn; } return (<> {this.renderWaveformArea()} - + {recordingInfo} ); } } diff --git a/src/components/views/voice_messages/IRecordingWaveformStateProps.ts b/src/components/views/voice_messages/IRecordingWaveformStateProps.ts new file mode 100644 index 0000000000..fcdbf3e3b1 --- /dev/null +++ b/src/components/views/voice_messages/IRecordingWaveformStateProps.ts @@ -0,0 +1,26 @@ +/* +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 {VoiceRecording} from "../../../voice/VoiceRecording"; + +export interface IRecordingWaveformProps { + recorder: VoiceRecording; +} + +export interface IRecordingWaveformState { + heights: number[]; +} + +export const DOWNSAMPLE_TARGET = 35; // number of bars we want diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx index c1f5e97fff..e9b3fea629 100644 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/voice_messages/LiveRecordingWaveform.tsx @@ -20,22 +20,13 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; import {arrayFastResample, arraySeed} from "../../../utils/arrays"; import {percentageOf} from "../../../utils/numbers"; import Waveform from "./Waveform"; - -interface IProps { - recorder: VoiceRecording; -} - -interface IState { - heights: number[]; -} - -const DOWNSAMPLE_TARGET = 35; // number of bars we want +import {DOWNSAMPLE_TARGET, IRecordingWaveformProps, IRecordingWaveformState} from "./IRecordingWaveformStateProps"; /** * A waveform which shows the waveform of a live recording */ @replaceableComponent("views.voice_messages.LiveRecordingWaveform") -export default class LiveRecordingWaveform extends React.PureComponent { +export default class LiveRecordingWaveform extends React.PureComponent { public constructor(props) { super(props); diff --git a/src/components/views/voice_messages/PlaybackWaveform.tsx b/src/components/views/voice_messages/PlaybackWaveform.tsx new file mode 100644 index 0000000000..02647aa3ee --- /dev/null +++ b/src/components/views/voice_messages/PlaybackWaveform.tsx @@ -0,0 +1,43 @@ +/* +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 from "react"; +import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {arrayFastResample, arraySeed, arrayTrimFill} from "../../../utils/arrays"; +import {percentageOf} from "../../../utils/numbers"; +import Waveform from "./Waveform"; +import {DOWNSAMPLE_TARGET, IRecordingWaveformProps, IRecordingWaveformState} from "./IRecordingWaveformStateProps"; + +/** + * A waveform which shows the waveform of a previously recorded recording + */ +@replaceableComponent("views.voice_messages.LiveRecordingWaveform") +export default class PlaybackWaveform extends React.PureComponent { + public constructor(props) { + super(props); + + // Like the live recording waveform + const bars = arrayFastResample(this.props.recorder.finalWaveform, DOWNSAMPLE_TARGET); + const seed = arraySeed(0, DOWNSAMPLE_TARGET); + const heights = arrayTrimFill(bars, DOWNSAMPLE_TARGET, seed).map(b => percentageOf(b, 0, 0.5)); + this.state = {heights}; + } + + public render() { + return ; + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c6f7a8d25e..f592cc19cf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1645,7 +1645,7 @@ "Jump to first unread message.": "Jump to first unread message.", "Mark all as read": "Mark all as read", "Record a voice message": "Record a voice message", - "Stop & send recording": "Stop & send recording", + "Stop the recording": "Stop the recording", "Error updating main address": "Error updating main address", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.", diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index cea377bfe9..f7e693452b 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -73,6 +73,26 @@ export function arraySeed(val: T, length: number): T[] { return a; } +/** + * Trims or fills the array to ensure it meets the desired length. The seed array + * given is pulled from to fill any missing slots - it is recommended that this be + * at least `len` long. The resulting array will be exactly `len` long, either + * trimmed from the source or filled with the some/all of the seed array. + * @param {T[]} a The array to trim/fill. + * @param {number} len The length to trim or fill to, as needed. + * @param {T[]} seed Values to pull from if the array needs filling. + * @returns {T[]} The resulting array of `len` length. + */ +export function arrayTrimFill(a: T[], len: number, seed: T[]): T[] { + // Dev note: we do length checks because the spread operator can result in some + // performance penalties in more critical code paths. As a utility, it should be + // as fast as possible to not cause a problem for the call stack, no matter how + // critical that stack is. + if (a.length === len) return a; + if (a.length > len) return a.slice(0, len); + return a.concat(seed.slice(0, len - a.length)); +} + /** * Clones an array as fast as possible, retaining references of the array's values. * @param a The array to clone. Must be defined. diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index b0cc3cd407..68a944da51 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -25,6 +25,7 @@ import {IDestroyable} from "../utils/IDestroyable"; import {Singleflight} from "../utils/Singleflight"; import {PayloadEvent, WORKLET_NAME} from "./consts"; import {arrayFastClone} from "../utils/arrays"; +import {UPDATE_EVENT} from "../stores/AsyncStore"; const CHANNELS = 1; // stereo isn't important const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. @@ -79,6 +80,16 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { return this.recorderContext.currentTime; } + public get isRecording(): boolean { + return this.recording; + } + + public emit(event: string, ...args: any[]): boolean { + super.emit(event, ...args); + super.emit(UPDATE_EVENT, event, ...args); + return true; // we don't ever care if the event had listeners, so just return "yes" + } + private async makeRecorder() { this.recorderStream = await navigator.mediaDevices.getUserMedia({ audio: { diff --git a/test/utils/arrays-test.ts b/test/utils/arrays-test.ts index ececd274b2..c5be59ab43 100644 --- a/test/utils/arrays-test.ts +++ b/test/utils/arrays-test.ts @@ -22,6 +22,7 @@ import { arrayHasOrderChange, arrayMerge, arraySeed, + arrayTrimFill, arrayUnion, ArrayUtil, GroupedArray, @@ -64,6 +65,38 @@ describe('arrays', () => { }); }); + describe('arrayTrimFill', () => { + it('should shrink arrays', () => { + const input = [1, 2, 3]; + const output = [1, 2]; + const seed = [4, 5, 6]; + const result = arrayTrimFill(input, output.length, seed); + expect(result).toBeDefined(); + expect(result).toHaveLength(output.length); + expect(result).toEqual(output); + }); + + it('should expand arrays', () => { + const input = [1, 2, 3]; + const output = [1, 2, 3, 4, 5]; + const seed = [4, 5, 6]; + const result = arrayTrimFill(input, output.length, seed); + expect(result).toBeDefined(); + expect(result).toHaveLength(output.length); + expect(result).toEqual(output); + }); + + it('should keep arrays the same', () => { + const input = [1, 2, 3]; + const output = [1, 2, 3]; + const seed = [4, 5, 6]; + const result = arrayTrimFill(input, output.length, seed); + expect(result).toBeDefined(); + expect(result).toHaveLength(output.length); + expect(result).toEqual(output); + }); + }); + describe('arraySeed', () => { it('should create an array of given length', () => { const val = 1; From e079f64a16f9bc14c9b458aca971daac2374630d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 20 Apr 2021 21:43:55 -0600 Subject: [PATCH 301/330] Adjust pixel dimensions --- .../views/rooms/_VoiceRecordComposerTile.scss | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index dc4835c4dd..1975cf943f 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -36,9 +36,8 @@ limitations under the License. } .mx_VoiceRecordComposerTile_waveformContainer { - padding: 8px; // makes us 4px taller than the send/stop button - padding-right: 4px; // there's 1px from the waveform itself, so account for that - padding-left: 15px; // +10px for the live circle, +5px for regular padding + padding: 6px; // makes us 4px taller than the send/stop button + padding-right: 5px; // there's 1px from the waveform itself, so account for that background-color: $voice-record-waveform-bg-color; border-radius: 12px; margin: 6px; // force the composer area to put a gutter around us @@ -53,27 +52,36 @@ limitations under the License. color: $voice-record-waveform-fg-color; font-size: $font-14px; - &.mx_VoiceRecordComposerTile_recording::before { - animation: recording-pulse 2s infinite; + &.mx_VoiceRecordComposerTile_recording { + padding-left: 16px; // +10px for the live circle, +6px for regular padding - content: ''; - background-color: $voice-record-live-circle-color; - width: 10px; - height: 10px; - position: absolute; - left: 8px; - top: 18px; // vertically center - border-radius: 10px; + &::before { + animation: recording-pulse 2s infinite; + + content: ''; + background-color: $voice-record-live-circle-color; + width: 10px; + height: 10px; + position: absolute; + left: 8px; + top: 16px; // vertically center + border-radius: 10px; + } } - .mx_Waveform_bar { - background-color: $voice-record-waveform-fg-color; + .mx_Waveform { + // We want the bars to be 2px shorter than the play/pause button in the waveform control + height: 28px; // default is 30px, so we're subtracting the 2px border off the bars + + .mx_Waveform_bar { + background-color: $voice-record-waveform-fg-color; + } } .mx_Clock { - padding-right: 8px; // isolate from waveform - padding-left: 10px; // isolate from live circle - width: 42px; // we're not using a monospace font, so fake it + padding-right: 4px; // isolate from waveform + padding-left: 8px; // isolate from live circle + width: 40px; // we're not using a monospace font, so fake it } } From 30e120284d0d9a8661ac164713b7c8cd91ce6495 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 26 Apr 2021 20:48:24 -0600 Subject: [PATCH 302/330] Add simple play/pause controls --- res/css/_components.scss | 1 + .../voice_messages/_PlayPauseButton.scss | 51 ++++++++++ res/img/element-icons/pause-symbol.svg | 4 + res/img/element-icons/play-symbol.svg | 3 + .../views/rooms/VoiceRecordComposerTile.tsx | 7 ++ .../views/voice_messages/PlayPauseButton.tsx | 89 +++++++++++++++++ src/i18n/strings/en_EN.json | 2 + src/voice/Playback.ts | 97 +++++++++++++++++++ src/voice/VoiceRecording.ts | 9 ++ 9 files changed, 263 insertions(+) create mode 100644 res/css/views/voice_messages/_PlayPauseButton.scss create mode 100644 res/img/element-icons/pause-symbol.svg create mode 100644 res/img/element-icons/play-symbol.svg create mode 100644 src/components/views/voice_messages/PlayPauseButton.tsx create mode 100644 src/voice/Playback.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index 253f97bf42..0179d146d1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -248,6 +248,7 @@ @import "./views/toasts/_AnalyticsToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; +@import "./views/voice_messages/_PlayPauseButton.scss"; @import "./views/voice_messages/_Waveform.scss"; @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; diff --git a/res/css/views/voice_messages/_PlayPauseButton.scss b/res/css/views/voice_messages/_PlayPauseButton.scss new file mode 100644 index 0000000000..0fd31be28a --- /dev/null +++ b/res/css/views/voice_messages/_PlayPauseButton.scss @@ -0,0 +1,51 @@ +/* +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. +*/ + +.mx_PlayPauseButton { + position: relative; + width: 32px; + height: 32px; + border-radius: 32px; + background-color: $primary-bg-color; + + &::before { + content: ''; + position: absolute; // sizing varies by icon + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + + &.mx_PlayPauseButton_disabled::before { + opacity: 0.5; + } + + &.mx_PlayPauseButton_play::before { + width: 13px; + height: 16px; + top: 8px; // center + left: 12px; // center + mask-image: url('$(res)/img/element-icons/play-symbol.svg'); + } + + &.mx_PlayPauseButton_pause::before { + width: 10px; + height: 12px; + top: 10px; // center + left: 11px; // center + mask-image: url('$(res)/img/element-icons/pause-symbol.svg'); + } +} diff --git a/res/img/element-icons/pause-symbol.svg b/res/img/element-icons/pause-symbol.svg new file mode 100644 index 0000000000..293c0a10d8 --- /dev/null +++ b/res/img/element-icons/pause-symbol.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/play-symbol.svg b/res/img/element-icons/play-symbol.svg new file mode 100644 index 0000000000..339e20b729 --- /dev/null +++ b/res/img/element-icons/play-symbol.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 3fcafafd05..53dc7cc9c5 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -27,6 +27,7 @@ import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import PlaybackWaveform from "../voice_messages/PlaybackWaveform"; +import PlayPauseButton from "../voice_messages/PlayPauseButton"; interface IProps { room: Room; @@ -131,7 +132,13 @@ export default class VoiceRecordComposerTile extends React.PureComponent; } + let playPause = null; + if (this.state.recordingPhase === RecordingState.Ended) { + playPause = ; + } + return
    + {playPause} {clock} {waveform}
    ; diff --git a/src/components/views/voice_messages/PlayPauseButton.tsx b/src/components/views/voice_messages/PlayPauseButton.tsx new file mode 100644 index 0000000000..1339caf77f --- /dev/null +++ b/src/components/views/voice_messages/PlayPauseButton.tsx @@ -0,0 +1,89 @@ +/* +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 from "react"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {VoiceRecording} from "../../../voice/VoiceRecording"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import {_t} from "../../../languageHandler"; +import {Playback, PlaybackState} from "../../../voice/Playback"; +import classNames from "classnames"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; + +interface IProps { + recorder: VoiceRecording; +} + +interface IState { + playback: Playback; + playbackPhase: PlaybackState; +} + +/** + * Displays a play/pause button (activating the play/pause function of the recorder) + * to be displayed in reference to a recording. + */ +@replaceableComponent("views.voice_messages.PlayPauseButton") +export default class PlayPauseButton extends React.PureComponent { + public constructor(props) { + super(props); + this.state = { + playback: null, // not ready yet + playbackPhase: PlaybackState.Decoding, + }; + } + + public async componentDidMount() { + const playback = await this.props.recorder.getPlayback(); + playback.on(UPDATE_EVENT, this.onPlaybackState); + this.setState({ + playback: playback, + + // We know the playback is no longer decoding when we get here. It'll emit an update + // before we've bound a listener, so we just update the state here. + playbackPhase: PlaybackState.Stopped, + }); + } + + public componentWillUnmount() { + if (this.state.playback) this.state.playback.off(UPDATE_EVENT, this.onPlaybackState); + } + + private onPlaybackState = (newState: PlaybackState) => { + this.setState({playbackPhase: newState}); + }; + + private onClick = async () => { + if (!this.state.playback) return; // ignore for now + await this.state.playback.toggle(); + }; + + public render() { + const isPlaying = this.state.playback?.isPlaying; + const isDisabled = this.state.playbackPhase === PlaybackState.Decoding; + const classes = classNames('mx_PlayPauseButton', { + 'mx_PlayPauseButton_play': !isPlaying, + 'mx_PlayPauseButton_pause': isPlaying, + 'mx_PlayPauseButton_disabled': isDisabled, + }); + return ; + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f592cc19cf..943b2291b3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -899,6 +899,8 @@ "Incoming call": "Incoming call", "Decline": "Decline", "Accept": "Accept", + "Pause": "Pause", + "Play": "Play", "The other party cancelled the verification.": "The other party cancelled the verification.", "Verified!": "Verified!", "You've successfully verified this user.": "You've successfully verified this user.", diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts new file mode 100644 index 0000000000..0039113a57 --- /dev/null +++ b/src/voice/Playback.ts @@ -0,0 +1,97 @@ +/* +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 EventEmitter from "events"; +import {UPDATE_EVENT} from "../stores/AsyncStore"; + +export enum PlaybackState { + Decoding = "decoding", + Stopped = "stopped", // no progress on timeline + Paused = "paused", // some progress on timeline + Playing = "playing", // active progress through timeline +} + +export class Playback extends EventEmitter { + private context: AudioContext; + private source: AudioBufferSourceNode; + private state = PlaybackState.Decoding; + private audioBuf: AudioBuffer; + + constructor(private buf: ArrayBuffer) { + super(); + this.context = new AudioContext(); + } + + public emit(event: PlaybackState, ...args: any[]): boolean { + this.state = event; + super.emit(event, ...args); + super.emit(UPDATE_EVENT, event, ...args); + return true; // we don't ever care if the event had listeners, so just return "yes" + } + + public async prepare() { + this.audioBuf = await this.context.decodeAudioData(this.buf); + this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore + } + + public get currentState(): PlaybackState { + return this.state; + } + + public get isPlaying(): boolean { + return this.currentState === PlaybackState.Playing; + } + + private onPlaybackEnd = async () => { + await this.context.suspend(); + this.emit(PlaybackState.Stopped); + }; + + public async play() { + // We can't restart a buffer source, so we need to create a new one if we hit the end + if (this.state === PlaybackState.Stopped) { + if (this.source) { + this.source.disconnect(); + this.source.removeEventListener("ended", this.onPlaybackEnd); + } + + this.source = this.context.createBufferSource(); + this.source.connect(this.context.destination); + this.source.buffer = this.audioBuf; + this.source.start(); // start immediately + this.source.addEventListener("ended", this.onPlaybackEnd); + } + + // We use the context suspend/resume functions because it allows us to pause a source + // node, but that still doesn't help us when the source node runs out (see above). + await this.context.resume(); + this.emit(PlaybackState.Playing); + } + + public async pause() { + await this.context.suspend(); + this.emit(PlaybackState.Paused); + } + + public async stop() { + await this.onPlaybackEnd(); + } + + public async toggle() { + if (this.isPlaying) await this.pause(); + else await this.play(); + } +} diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 68a944da51..692e317333 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -26,6 +26,7 @@ import {Singleflight} from "../utils/Singleflight"; import {PayloadEvent, WORKLET_NAME} from "./consts"; import {arrayFastClone} from "../utils/arrays"; import {UPDATE_EVENT} from "../stores/AsyncStore"; +import {Playback} from "./Playback"; const CHANNELS = 1; // stereo isn't important const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. @@ -270,6 +271,14 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { }); } + public getPlayback(): Promise { + return Singleflight.for(this, "playback").do(async () => { + const playback = new Playback(this.buffer.buffer); // cast to ArrayBuffer proper + await playback.prepare(); + return playback; + }); + } + public destroy() { // noinspection JSIgnoredPromiseFromCall - not concerned about stop() being called async here this.stop(); From c1bb0bb0b8866a9b4fb8cdcfff5569ac9a0ad306 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 26 Apr 2021 21:08:51 -0600 Subject: [PATCH 303/330] Add a delete button --- .../views/rooms/_VoiceRecordComposerTile.scss | 11 ++++++++ .../views/rooms/VoiceRecordComposerTile.tsx | 25 +++++++++++++++++-- src/i18n/strings/en_EN.json | 1 + 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index 1975cf943f..cb77878666 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -35,6 +35,17 @@ limitations under the License. } } +.mx_VoiceRecordComposerTile_delete { + width: 14px; // w&h are size of icon + height: 18px; + vertical-align: middle; + margin-right: 7px; // distance from left edge of waveform container (container has some margin too) + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/trashcan.svg'); +} + .mx_VoiceRecordComposerTile_waveformContainer { padding: 6px; // makes us 4px taller than the send/stop button padding-right: 5px; // there's 1px from the waveform itself, so account for that diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 53dc7cc9c5..b02c6a4bae 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -97,10 +97,20 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)), }, }); - await VoiceRecordingStore.instance.disposeRecording(); - this.setState({recorder: null}); + await this.disposeRecording(); } + private async disposeRecording() { + await VoiceRecordingStore.instance.disposeRecording(); + + // Reset back to no recording, which means no phase (ie: restart component entirely) + this.setState({recorder: null, recordingPhase: null}); + } + + private onCancel = async () => { + await this.disposeRecording(); + }; + private onRecordStartEndClick = async () => { if (this.state.recorder) { await this.state.recorder.stop(); @@ -134,6 +144,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent; } @@ -146,6 +157,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent; + } + return (<> + {deleteButton} {this.renderWaveformArea()} {recordingInfo} ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 943b2291b3..264c35144e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1648,6 +1648,7 @@ "Mark all as read": "Mark all as read", "Record a voice message": "Record a voice message", "Stop the recording": "Stop the recording", + "Delete recording": "Delete recording", "Error updating main address": "Error updating main address", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.", From 5e646f861cf592e72ae7eb18e64bd65f15de3245 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 18:59:10 -0600 Subject: [PATCH 304/330] Wire up the send button for voice messages This fixes a bug where we couldn't upload voice messages because the audio buffer was being read, therefore changing the position of the cursor. When this happened, the upload function would claim that the buffer was empty and could not be read. --- src/components/views/rooms/MessageComposer.tsx | 12 +++++++++++- src/voice/VoiceRecording.ts | 14 ++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index d126a7b161..3671069903 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -198,6 +198,7 @@ interface IState { export default class MessageComposer extends React.Component { private dispatcherRef: string; private messageComposerInput: SendMessageComposer; + private voiceRecordingButton: VoiceRecordComposerTile; constructor(props) { super(props); @@ -322,7 +323,15 @@ export default class MessageComposer extends React.Component { }); } - sendMessage = () => { + sendMessage = async () => { + if (this.state.haveRecording && this.voiceRecordingButton) { + // There shouldn't be any text message to send when a voice recording is active, so + // just send out the voice recording. + await this.voiceRecordingButton.send(); + return; + } + + // XXX: Private function access this.messageComposerInput._sendMessage(); } @@ -387,6 +396,7 @@ export default class MessageComposer extends React.Component { if (SettingsStore.getValue("feature_voice_messages")) { controls.push( this.voiceRecordingButton = c} room={this.props.room} />); } diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 692e317333..0c76ac406f 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -54,7 +54,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private recorderStream: MediaStream; private recorderFFT: AnalyserNode; private recorderWorklet: AudioWorkletNode; - private buffer = new Uint8Array(0); + private buffer = new Uint8Array(0); // use this.audioBuffer to access private mxc: string; private recording = false; private observable: SimpleObservable; @@ -166,6 +166,12 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { }; } + private get audioBuffer(): Uint8Array { + // We need a clone of the buffer to avoid accidentally changing the position + // on the real thing. + return this.buffer.slice(0); + } + public get liveData(): SimpleObservable { if (!this.recording) throw new Error("No observable when not recording"); return this.observable; @@ -267,13 +273,13 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { await this.recorder.close(); this.emit(RecordingState.Ended); - return this.buffer; + return this.audioBuffer; }); } public getPlayback(): Promise { return Singleflight.for(this, "playback").do(async () => { - const playback = new Playback(this.buffer.buffer); // cast to ArrayBuffer proper + const playback = new Playback(this.audioBuffer.buffer); // cast to ArrayBuffer proper await playback.prepare(); return playback; }); @@ -294,7 +300,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { if (this.mxc) return this.mxc; this.emit(RecordingState.Uploading); - this.mxc = await this.client.uploadContent(new Blob([this.buffer], { + this.mxc = await this.client.uploadContent(new Blob([this.audioBuffer], { type: this.contentType, }), { onlyContentUri: false, // to stop the warnings in the console From c2d37af1cb2e4471988d32e27c95dbee9a57c89c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 20:27:36 -0600 Subject: [PATCH 305/330] Move playback to its own set of classes This all started with a bug where the clock wouldn't update appropriately, and ended with a whole refactoring to support later playback in the timeline. Playback and recording instances are now independent, and this applies to the components as well. Instead of those playback components taking a recording, they take a playback instance which has all the information the components need. The clock was incredibly difficult to do because of the audio context's time tracking and the source's inability to say where it is at in the buffer/in time. This means we have to track when we started playing the clip so we can capture the audio context's current time, which may be a few seconds by the first time the user hits play. We also track stops so we know when to reset that flag. Waveform calculations have also been moved into the base component, deduplicating the math a bit. --- res/css/_components.scss | 1 + .../views/rooms/_VoiceRecordComposerTile.scss | 30 +------ .../voice_messages/_PlaybackContainer.scss | 48 ++++++++++++ .../views/rooms/VoiceRecordComposerTile.tsx | 35 +++------ src/components/views/voice_messages/Clock.tsx | 8 +- .../IRecordingWaveformStateProps.ts | 26 ------- .../voice_messages/LiveRecordingClock.tsx | 8 +- .../voice_messages/LiveRecordingWaveform.tsx | 16 +++- .../views/voice_messages/PlayPauseButton.tsx | 44 +++-------- .../views/voice_messages/PlaybackClock.tsx | 71 +++++++++++++++++ .../views/voice_messages/PlaybackWaveform.tsx | 35 ++++++--- .../voice_messages/RecordingPlayback.tsx | 62 +++++++++++++++ src/voice/Playback.ts | 66 +++++++++++++--- src/voice/PlaybackClock.ts | 78 +++++++++++++++++++ src/voice/VoiceRecording.ts | 26 ++++--- 15 files changed, 400 insertions(+), 154 deletions(-) create mode 100644 res/css/views/voice_messages/_PlaybackContainer.scss delete mode 100644 src/components/views/voice_messages/IRecordingWaveformStateProps.ts create mode 100644 src/components/views/voice_messages/PlaybackClock.tsx create mode 100644 src/components/views/voice_messages/RecordingPlayback.tsx create mode 100644 src/voice/PlaybackClock.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index 0179d146d1..0057f8a8fc 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -249,6 +249,7 @@ @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voice_messages/_PlayPauseButton.scss"; +@import "./views/voice_messages/_PlaybackContainer.scss"; @import "./views/voice_messages/_Waveform.scss"; @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index cb77878666..e99e1a00e1 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -46,23 +46,14 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/trashcan.svg'); } -.mx_VoiceRecordComposerTile_waveformContainer { - padding: 6px; // makes us 4px taller than the send/stop button - padding-right: 5px; // there's 1px from the waveform itself, so account for that - background-color: $voice-record-waveform-bg-color; - border-radius: 12px; +.mx_VoiceMessagePrimaryContainer { + // Note: remaining class properties are in the PlayerContainer CSS. + margin: 6px; // force the composer area to put a gutter around us margin-right: 12px; // isolate from stop button - // Cheat at alignment a bit - display: flex; - align-items: center; - position: relative; // important for the live circle - color: $voice-record-waveform-fg-color; - font-size: $font-14px; - &.mx_VoiceRecordComposerTile_recording { padding-left: 16px; // +10px for the live circle, +6px for regular padding @@ -79,21 +70,6 @@ limitations under the License. border-radius: 10px; } } - - .mx_Waveform { - // We want the bars to be 2px shorter than the play/pause button in the waveform control - height: 28px; // default is 30px, so we're subtracting the 2px border off the bars - - .mx_Waveform_bar { - background-color: $voice-record-waveform-fg-color; - } - } - - .mx_Clock { - padding-right: 4px; // isolate from waveform - padding-left: 8px; // isolate from live circle - width: 40px; // we're not using a monospace font, so fake it - } } // The keyframes are slightly weird here to help make a ramping/punch effect diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/voice_messages/_PlaybackContainer.scss new file mode 100644 index 0000000000..a9ebc19667 --- /dev/null +++ b/res/css/views/voice_messages/_PlaybackContainer.scss @@ -0,0 +1,48 @@ +/* +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. +*/ + +// Dev note: there's no actual component called . These classes +// are shared amongst multiple voice message components. + +// Container for live recording and playback controls +.mx_VoiceMessagePrimaryContainer { + padding: 6px; // makes us 4px taller than the send/stop button + padding-right: 5px; // there's 1px from the waveform itself, so account for that + background-color: $voice-record-waveform-bg-color; + border-radius: 12px; + + // Cheat at alignment a bit + display: flex; + align-items: center; + + color: $voice-record-waveform-fg-color; + font-size: $font-14px; + + .mx_Waveform { + // We want the bars to be 2px shorter than the play/pause button in the waveform control + height: 28px; // default is 30px, so we're subtracting the 2px border off the bars + + .mx_Waveform_bar { + background-color: $voice-record-waveform-fg-color; + } + } + + .mx_Clock { + padding-right: 4px; // isolate from waveform + padding-left: 8px; // isolate from live circle + width: 40px; // we're not using a monospace font, so fake it + } +} diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index b02c6a4bae..bab95291ba 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -16,7 +16,7 @@ limitations under the License. import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {_t} from "../../../languageHandler"; -import React from "react"; +import React, {ReactNode} from "react"; import {RecordingState, VoiceRecording} from "../../../voice/VoiceRecording"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; @@ -26,8 +26,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; -import PlaybackWaveform from "../voice_messages/PlaybackWaveform"; -import PlayPauseButton from "../voice_messages/PlayPauseButton"; +import RecordingPlayback from "../voice_messages/RecordingPlayback"; interface IProps { room: Room; @@ -94,7 +93,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)), + waveform: this.state.recorder.getPlayback().waveform.map(v => Math.round(v * 1024)), }, }); await this.disposeRecording(); @@ -128,34 +127,22 @@ export default class VoiceRecordComposerTile extends React.PureComponent; - let waveform = ; if (this.state.recordingPhase !== RecordingState.Started) { - waveform = ; - } - - let playPause = null; - if (this.state.recordingPhase === RecordingState.Ended) { // TODO: @@ TR: Should we disable this during upload? What does a failed upload look like? - playPause = ; + return ; } - return
    - {playPause} - {clock} - {waveform} + // only other UI is the recording-in-progress UI + return
    + +
    ; } - public render() { + public render(): ReactNode { let recordingInfo; let deleteButton; if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) { diff --git a/src/components/views/voice_messages/Clock.tsx b/src/components/views/voice_messages/Clock.tsx index 6c256957e9..8b71f6b7fe 100644 --- a/src/components/views/voice_messages/Clock.tsx +++ b/src/components/views/voice_messages/Clock.tsx @@ -29,11 +29,17 @@ interface IState { * displayed, making it possible to see "82:29". */ @replaceableComponent("views.voice_messages.Clock") -export default class Clock extends React.PureComponent { +export default class Clock extends React.Component { public constructor(props) { super(props); } + shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { + const currentFloor = Math.floor(this.props.seconds); + const nextFloor = Math.floor(nextProps.seconds); + return currentFloor !== nextFloor; + } + public render() { const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0'); const seconds = Math.round(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis diff --git a/src/components/views/voice_messages/IRecordingWaveformStateProps.ts b/src/components/views/voice_messages/IRecordingWaveformStateProps.ts deleted file mode 100644 index fcdbf3e3b1..0000000000 --- a/src/components/views/voice_messages/IRecordingWaveformStateProps.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* -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 {VoiceRecording} from "../../../voice/VoiceRecording"; - -export interface IRecordingWaveformProps { - recorder: VoiceRecording; -} - -export interface IRecordingWaveformState { - heights: number[]; -} - -export const DOWNSAMPLE_TARGET = 35; // number of bars we want diff --git a/src/components/views/voice_messages/LiveRecordingClock.tsx b/src/components/views/voice_messages/LiveRecordingClock.tsx index 5e9006c6ab..b82539eb16 100644 --- a/src/components/views/voice_messages/LiveRecordingClock.tsx +++ b/src/components/views/voice_messages/LiveRecordingClock.tsx @@ -31,7 +31,7 @@ interface IState { * A clock for a live recording. */ @replaceableComponent("views.voice_messages.LiveRecordingClock") -export default class LiveRecordingClock extends React.Component { +export default class LiveRecordingClock extends React.PureComponent { public constructor(props) { super(props); @@ -39,12 +39,6 @@ export default class LiveRecordingClock extends React.Component this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); } - shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { - const currentFloor = Math.floor(this.state.seconds); - const nextFloor = Math.floor(nextState.seconds); - return currentFloor !== nextFloor; - } - private onRecordingUpdate = (update: IRecordingUpdate) => { this.setState({seconds: update.timeSeconds}); }; diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx index e9b3fea629..e7c34c9177 100644 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/voice_messages/LiveRecordingWaveform.tsx @@ -20,24 +20,32 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; import {arrayFastResample, arraySeed} from "../../../utils/arrays"; import {percentageOf} from "../../../utils/numbers"; import Waveform from "./Waveform"; -import {DOWNSAMPLE_TARGET, IRecordingWaveformProps, IRecordingWaveformState} from "./IRecordingWaveformStateProps"; +import {PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback"; + +interface IProps { + recorder: VoiceRecording; +} + +interface IState { + heights: number[]; +} /** * A waveform which shows the waveform of a live recording */ @replaceableComponent("views.voice_messages.LiveRecordingWaveform") -export default class LiveRecordingWaveform extends React.PureComponent { +export default class LiveRecordingWaveform extends React.PureComponent { public constructor(props) { super(props); - this.state = {heights: arraySeed(0, DOWNSAMPLE_TARGET)}; + this.state = {heights: arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES)}; this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); } private onRecordingUpdate = (update: IRecordingUpdate) => { // The waveform and the downsample target are pretty close, so we should be fine to // do this, despite the docs on arrayFastResample. - const bars = arrayFastResample(Array.from(update.waveform), DOWNSAMPLE_TARGET); + const bars = arrayFastResample(Array.from(update.waveform), PLAYBACK_WAVEFORM_SAMPLES); this.setState({ // The incoming data is between zero and one, but typically even screaming into a // microphone won't send you over 0.6, so we artificially adjust the gain for the diff --git a/src/components/views/voice_messages/PlayPauseButton.tsx b/src/components/views/voice_messages/PlayPauseButton.tsx index 1339caf77f..b4f69b02bc 100644 --- a/src/components/views/voice_messages/PlayPauseButton.tsx +++ b/src/components/views/voice_messages/PlayPauseButton.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, {ReactNode} from "react"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {VoiceRecording} from "../../../voice/VoiceRecording"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; @@ -24,12 +24,14 @@ import classNames from "classnames"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; interface IProps { - recorder: VoiceRecording; + // Playback instance to manipulate. Cannot change during the component lifecycle. + playback: Playback; + + // The playback phase to render. Able to change during the component lifecycle. + playbackPhase: PlaybackState; } interface IState { - playback: Playback; - playbackPhase: PlaybackState; } /** @@ -40,40 +42,16 @@ interface IState { export default class PlayPauseButton extends React.PureComponent { public constructor(props) { super(props); - this.state = { - playback: null, // not ready yet - playbackPhase: PlaybackState.Decoding, - }; + this.state = {}; } - public async componentDidMount() { - const playback = await this.props.recorder.getPlayback(); - playback.on(UPDATE_EVENT, this.onPlaybackState); - this.setState({ - playback: playback, - - // We know the playback is no longer decoding when we get here. It'll emit an update - // before we've bound a listener, so we just update the state here. - playbackPhase: PlaybackState.Stopped, - }); - } - - public componentWillUnmount() { - if (this.state.playback) this.state.playback.off(UPDATE_EVENT, this.onPlaybackState); - } - - private onPlaybackState = (newState: PlaybackState) => { - this.setState({playbackPhase: newState}); - }; - private onClick = async () => { - if (!this.state.playback) return; // ignore for now - await this.state.playback.toggle(); + await this.props.playback.toggle(); }; - public render() { - const isPlaying = this.state.playback?.isPlaying; - const isDisabled = this.state.playbackPhase === PlaybackState.Decoding; + public render(): ReactNode { + const isPlaying = this.props.playback.isPlaying; + const isDisabled = this.props.playbackPhase === PlaybackState.Decoding; const classes = classNames('mx_PlayPauseButton', { 'mx_PlayPauseButton_play': !isPlaying, 'mx_PlayPauseButton_pause': isPlaying, diff --git a/src/components/views/voice_messages/PlaybackClock.tsx b/src/components/views/voice_messages/PlaybackClock.tsx new file mode 100644 index 0000000000..2e8ec9a3e7 --- /dev/null +++ b/src/components/views/voice_messages/PlaybackClock.tsx @@ -0,0 +1,71 @@ +/* +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 from "react"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import Clock from "./Clock"; +import {Playback, PlaybackState} from "../../../voice/Playback"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; + +interface IProps { + playback: Playback; +} + +interface IState { + seconds: number; + durationSeconds: number; + playbackPhase: PlaybackState; +} + +/** + * A clock for a playback of a recording. + */ +@replaceableComponent("views.voice_messages.PlaybackClock") +export default class PlaybackClock extends React.PureComponent { + public constructor(props) { + super(props); + + this.state = { + seconds: this.props.playback.clockInfo.timeSeconds, + // we track the duration on state because we won't really know what the clip duration + // is until the first time update, and as a PureComponent we are trying to dedupe state + // updates as much as possible. This is just the easiest way to avoid a forceUpdate() or + // member property to track "did we get a duration". + durationSeconds: this.props.playback.clockInfo.durationSeconds, + playbackPhase: PlaybackState.Stopped, // assume not started, so full clock + }; + this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); + this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); + } + + private onPlaybackUpdate = (ev: PlaybackState) => { + // Convert Decoding -> Stopped because we don't care about the distinction here + if (ev === PlaybackState.Decoding) ev = PlaybackState.Stopped; + this.setState({playbackPhase: ev}); + }; + + private onTimeUpdate = (time: number[]) => { + this.setState({seconds: time[0], durationSeconds: time[1]}); + }; + + public render() { + let seconds = this.state.seconds; + if (this.state.playbackPhase === PlaybackState.Stopped) { + seconds = this.state.durationSeconds; + } + return ; + } +} diff --git a/src/components/views/voice_messages/PlaybackWaveform.tsx b/src/components/views/voice_messages/PlaybackWaveform.tsx index 02647aa3ee..89de908575 100644 --- a/src/components/views/voice_messages/PlaybackWaveform.tsx +++ b/src/components/views/voice_messages/PlaybackWaveform.tsx @@ -15,28 +15,41 @@ limitations under the License. */ import React from "react"; -import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {arrayFastResample, arraySeed, arrayTrimFill} from "../../../utils/arrays"; -import {percentageOf} from "../../../utils/numbers"; +import {arraySeed, arrayTrimFill} from "../../../utils/arrays"; import Waveform from "./Waveform"; -import {DOWNSAMPLE_TARGET, IRecordingWaveformProps, IRecordingWaveformState} from "./IRecordingWaveformStateProps"; +import {Playback, PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback"; + +interface IProps { + playback: Playback; +} + +interface IState { + heights: number[]; +} /** * A waveform which shows the waveform of a previously recorded recording */ -@replaceableComponent("views.voice_messages.LiveRecordingWaveform") -export default class PlaybackWaveform extends React.PureComponent { +@replaceableComponent("views.voice_messages.PlaybackWaveform") +export default class PlaybackWaveform extends React.PureComponent { public constructor(props) { super(props); - // Like the live recording waveform - const bars = arrayFastResample(this.props.recorder.finalWaveform, DOWNSAMPLE_TARGET); - const seed = arraySeed(0, DOWNSAMPLE_TARGET); - const heights = arrayTrimFill(bars, DOWNSAMPLE_TARGET, seed).map(b => percentageOf(b, 0, 0.5)); - this.state = {heights}; + this.state = {heights: this.toHeights(this.props.playback.waveform)}; + + this.props.playback.waveformData.onUpdate(this.onWaveformUpdate); } + private toHeights(waveform: number[]) { + const seed = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); + return arrayTrimFill(waveform, PLAYBACK_WAVEFORM_SAMPLES, seed); + } + + private onWaveformUpdate = (waveform: number[]) => { + this.setState({heights: this.toHeights(waveform)}); + }; + public render() { return ; } diff --git a/src/components/views/voice_messages/RecordingPlayback.tsx b/src/components/views/voice_messages/RecordingPlayback.tsx new file mode 100644 index 0000000000..776997cec2 --- /dev/null +++ b/src/components/views/voice_messages/RecordingPlayback.tsx @@ -0,0 +1,62 @@ +/* +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 {Playback, PlaybackState} from "../../../voice/Playback"; +import React, {ReactNode} from "react"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import PlaybackWaveform from "./PlaybackWaveform"; +import PlayPauseButton from "./PlayPauseButton"; +import PlaybackClock from "./PlaybackClock"; + +interface IProps { + // Playback instance to render. Cannot change during component lifecycle: create + // an all-new component instead. + playback: Playback; +} + +interface IState { + playbackPhase: PlaybackState; +} + +export default class RecordingPlayback extends React.PureComponent { + constructor(props: IProps) { + super(props); + + this.state = { + playbackPhase: PlaybackState.Decoding, // default assumption + }; + + // We don't need to de-register: the class handles this for us internally + this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); + + // Don't wait for the promise to complete - it will emit a progress update when it + // is done, and it's not meant to take long anyhow. + // noinspection JSIgnoredPromiseFromCall + this.props.playback.prepare(); + } + + private onPlaybackUpdate = (ev: PlaybackState) => { + this.setState({playbackPhase: ev}); + }; + + public render(): ReactNode { + return
    + + + +
    + } +} diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index 0039113a57..99b1f62866 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -16,6 +16,10 @@ limitations under the License. import EventEmitter from "events"; import {UPDATE_EVENT} from "../stores/AsyncStore"; +import {arrayFastResample, arraySeed} from "../utils/arrays"; +import {SimpleObservable} from "matrix-widget-api"; +import {IDestroyable} from "../utils/IDestroyable"; +import {PlaybackClock} from "./PlaybackClock"; export enum PlaybackState { Decoding = "decoding", @@ -24,15 +28,52 @@ export enum PlaybackState { Playing = "playing", // active progress through timeline } -export class Playback extends EventEmitter { - private context: AudioContext; +export const PLAYBACK_WAVEFORM_SAMPLES = 35; +const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); + +export class Playback extends EventEmitter implements IDestroyable { + private readonly context: AudioContext; private source: AudioBufferSourceNode; private state = PlaybackState.Decoding; private audioBuf: AudioBuffer; + private resampledWaveform: number[]; + private waveformObservable = new SimpleObservable(); + private readonly clock: PlaybackClock; - constructor(private buf: ArrayBuffer) { + /** + * Creates a new playback instance from a buffer. + * @param {ArrayBuffer} buf The buffer containing the sound sample. + * @param {number[]} seedWaveform Optional seed waveform to present until the proper waveform + * can be calculated. Contains values between zero and one, inclusive. + */ + constructor(private buf: ArrayBuffer, seedWaveform = DEFAULT_WAVEFORM) { super(); this.context = new AudioContext(); + this.resampledWaveform = arrayFastResample(seedWaveform, PLAYBACK_WAVEFORM_SAMPLES); + this.waveformObservable.update(this.resampledWaveform); + this.clock = new PlaybackClock(this.context); + + // TODO: @@ TR: Calculate real waveform + } + + public get waveform(): number[] { + return this.resampledWaveform; + } + + public get waveformData(): SimpleObservable { + return this.waveformObservable; + } + + public get clockInfo(): PlaybackClock { + return this.clock; + } + + public get currentState(): PlaybackState { + return this.state; + } + + public get isPlaying(): boolean { + return this.currentState === PlaybackState.Playing; } public emit(event: PlaybackState, ...args: any[]): boolean { @@ -42,17 +83,18 @@ export class Playback extends EventEmitter { return true; // we don't ever care if the event had listeners, so just return "yes" } + public destroy() { + // noinspection JSIgnoredPromiseFromCall - not concerned about being called async here + this.stop(); + this.removeAllListeners(); + this.clock.destroy(); + this.waveformObservable.close(); + } + public async prepare() { this.audioBuf = await this.context.decodeAudioData(this.buf); this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore - } - - public get currentState(): PlaybackState { - return this.state; - } - - public get isPlaying(): boolean { - return this.currentState === PlaybackState.Playing; + this.clock.durationSeconds = this.audioBuf.duration; } private onPlaybackEnd = async () => { @@ -78,6 +120,7 @@ export class Playback extends EventEmitter { // We use the context suspend/resume functions because it allows us to pause a source // node, but that still doesn't help us when the source node runs out (see above). await this.context.resume(); + this.clock.flagStart(); this.emit(PlaybackState.Playing); } @@ -88,6 +131,7 @@ export class Playback extends EventEmitter { public async stop() { await this.onPlaybackEnd(); + this.clock.flagStop(); } public async toggle() { diff --git a/src/voice/PlaybackClock.ts b/src/voice/PlaybackClock.ts new file mode 100644 index 0000000000..06d6381691 --- /dev/null +++ b/src/voice/PlaybackClock.ts @@ -0,0 +1,78 @@ +/* +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 {SimpleObservable} from "matrix-widget-api"; +import {IDestroyable} from "../utils/IDestroyable"; + +// Because keeping track of time is sufficiently complicated... +export class PlaybackClock implements IDestroyable { + private clipStart = 0; + private stopped = true; + private lastCheck = 0; + private observable = new SimpleObservable(); + private timerId: number; + private clipDuration = 0; + + public constructor(private context: AudioContext) { + } + + public get durationSeconds(): number { + return this.clipDuration; + } + + public set durationSeconds(val: number) { + this.clipDuration = val; + this.observable.update([this.timeSeconds, this.clipDuration]); + } + + public get timeSeconds(): number { + return (this.context.currentTime - this.clipStart) % this.clipDuration; + } + + public get liveData(): SimpleObservable { + return this.observable; + } + + private checkTime = () => { + const now = this.timeSeconds; + if (this.lastCheck !== now) { + this.observable.update([now, this.durationSeconds]); + this.lastCheck = now; + } + }; + + public flagStart() { + if (this.stopped) { + this.clipStart = this.context.currentTime; + this.stopped = false; + } + + if (!this.timerId) { + // case to number because the types are wrong + // 100ms interval to make sure the time is as accurate as possible + this.timerId = setInterval(this.checkTime, 100); + } + } + + public flagStop() { + this.stopped = true; + } + + public destroy() { + this.observable.close(); + if (this.timerId) clearInterval(this.timerId); + } +} diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 0c76ac406f..6b0b84ad18 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -24,7 +24,6 @@ import EventEmitter from "events"; import {IDestroyable} from "../utils/IDestroyable"; import {Singleflight} from "../utils/Singleflight"; import {PayloadEvent, WORKLET_NAME} from "./consts"; -import {arrayFastClone} from "../utils/arrays"; import {UPDATE_EVENT} from "../stores/AsyncStore"; import {Playback} from "./Playback"; @@ -59,15 +58,12 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private recording = false; private observable: SimpleObservable; private amplitudes: number[] = []; // at each second mark, generated + private playback: Playback; public constructor(private client: MatrixClient) { super(); } - public get finalWaveform(): number[] { - return arrayFastClone(this.amplitudes); - } - public get contentType(): string { return "audio/ogg"; } @@ -277,12 +273,19 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { }); } - public getPlayback(): Promise { - return Singleflight.for(this, "playback").do(async () => { - const playback = new Playback(this.audioBuffer.buffer); // cast to ArrayBuffer proper - await playback.prepare(); - return playback; + /** + * Gets a playback instance for this voice recording. Note that the playback will not + * have been prepared fully, meaning the `prepare()` function needs to be called on it. + * + * The same playback instance is returned each time. + * + * @returns {Playback} The playback instance. + */ + public getPlayback(): Playback { + this.playback = Singleflight.for(this, "playback").do(() => { + return new Playback(this.audioBuffer.buffer, this.amplitudes); // cast to ArrayBuffer proper; }); + return this.playback; } public destroy() { @@ -290,6 +293,9 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { this.stop(); this.removeAllListeners(); Singleflight.forgetAllFor(this); + // noinspection JSIgnoredPromiseFromCall - not concerned about being called async here + this.playback?.destroy(); + this.observable.close(); } public async upload(): Promise { From e2ce699130b00ca1e37b9738ba0e290fcb40e7f0 Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Wed, 28 Apr 2021 10:02:20 +0530 Subject: [PATCH 306/330] Fixed linting warnings in MessagePanel.js --- src/components/structures/MessagePanel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index adf1874ba0..f669a10267 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -1101,7 +1101,8 @@ class RedactionGrouper { let eventTiles = this.events.map((e, i) => { senders.add(e.sender); const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1]; - return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile); + return panel._getTilesForEvent( + prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { From c4d85c457b084db57b676ce488984d616d8068b5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 22:59:16 -0600 Subject: [PATCH 307/330] Add progress effect to playback waveform --- .../voice_messages/_PlaybackContainer.scss | 8 +++++++- res/themes/legacy-light/css/_legacy-light.scss | 1 + res/themes/light/css/_light.scss | 1 + .../views/voice_messages/PlaybackWaveform.tsx | 15 +++++++++++++-- .../views/voice_messages/Waveform.tsx | 17 ++++++++++++++++- 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/voice_messages/_PlaybackContainer.scss index a9ebc19667..49bd81ef81 100644 --- a/res/css/views/voice_messages/_PlaybackContainer.scss +++ b/res/css/views/voice_messages/_PlaybackContainer.scss @@ -36,7 +36,13 @@ limitations under the License. height: 28px; // default is 30px, so we're subtracting the 2px border off the bars .mx_Waveform_bar { - background-color: $voice-record-waveform-fg-color; + background-color: $voice-record-waveform-incomplete-fg-color; + + &.mx_Waveform_bar_100pct { + // Small animation to remove the mechanical feel of progress + transition: background-color 250ms ease; + background-color: $voice-record-waveform-fg-color; + } } } diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index e05285721e..d7352e5684 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -196,6 +196,7 @@ $voice-record-stop-border-color: #E3E8F0; $voice-record-stop-symbol-color: #ff4b55; $voice-record-waveform-bg-color: #E3E8F0; $voice-record-waveform-fg-color: $muted-fg-color; +$voice-record-waveform-incomplete-fg-color: #C1C6CD; $voice-record-live-circle-color: #ff4b55; $roomtile-preview-color: #9e9e9e; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 342b5dfd9a..20ccc2ee41 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -186,6 +186,7 @@ $voice-record-stop-border-color: #E3E8F0; $voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes $voice-record-waveform-bg-color: #E3E8F0; $voice-record-waveform-fg-color: $muted-fg-color; +$voice-record-waveform-incomplete-fg-color: #C1C6CD; $voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes $roomtile-preview-color: $secondary-fg-color; diff --git a/src/components/views/voice_messages/PlaybackWaveform.tsx b/src/components/views/voice_messages/PlaybackWaveform.tsx index 89de908575..de38de63bb 100644 --- a/src/components/views/voice_messages/PlaybackWaveform.tsx +++ b/src/components/views/voice_messages/PlaybackWaveform.tsx @@ -19,6 +19,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent"; import {arraySeed, arrayTrimFill} from "../../../utils/arrays"; import Waveform from "./Waveform"; import {Playback, PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback"; +import {percentageOf} from "../../../utils/numbers"; interface IProps { playback: Playback; @@ -26,6 +27,7 @@ interface IProps { interface IState { heights: number[]; + progress: number; } /** @@ -36,9 +38,13 @@ export default class PlaybackWaveform extends React.PureComponent { + const progress = percentageOf(time[0], 0, time[1]); + this.setState({progress}); + }; + public render() { - return ; + return ; } } diff --git a/src/components/views/voice_messages/Waveform.tsx b/src/components/views/voice_messages/Waveform.tsx index 5fa68dcadc..840a5a12b3 100644 --- a/src/components/views/voice_messages/Waveform.tsx +++ b/src/components/views/voice_messages/Waveform.tsx @@ -16,9 +16,11 @@ limitations under the License. import React from "react"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import classNames from "classnames"; interface IProps { relHeights: number[]; // relative heights (0-1) + progress: number; // percent complete, 0-1, default 100% } interface IState { @@ -28,9 +30,16 @@ interface IState { * A simple waveform component. This renders bars (centered vertically) for each * height provided in the component properties. Updating the properties will update * the rendered waveform. + * + * For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be + * "filled", as a demonstration of the progress property. */ @replaceableComponent("views.voice_messages.Waveform") export default class Waveform extends React.PureComponent { + public static defaultProps = { + progress: 1, + }; + public constructor(props) { super(props); } @@ -38,7 +47,13 @@ export default class Waveform extends React.PureComponent { public render() { return
    {this.props.relHeights.map((h, i) => { - return ; + const progress = this.props.progress; + const isCompleteBar = (i / this.props.relHeights.length) <= progress && progress > 0; + const classes = classNames({ + 'mx_Waveform_bar': true, + 'mx_Waveform_bar_100pct': isCompleteBar, + }); + return ; })}
    ; } From c2bcdae8a9bb26ab2aa69879c51c37e5ac079a14 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 23:04:49 -0600 Subject: [PATCH 308/330] Switch global var to the store for easier debugging --- src/@types/global.d.ts | 4 ++-- src/stores/VoiceRecordingStore.ts | 2 ++ src/voice/VoiceRecording.ts | 2 -- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 01bb51732e..0ab26ef943 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -39,9 +39,9 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import VoipUserMapper from "../VoipUserMapper"; import {SpaceStoreClass} from "../stores/SpaceStore"; -import {VoiceRecording} from "../voice/VoiceRecording"; import TypingStore from "../stores/TypingStore"; import { EventIndexPeg } from "../indexing/EventIndexPeg"; +import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; declare global { interface Window { @@ -73,7 +73,7 @@ declare global { mxModalWidgetStore: ModalWidgetStore; mxVoipUserMapper: VoipUserMapper; mxSpaceStore: SpaceStoreClass; - mxVoiceRecorder: typeof VoiceRecording; + mxVoiceRecordingStore: VoiceRecordingStore; mxTypingStore: TypingStore; mxEventIndexPeg: EventIndexPeg; } diff --git a/src/stores/VoiceRecordingStore.ts b/src/stores/VoiceRecordingStore.ts index cc999f23f8..8ee44359fb 100644 --- a/src/stores/VoiceRecordingStore.ts +++ b/src/stores/VoiceRecordingStore.ts @@ -78,3 +78,5 @@ export class VoiceRecordingStore extends AsyncStoreWithClient { return this.updateState({recording: null}); } } + +window.mxVoiceRecordingStore = VoiceRecordingStore.instance; diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 6b0b84ad18..3a083a60b1 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -315,5 +315,3 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { return this.mxc; } } - -window.mxVoiceRecorder = VoiceRecording; From 617d74f9cdbf8b6cd4d7613088a2b3dee0e1ad13 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 23:07:45 -0600 Subject: [PATCH 309/330] Treat 119.68 seconds as 1:59 instead of 1:60 --- src/components/views/voice_messages/Clock.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voice_messages/Clock.tsx b/src/components/views/voice_messages/Clock.tsx index 8b71f6b7fe..23e6762c52 100644 --- a/src/components/views/voice_messages/Clock.tsx +++ b/src/components/views/voice_messages/Clock.tsx @@ -42,7 +42,7 @@ export default class Clock extends React.Component { public render() { const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0'); - const seconds = Math.round(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis + const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis return {minutes}:{seconds}; } } From f0ff2fc38d7d43cb09314c5106f9ec88e59a3461 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 23:30:54 -0600 Subject: [PATCH 310/330] Ensure we capture an absolute maximum amount of audio samples We say the limit is 2 minutes, not 1m59s, so let's give the user that last frame. --- src/voice/VoiceRecording.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 3a083a60b1..eb705200ca 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -217,8 +217,19 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { // Now that we've updated the data/waveform, let's do a time check. We don't want to // go horribly over the limit. We also emit a warning state if needed. - const secondsLeft = TARGET_MAX_LENGTH - timeSeconds; - if (secondsLeft <= 0) { + // + // We use the recorder's perspective of time to make sure we don't cut off the last + // frame of audio, otherwise we end up with a 1:59 clip (119.68 seconds). This extra + // safety can allow us to overshoot the target a bit, but at least when we say 2min + // maximum we actually mean it. + // + // In testing, recorder time and worker time lag by about 400ms, which is roughly the + // time needed to encode a sample/frame. + // + // Ref for recorderSeconds: https://github.com/chris-rudmin/opus-recorder#instance-fields + const recorderSeconds = this.recorder.encodedSamplePosition / 48000; + const secondsLeft = TARGET_MAX_LENGTH - recorderSeconds; + if (secondsLeft < 0) { // go over to make sure we definitely capture that last frame // noinspection JSIgnoredPromiseFromCall - we aren't concerned with it overlapping this.stop(); } else if (secondsLeft <= TARGET_WARN_TIME_LEFT) { @@ -253,9 +264,9 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { } // Disconnect the source early to start shutting down resources + await this.recorder.stop(); // stop first to flush the last frame this.recorderSource.disconnect(); this.recorderWorklet.disconnect(); - await this.recorder.stop(); // close the context after the recorder so the recorder doesn't try to // connect anything to the context (this would generate a warning) From 8213c48b7f8e1947803faa8ddee8c0679aaa5ab5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 23:34:26 -0600 Subject: [PATCH 311/330] Fix first waveform bar highlighting in playback at 0% --- src/components/views/voice_messages/PlaybackWaveform.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/voice_messages/PlaybackWaveform.tsx b/src/components/views/voice_messages/PlaybackWaveform.tsx index de38de63bb..123af5dfa5 100644 --- a/src/components/views/voice_messages/PlaybackWaveform.tsx +++ b/src/components/views/voice_messages/PlaybackWaveform.tsx @@ -57,7 +57,8 @@ export default class PlaybackWaveform extends React.PureComponent { - const progress = percentageOf(time[0], 0, time[1]); + // Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar. + const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1)); this.setState({progress}); }; From 8fca32d651c672bd7c26213e6ca879d8b48d2eb7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 23:48:07 -0600 Subject: [PATCH 312/330] Clean up imports from refactoring --- src/components/views/voice_messages/PlayPauseButton.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/voice_messages/PlayPauseButton.tsx b/src/components/views/voice_messages/PlayPauseButton.tsx index b4f69b02bc..2ed0368467 100644 --- a/src/components/views/voice_messages/PlayPauseButton.tsx +++ b/src/components/views/voice_messages/PlayPauseButton.tsx @@ -16,12 +16,10 @@ limitations under the License. import React, {ReactNode} from "react"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {VoiceRecording} from "../../../voice/VoiceRecording"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {_t} from "../../../languageHandler"; import {Playback, PlaybackState} from "../../../voice/Playback"; import classNames from "classnames"; -import {UPDATE_EVENT} from "../../../stores/AsyncStore"; interface IProps { // Playback instance to manipulate. Cannot change during the component lifecycle. From 5966fade0b4875654b597fcaded0141502edd749 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Apr 2021 09:04:02 +0100 Subject: [PATCH 313/330] Fix joining room using via servers regression --- src/stores/RoomViewStore.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 601c77cdf3..8dda310ab4 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -60,6 +60,8 @@ const INITIAL_STATE = { replyingToEvent: null, shouldPeek: false, + + viaServers: [], }; /** @@ -113,6 +115,7 @@ class RoomViewStore extends Store { this.setState({ roomId: null, roomAlias: null, + viaServers: [], }); break; case 'view_room_error': @@ -191,6 +194,7 @@ class RoomViewStore extends Store { replyingToEvent: null, // pull the user out of Room Settings isEditingSettings: false, + viaServers: payload.via_servers, }; // Allow being given an event to be replied to when switching rooms but sanity check its for this room @@ -226,6 +230,7 @@ class RoomViewStore extends Store { roomAlias: payload.room_alias, roomLoading: true, roomLoadError: null, + viaServers: payload.via_servers, }); try { const result = await MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias); @@ -261,6 +266,7 @@ class RoomViewStore extends Store { roomAlias: payload.room_alias, roomLoading: false, roomLoadError: payload.err, + viaServers: [], }); } @@ -273,8 +279,9 @@ class RoomViewStore extends Store { const cli = MatrixClientPeg.get(); const address = this.state.roomAlias || this.state.roomId; try { + const viaServers = this.state.viaServers || []; await retry(() => cli.joinRoom(address, { - viaServers: payload.via_servers, + viaServers, ...payload.opts, }), NUM_JOIN_RETRY, (err) => { // if we received a Gateway timeout then retry From 27731ac25bacee066284f5e9fff6654f769f952b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Apr 2021 09:07:02 +0100 Subject: [PATCH 314/330] tidy --- src/stores/RoomViewStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 8dda310ab4..a5bdb7ef33 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -278,8 +278,8 @@ class RoomViewStore extends Store { const cli = MatrixClientPeg.get(); const address = this.state.roomAlias || this.state.roomId; + const viaServers = this.state.viaServers || []; try { - const viaServers = this.state.viaServers || []; await retry(() => cli.joinRoom(address, { viaServers, ...payload.opts, From 69f797eda4cf384e89e6e6fe521065f6958a6403 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 28 Apr 2021 14:17:57 +0100 Subject: [PATCH 315/330] Add test coverage collection script This makes it clear to how collect basic test coverage when desired. --- package.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 39c3b68103..e54de8a96c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", - "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080" + "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080", + "coverage": "yarn test --coverage" }, "dependencies": { "@babel/runtime": "^7.12.5", @@ -188,6 +189,12 @@ }, "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" + ], + "collectCoverageFrom": [ + "/src/**/*.{js,ts,tsx}" + ], + "coverageReporters": [ + "text" ] } } From d4acd0e41ceddf74b7123df07def785ed5b48f73 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 28 Apr 2021 09:28:46 -0600 Subject: [PATCH 316/330] Remove excess IState --- src/components/views/voice_messages/PlayPauseButton.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/voice_messages/PlayPauseButton.tsx b/src/components/views/voice_messages/PlayPauseButton.tsx index 2ed0368467..1f87eb012d 100644 --- a/src/components/views/voice_messages/PlayPauseButton.tsx +++ b/src/components/views/voice_messages/PlayPauseButton.tsx @@ -29,18 +29,14 @@ interface IProps { playbackPhase: PlaybackState; } -interface IState { -} - /** * Displays a play/pause button (activating the play/pause function of the recorder) * to be displayed in reference to a recording. */ @replaceableComponent("views.voice_messages.PlayPauseButton") -export default class PlayPauseButton extends React.PureComponent { +export default class PlayPauseButton extends React.PureComponent { public constructor(props) { super(props); - this.state = {}; } private onClick = async () => { From 6764b8d645df5cdac8977836f3386dd8fbf5bad5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 28 Apr 2021 09:29:31 -0600 Subject: [PATCH 317/330] Change symbol names --- res/css/views/voice_messages/_PlayPauseButton.scss | 4 ++-- res/img/element-icons/{pause-symbol.svg => pause.svg} | 0 res/img/element-icons/{play-symbol.svg => play.svg} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename res/img/element-icons/{pause-symbol.svg => pause.svg} (100%) rename res/img/element-icons/{play-symbol.svg => play.svg} (100%) diff --git a/res/css/views/voice_messages/_PlayPauseButton.scss b/res/css/views/voice_messages/_PlayPauseButton.scss index 0fd31be28a..c8ab162694 100644 --- a/res/css/views/voice_messages/_PlayPauseButton.scss +++ b/res/css/views/voice_messages/_PlayPauseButton.scss @@ -38,7 +38,7 @@ limitations under the License. height: 16px; top: 8px; // center left: 12px; // center - mask-image: url('$(res)/img/element-icons/play-symbol.svg'); + mask-image: url('$(res)/img/element-icons/play.svg'); } &.mx_PlayPauseButton_pause::before { @@ -46,6 +46,6 @@ limitations under the License. height: 12px; top: 10px; // center left: 11px; // center - mask-image: url('$(res)/img/element-icons/pause-symbol.svg'); + mask-image: url('$(res)/img/element-icons/pause.svg'); } } diff --git a/res/img/element-icons/pause-symbol.svg b/res/img/element-icons/pause.svg similarity index 100% rename from res/img/element-icons/pause-symbol.svg rename to res/img/element-icons/pause.svg diff --git a/res/img/element-icons/play-symbol.svg b/res/img/element-icons/play.svg similarity index 100% rename from res/img/element-icons/play-symbol.svg rename to res/img/element-icons/play.svg From b90c845fcb837e986f97bb015f22af11f26817e7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 28 Apr 2021 10:07:22 -0600 Subject: [PATCH 318/330] Revert "Fixes the two Todays problem in Redaction" --- src/components/structures/MessagePanel.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index f669a10267..132d9ab4c3 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -562,7 +562,7 @@ export default class MessagePanel extends React.Component { return ret; } - _getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) { + _getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) { const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary'); const EventTile = sdk.getComponent('rooms.EventTile'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); @@ -582,7 +582,7 @@ export default class MessagePanel extends React.Component { // do we need a date separator since the last event? const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate); - if (wantsDateSeparator && !isGrouped) { + if (wantsDateSeparator) { const dateSeparator =
  • ; ret.push(dateSeparator); } @@ -966,6 +966,7 @@ class CreationGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); + const panel = this.panel; const ret = []; const createEvent = this.createEvent; @@ -981,7 +982,7 @@ class CreationGrouper { // If this m.room.create event should be shown (room upgrade) then show it before the summary if (panel._shouldShowEvent(createEvent)) { // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered - ret.push(...panel._getTilesForEvent(createEvent, createEvent)); + ret.push(...panel._getTilesForEvent(createEvent, createEvent, false)); } for (const ejected of this.ejectedEvents) { @@ -1080,7 +1081,7 @@ class RedactionGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); - const isGrouped=true; + const panel = this.panel; const ret = []; const lastShownEvent = this.lastShownEvent; @@ -1097,12 +1098,10 @@ class RedactionGrouper { ); const senders = new Set(); - let eventTiles = this.events.map((e, i) => { senders.add(e.sender); const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1]; - return panel._getTilesForEvent( - prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile); + return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { @@ -1181,7 +1180,7 @@ class MemberGrouper { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); - const isGrouped=true; + const panel = this.panel; const lastShownEvent = this.lastShownEvent; const ret = []; @@ -1214,7 +1213,7 @@ class MemberGrouper { // of MemberEventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeparator is inserted. - return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped); + return panel._getTilesForEvent(e, e, e === lastShownEvent); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { From e0bcccd6003cfab277cff644fb0c369bb6afa2ba Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 28 Apr 2021 17:17:43 +0100 Subject: [PATCH 319/330] Ignore possible coverage output --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e1dd7726e1..50aa10fbfd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /*.log package-lock.json +/coverage /node_modules /lib From bbce1ac7043ca9aa632b9e85d34e2c48c0ee2854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20M=C3=A4der?= Date: Wed, 28 Apr 2021 19:39:38 +0200 Subject: [PATCH 320/330] Disallow inline display maths --- src/editor/serialize.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index 5167e3d376..7cf9d9bb9d 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -61,9 +61,9 @@ export function htmlSerializeIfNeeded(model: EditorModel, {forceHTML = false} = // const inlinePattern = "(?:^|\\s)(? Date: Wed, 28 Apr 2021 23:18:42 +0100 Subject: [PATCH 321/330] Make the text filter search all spaces instead of just the selected one --- src/stores/room-list/RoomListStore.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index caab46a0c2..6e9216423a 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -601,7 +601,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient { let rooms = this.matrixClient.getVisibleRooms().filter(r => VisibilityProvider.instance.isRoomVisible(r)); - if (this.prefilterConditions.length > 0) { + // if spaces are enabled only consider the prefilter conditions when there are no runtime conditions + // for the search all spaces feature + if (this.prefilterConditions.length > 0 + && (!SettingsStore.getValue("feature_spaces") || !this.filterConditions.length) + ) { rooms = rooms.filter(r => { for (const filter of this.prefilterConditions) { if (!filter.isVisible(r)) { @@ -675,6 +679,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient { if (this.algorithm) { this.algorithm.addFilterCondition(filter); } + // Runtime filters with spaces disable prefiltering for the search all spaces effect + if (SettingsStore.getValue("feature_spaces")) { + promise = this.recalculatePrefiltering(); + } } promise.then(() => this.updateFn.trigger()); } @@ -698,6 +706,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient { if (this.algorithm) { this.algorithm.removeFilterCondition(filter); + // Runtime filters with spaces disable prefiltering for the search all spaces effect + if (SettingsStore.getValue("feature_spaces")) { + promise = this.recalculatePrefiltering(); + } } } idx = this.prefilterConditions.indexOf(filter); From e390c3c732d43fce284bfe4cdf6741d92f2da1cf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 29 Apr 2021 09:37:21 +0100 Subject: [PATCH 322/330] Inhibit sending RR when context switching to a room --- src/components/structures/RoomView.tsx | 5 +++++ src/components/structures/TimelinePanel.js | 6 +++++- src/stores/RoomViewStore.tsx | 12 +++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 7168b7d139..5108643673 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -190,6 +190,9 @@ export interface IState { rejectError?: Error; hasPinnedWidgets?: boolean; dragCounter: number; + // whether or not a spaces context switch brought us here, + // if it did we don't want the room to be marked as read as soon as it is loaded. + wasContextSwitch?: boolean; } @replaceableComponent("structures.RoomView") @@ -326,6 +329,7 @@ export default class RoomView extends React.Component { shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), + wasContextSwitch: RoomViewStore.getWasContextSwitch(), }; if (!initial && this.state.shouldPeek && !newState.shouldPeek) { @@ -2014,6 +2018,7 @@ export default class RoomView extends React.Component { timelineSet={this.state.room.getUnfilteredTimelineSet()} showReadReceipts={this.state.showReadReceipts} manageReadReceipts={!this.state.isPeeking} + sendReadReceiptOnLoad={!this.state.wasContextSwitch} manageReadMarkers={!this.state.isPeeking} hidden={hideMessagePanel} highlightedEventId={highlightedEventId} diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 12f5d6e890..755a4e7784 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -68,6 +68,7 @@ class TimelinePanel extends React.Component { showReadReceipts: PropTypes.bool, // Enable managing RRs and RMs. These require the timelineSet to have a room. manageReadReceipts: PropTypes.bool, + sendReadReceiptOnLoad: PropTypes.bool, manageReadMarkers: PropTypes.bool, // true to give the component a 'display: none' style. @@ -126,6 +127,7 @@ class TimelinePanel extends React.Component { // event tile heights. (See _unpaginateEvents) timelineCap: Number.MAX_VALUE, className: 'mx_RoomView_messagePanel', + sendReadReceiptOnLoad: true, }; constructor(props) { @@ -1049,7 +1051,9 @@ class TimelinePanel extends React.Component { this._messagePanel.current.scrollToBottom(); } - this.sendReadReceipt(); + if (this.props.sendReadReceiptOnLoad) { + this.sendReadReceipt(); + } }); }; diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index a5bdb7ef33..fe2e0a66b2 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -62,6 +62,8 @@ const INITIAL_STATE = { shouldPeek: false, viaServers: [], + + wasContextSwitch: false, }; /** @@ -116,6 +118,7 @@ class RoomViewStore extends Store { roomId: null, roomAlias: null, viaServers: [], + wasContextSwitch: false, }); break; case 'view_room_error': @@ -195,6 +198,7 @@ class RoomViewStore extends Store { // pull the user out of Room Settings isEditingSettings: false, viaServers: payload.via_servers, + wasContextSwitch: payload.context_switch, }; // Allow being given an event to be replied to when switching rooms but sanity check its for this room @@ -231,6 +235,7 @@ class RoomViewStore extends Store { roomLoading: true, roomLoadError: null, viaServers: payload.via_servers, + wasContextSwitch: payload.context_switch, }); try { const result = await MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias); @@ -256,6 +261,8 @@ class RoomViewStore extends Store { room_alias: payload.room_alias, auto_join: payload.auto_join, oob_data: payload.oob_data, + viaServers: payload.via_servers, + wasContextSwitch: payload.context_switch, }); } } @@ -266,7 +273,6 @@ class RoomViewStore extends Store { roomAlias: payload.room_alias, roomLoading: false, roomLoadError: payload.err, - viaServers: [], }); } @@ -426,6 +432,10 @@ class RoomViewStore extends Store { public shouldPeek() { return this.state.shouldPeek; } + + public getWasContextSwitch() { + return this.state.wasContextSwitch; + } } let singletonRoomViewStore = null; From 62198601d27ad59f847b2e03b980cd120150a299 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 29 Apr 2021 16:40:08 +0100 Subject: [PATCH 323/330] Tweak room list filter placeholder and results copy for spaces --- src/components/structures/RoomSearch.tsx | 21 +++++++++++++++++-- .../views/rooms/RoomListNumResults.tsx | 6 +++++- src/i18n/strings/en_EN.json | 3 +++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index a64feed42c..586a0825dd 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -17,6 +17,8 @@ limitations under the License. import * as React from "react"; import { createRef } from "react"; import classNames from "classnames"; +import { Room } from "matrix-js-sdk/src/models/room"; + import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import { ActionPayload } from "../../dispatcher/payloads"; @@ -26,7 +28,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import {replaceableComponent} from "../../utils/replaceableComponent"; -import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; +import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore"; interface IProps { isMinimized: boolean; @@ -40,6 +42,7 @@ interface IProps { interface IState { query: string; focused: boolean; + inSpaces: boolean; } @replaceableComponent("structures.RoomSearch") @@ -54,11 +57,13 @@ export default class RoomSearch extends React.PureComponent { this.state = { query: "", focused: false, + inSpaces: false, }; this.dispatcherRef = defaultDispatcher.register(this.onAction); // clear filter when changing spaces, in future we may wish to maintain a filter per-space SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput); + SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces); } public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { @@ -79,8 +84,15 @@ export default class RoomSearch extends React.PureComponent { public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput); + SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces); } + private onSpaces = (spaces: Room[]) => { + this.setState({ + inSpaces: spaces.length > 0, + }); + }; + private onAction = (payload: ActionPayload) => { if (payload.action === 'view_room' && payload.clear_search) { this.clearInput(); @@ -152,6 +164,11 @@ export default class RoomSearch extends React.PureComponent { 'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused, }); + let placeholder = _t("Filter"); + if (SpaceStore.instance.spacePanelSpaces.length) { + placeholder = _t("Filter all spaces"); + } + let icon = (
    ); @@ -165,7 +182,7 @@ export default class RoomSearch extends React.PureComponent { onBlur={this.onBlur} onChange={this.onChange} onKeyDown={this.onKeyDown} - placeholder={_t("Filter")} + placeholder={placeholder} autoComplete="off" /> ); diff --git a/src/components/views/rooms/RoomListNumResults.tsx b/src/components/views/rooms/RoomListNumResults.tsx index fcac91a56a..01cbecf05d 100644 --- a/src/components/views/rooms/RoomListNumResults.tsx +++ b/src/components/views/rooms/RoomListNumResults.tsx @@ -19,6 +19,7 @@ import React, {useState} from "react"; import { _t } from "../../../languageHandler"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import SpaceStore from "../../../stores/SpaceStore"; const RoomListNumResults: React.FC = () => { const [count, setCount] = useState(null); @@ -34,7 +35,10 @@ const RoomListNumResults: React.FC = () => { if (typeof count !== "number") return null; return
    - {_t("%(count)s results", { count })} + { SpaceStore.instance.spacePanelSpaces.length + ? _t("%(count)s results in all spaces", { count }) + : _t("%(count)s results", { count }) + }
    ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 85e8e54258..5863f2a834 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1552,6 +1552,8 @@ "Explore all public rooms": "Explore all public rooms", "Quick actions": "Quick actions", "Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below", + "%(count)s results in all spaces|other": "%(count)s results in all spaces", + "%(count)s results in all spaces|one": "%(count)s result in all spaces", "%(count)s results|other": "%(count)s results", "%(count)s results|one": "%(count)s result", "This room": "This room", @@ -2612,6 +2614,7 @@ "If you can't find the room you're looking for, ask for an invite or Create a new room.": "If you can't find the room you're looking for, ask for an invite or Create a new room.", "Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s", "Filter": "Filter", + "Filter all spaces": "Filter all spaces", "Clear filter": "Clear filter", "Filter rooms and people": "Filter rooms and people", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", From 73abe51fb93bf8b87a12f8a76d554710301358d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 29 Apr 2021 16:46:21 +0100 Subject: [PATCH 324/330] actually use the new state --- src/components/structures/RoomSearch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 586a0825dd..34682877e0 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -165,7 +165,7 @@ export default class RoomSearch extends React.PureComponent { }); let placeholder = _t("Filter"); - if (SpaceStore.instance.spacePanelSpaces.length) { + if (this.state.inSpaces) { placeholder = _t("Filter all spaces"); } From 70204d6111bef4d640d2b1980f531bd5039c95e9 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Thu, 29 Apr 2021 22:41:57 +0530 Subject: [PATCH 325/330] Prevent peeking members from reacting Signed-off-by: Jaiwanth --- src/components/views/messages/ReactionsRowButton.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js index 06421c02a2..b37a949e57 100644 --- a/src/components/views/messages/ReactionsRowButton.js +++ b/src/components/views/messages/ReactionsRowButton.js @@ -129,12 +129,13 @@ export default class ReactionsRowButton extends React.PureComponent { }, ); } - + const isPeeking = room.getMyMembership() !== "join"; const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return From 232b87a3b41fb3937e938d79e1c4af88b7f0be99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 29 Apr 2021 19:57:02 +0200 Subject: [PATCH 326/330] Improve formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/GroupAddressPicker.js | 16 +-- src/TextForEvent.js | 18 ++-- src/components/structures/GroupView.js | 79 +++++++------- src/components/structures/ScrollPanel.js | 27 ++--- .../views/dialogs/RoomSettingsDialog.js | 8 +- .../views/dialogs/StorageEvictedDialog.js | 5 +- .../views/dialogs/UserSettingsDialog.js | 8 +- .../dialogs/WidgetOpenIDPermissionsDialog.js | 7 +- .../ConfirmDestroyCrossSigningDialog.js | 3 +- .../security/RestoreKeyBackupDialog.js | 51 +++++---- .../views/elements/EditableItemList.js | 8 +- .../messages/MKeyVerificationConclusion.js | 4 +- .../views/rooms/ReadReceiptMarker.js | 3 +- src/indexing/EventIndex.js | 10 +- src/stores/CustomRoomTagStore.js | 4 +- src/utils/MegolmExportEncryption.js | 3 +- .../elements/MemberEventListSummary-test.js | 102 +++++++++--------- 17 files changed, 193 insertions(+), 163 deletions(-) diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 58b65769ae..d956189f0d 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -148,13 +148,15 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog( 'Failed to add the following room to the group', - '', ErrorDialog, + '', + ErrorDialog, { - title: _t( - "Failed to add the following rooms to %(groupId)s:", - {groupId}, - ), - description: errorList.join(", "), - }); + title: _t( + "Failed to add the following rooms to %(groupId)s:", + {groupId}, + ), + description: errorList.join(", "), + }, + ); }); } diff --git a/src/TextForEvent.js b/src/TextForEvent.js index fd7b4fd18f..86f9ff20f4 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -547,17 +547,23 @@ function textForMjolnirEvent(event) { // else the entity !== prevEntity - count as a removal & add if (USER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + + return _t( + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}, + ); } else if (ROOM_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + + return _t( + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}, + ); } else if (SERVER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + + return _t( + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}, + ); } // Unknown type. We'll say something but we shouldn't end up here. diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index ef74499473..c17bae9d49 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -44,14 +44,14 @@ import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( `

    HTML for your community's page

    -

    - Use the long description to introduce new members to the community, or distribute - some important links -

    -

    - You can even add images with Matrix URLs -

    -`); +

    + Use the long description to introduce new members to the community, or distribute + some important links +

    +

    + You can even add images with Matrix URLs +

    `, +); const RoomSummaryType = PropTypes.shape({ room_id: PropTypes.string.isRequired, @@ -110,13 +110,14 @@ class CategoryRoomList extends React.Component { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog( 'Failed to add the following room to the group summary', - '', ErrorDialog, + '', + ErrorDialog, { - title: _t( - "Failed to add the following rooms to the summary of %(groupId)s:", - {groupId: this.props.groupId}, - ), - description: errorList.join(", "), + title: _t( + "Failed to add the following rooms to the summary of %(groupId)s:", + {groupId: this.props.groupId}, + ), + description: errorList.join(", "), }, ); }); @@ -146,8 +147,7 @@ class CategoryRoomList extends React.Component { let catHeader =
    ; if (this.props.category && this.props.category.profile) { - catHeader =
    + catHeader =
    { this.props.category.profile.name }
    ; } @@ -193,11 +193,11 @@ class FeaturedRoom extends React.Component { 'Failed to remove room from group summary', '', ErrorDialog, { - title: _t( - "Failed to remove the room from the summary of %(groupId)s", - {groupId: this.props.groupId}, - ), - description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}), + title: _t( + "Failed to remove the room from the summary of %(groupId)s", + {groupId: this.props.groupId}, + ), + description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}), }, ); }); @@ -287,12 +287,13 @@ class RoleUserList extends React.Component { 'Failed to add the following users to the community summary', '', ErrorDialog, { - title: _t( - "Failed to add the following users to the summary of %(groupId)s:", - {groupId: this.props.groupId}, - ), - description: errorList.join(", "), - }); + title: _t( + "Failed to add the following users to the summary of %(groupId)s:", + {groupId: this.props.groupId}, + ), + description: errorList.join(", "), + }, + ); }); }, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -355,13 +356,14 @@ class FeaturedUser extends React.Component { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog( 'Failed to remove user from community summary', - '', ErrorDialog, + '', + ErrorDialog, { - title: _t( - "Failed to remove a user from the summary of %(groupId)s", - {groupId: this.props.groupId}, - ), - description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}), + title: _t( + "Failed to remove a user from the summary of %(groupId)s", + {groupId: this.props.groupId}, + ), + description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}), }, ); }); @@ -1059,11 +1061,12 @@ export default class GroupView extends React.Component { return null; } - const membershipButtonClasses = classnames([ - 'mx_RoomHeader_textButton', - 'mx_GroupView_textButton', - ], - membershipButtonExtraClasses, + const membershipButtonClasses = classnames( + [ + 'mx_RoomHeader_textButton', + 'mx_GroupView_textButton', + ], + membershipButtonExtraClasses, ); const membershipContainerClasses = classnames( diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 93a4c29b81..d423d4413e 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -884,19 +884,20 @@ export default class ScrollPanel extends React.Component { // give the
      an explicit role=list because Safari+VoiceOver seems to think an ordered-list with // list-style-type: none; is no longer a list - return ( - { this.props.fixedChildren } -
      -
        - { this.props.children } -
      -
      -
      + return ( + + { this.props.fixedChildren } +
      +
        + { this.props.children } +
      +
      +
      ); } } diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index c052b5c5bb..b6c4d42243 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -116,8 +116,12 @@ export default class RoomSettingsDialog extends React.Component { const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name; return ( - +
      diff --git a/src/components/views/dialogs/StorageEvictedDialog.js b/src/components/views/dialogs/StorageEvictedDialog.js index 629990032f..1e17ab1738 100644 --- a/src/components/views/dialogs/StorageEvictedDialog.js +++ b/src/components/views/dialogs/StorageEvictedDialog.js @@ -45,9 +45,10 @@ export default class StorageEvictedDialog extends React.Component { let logRequest; if (SdkConfig.get().bug_report_endpoint_url) { logRequest = _t( - "To help us prevent this in future, please send us logs.", {}, + "To help us prevent this in future, please send us logs.", + {}, { - a: text => {text}, + a: text => {text}, }, ); } diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 8d99ffb5cd..e7f6953589 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -155,8 +155,12 @@ export default class UserSettingsDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( - +
      diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js index bf3671a0fe..f77661c54e 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js @@ -70,9 +70,12 @@ export default class WidgetOpenIDPermissionsDialog extends React.Component { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( - + title={_t("Allow this widget to verify your identity")} + >

      {_t("The widget will verify your user ID, but won't be able to perform actions for you:")} diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js index dabd7950b4..e71983b074 100644 --- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js +++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js @@ -43,7 +43,8 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component { className='mx_ConfirmDestroyCrossSigningDialog' hasCancel={true} onFinished={this.props.onFinished} - title={_t("Destroy cross-signing keys?")}> + title={_t("Destroy cross-signing keys?")} + >

      {_t( diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js index faabbacb81..4ac15ab5a3 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js @@ -373,20 +373,23 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { {_t( "If you've forgotten your Security Phrase you can "+ "use your Security Key or " + - "set up new recovery options" - , {}, { - button1: s => - {s} - , - button2: s => - {s} - , + "set up new recovery options", + {}, + { + button1: s => + {s} + , + button2: s => + {s} + , })}

      ; } else { @@ -435,15 +438,17 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
      {_t( "If you've forgotten your Security Key you can "+ - "" - , {}, { - button: s => - {s} - , - })} + "", + {}, + { + button: s => + {s} + , + }, + )}
    ; } diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js index e3fe54333a..d8ec5af278 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.js @@ -127,8 +127,12 @@ export default class EditableItemList extends React.Component { _renderNewItemField() { return ( -
    + diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js index 7fc5a62434..c9489711aa 100644 --- a/src/components/views/messages/MKeyVerificationConclusion.js +++ b/src/components/views/messages/MKeyVerificationConclusion.js @@ -82,9 +82,7 @@ export default class MKeyVerificationConclusion extends React.Component { } // User isn't actually verified - if (!MatrixClientPeg.get() - .checkUserTrust(request.otherUserId) - .isCrossSigningVerified()) { + if (!MatrixClientPeg.get().checkUserTrust(request.otherUserId).isCrossSigningVerified()) { return false; } diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 4d01c38196..64f2c1160b 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -187,8 +187,7 @@ export default class ReadReceiptMarker extends React.PureComponent { } return ( - + } Resolves to true if events were added to the * timeline, false otherwise. */ - async populateFileTimeline(timelineSet, timeline, room, limit = 10, - fromEvent = null, direction = EventTimeline.BACKWARDS) { + async populateFileTimeline( + timelineSet, + timeline, + room, + limit = 10, + fromEvent = null, + direction = EventTimeline.BACKWARDS, + ) { const matrixEvents = await this.loadFileEvents(room, limit, fromEvent, direction); // If this is a normal fill request, not a pagination request, we need diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index 060f1f3749..55c9699f7a 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -124,15 +124,15 @@ class CustomRoomTagStore extends EventEmitter { const tags = Object.assign({}, oldTags, tag); this._setState({tags}); } - } break; + } case 'on_client_not_viable': case 'on_logged_out': { // we assume to always have a tags object in the state this._state = {tags: {}}; RoomListStore.instance.off(LISTS_UPDATE_EVENT, this._onListsUpdated); - } break; + } } } diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.js index 20f3cd6cb6..6f5c7104b1 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.js @@ -310,8 +310,7 @@ function unpackMegolmKeyFile(data) { // look for the end line while (1) { const lineEnd = fileStr.indexOf('\n', lineStart); - const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd) - .trim(); + const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim(); if (line === TRAILER_LINE) { break; } diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index 9386d8cf4a..95bf206d02 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -245,9 +245,8 @@ describe('MemberEventListSummary', function() { ); }); - it('truncates multiple sequences of repetitions with other events between', - function() { - const events = generateEvents([ + it('truncates multiple sequences of repetitions with other events between', function() { + const events = generateEvents([ { userId: "@user_1:some.domain", prevMembership: "ban", @@ -276,29 +275,28 @@ describe('MemberEventListSummary', function() { membership: "invite", senderId: "@some_other_user:some.domain", }, - ]); - const props = { + ]); + const props = { events: events, children: generateTiles(events), summaryLength: 1, avatarsMaxLength: 5, threshold: 3, - }; + }; - const instance = ReactTestUtils.renderIntoDocument( - , - ); - const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_EventListSummary_summary", - ); - const summaryText = summary.textContent; + const instance = ReactTestUtils.renderIntoDocument( + , + ); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, "mx_EventListSummary_summary", + ); + const summaryText = summary.textContent; - expect(summaryText).toBe( - "user_1 was unbanned, joined and left 2 times, was banned, " + + expect(summaryText).toBe( + "user_1 was unbanned, joined and left 2 times, was banned, " + "joined and left 3 times and was invited", - ); - }, - ); + ); + }); it('handles multiple users following the same sequence of memberships', function() { const events = generateEvents([ @@ -396,9 +394,8 @@ describe('MemberEventListSummary', function() { ); }); - it('correctly orders sequences of transitions by the order of their first event', - function() { - const events = generateEvents([ + it('correctly orders sequences of transitions by the order of their first event', function() { + const events = generateEvents([ { userId: "@user_2:some.domain", prevMembership: "ban", @@ -425,29 +422,28 @@ describe('MemberEventListSummary', function() { {userId: "@user_2:some.domain", prevMembership: "join", membership: "leave"}, {userId: "@user_2:some.domain", prevMembership: "leave", membership: "join"}, {userId: "@user_2:some.domain", prevMembership: "join", membership: "leave"}, - ]); - const props = { + ]); + const props = { events: events, children: generateTiles(events), summaryLength: 1, avatarsMaxLength: 5, threshold: 3, - }; + }; - const instance = ReactTestUtils.renderIntoDocument( - , - ); - const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_EventListSummary_summary", - ); - const summaryText = summary.textContent; + const instance = ReactTestUtils.renderIntoDocument( + , + ); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, "mx_EventListSummary_summary", + ); + const summaryText = summary.textContent; - expect(summaryText).toBe( - "user_2 was unbanned and joined and left 2 times, user_1 was unbanned, " + + expect(summaryText).toBe( + "user_2 was unbanned and joined and left 2 times, user_1 was unbanned, " + "joined and left 2 times and was banned", - ); - }, - ); + ); + }); it('correctly identifies transitions', function() { const events = generateEvents([ @@ -570,9 +566,8 @@ describe('MemberEventListSummary', function() { ); }); - it('handles invitation plurals correctly when there are multiple invites', - function() { - const events = generateEvents([ + it('handles invitation plurals correctly when there are multiple invites', function() { + const events = generateEvents([ { userId: "@user_1:some.domain", prevMembership: "invite", @@ -583,28 +578,27 @@ describe('MemberEventListSummary', function() { prevMembership: "invite", membership: "leave", }, - ]); - const props = { + ]); + const props = { events: events, children: generateTiles(events), summaryLength: 1, avatarsMaxLength: 5, threshold: 1, // threshold = 1 to force collapse - }; + }; - const instance = ReactTestUtils.renderIntoDocument( - , - ); - const summary = ReactTestUtils.findRenderedDOMComponentWithClass( - instance, "mx_EventListSummary_summary", - ); - const summaryText = summary.textContent; + const instance = ReactTestUtils.renderIntoDocument( + , + ); + const summary = ReactTestUtils.findRenderedDOMComponentWithClass( + instance, "mx_EventListSummary_summary", + ); + const summaryText = summary.textContent; - expect(summaryText).toBe( - "user_1 rejected their invitation 2 times", - ); - }, - ); + expect(summaryText).toBe( + "user_1 rejected their invitation 2 times", + ); + }); it('handles a summary length = 2, with no "others"', function() { const events = generateEvents([ From c6bd2c7d674e16e19c5a03cd93019604ea650d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 29 Apr 2021 20:18:26 +0200 Subject: [PATCH 327/330] Fix some more formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/GroupView.js | 16 ++++++++-------- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index c17bae9d49..ce5cd65c22 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -43,14 +43,14 @@ import {mediaFromMxc} from "../../customisations/Media"; import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( - `

    HTML for your community's page

    -

    - Use the long description to introduce new members to the community, or distribute - some important links -

    -

    - You can even add images with Matrix URLs -

    `, + "

    HTML for your community's page

    " + + "

    " + + "Use the long description to introduce new members to the community, or distribute" + + "some important links" + + "

    " + + "

    " + + "You can even add images with Matrix URLs " + + "

    ", ); const RoomSummaryType = PropTypes.shape({ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 85e8e54258..0274e05bcf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2508,7 +2508,7 @@ "Attach files from chat or just drag and drop them anywhere in a room.": "Attach files from chat or just drag and drop them anywhere in a room.", "Communities": "Communities", "Create community": "Create community", - "

    HTML for your community's page

    \n

    \n Use the long description to introduce new members to the community, or distribute\n some important links\n

    \n

    \n You can even add images with Matrix URLs \n

    \n": "

    HTML for your community's page

    \n

    \n Use the long description to introduce new members to the community, or distribute\n some important links\n

    \n

    \n You can even add images with Matrix URLs \n

    \n", + "

    HTML for your community's page

    Use the long description to introduce new members to the community, or distributesome important links

    You can even add images with Matrix URLs

    ": "

    HTML for your community's page

    Use the long description to introduce new members to the community, or distributesome important links

    You can even add images with Matrix URLs

    ", "Add rooms to the community summary": "Add rooms to the community summary", "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", "Add to summary": "Add to summary", From f766f985e43d6ef8914529f4457c64b80a8b33bd Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Fri, 30 Apr 2021 08:25:58 +0530 Subject: [PATCH 328/330] Change cursor to not-allowed --- res/css/views/messages/_ReactionsRowButton.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/messages/_ReactionsRowButton.scss b/res/css/views/messages/_ReactionsRowButton.scss index 7158ffc027..c132fa5a0f 100644 --- a/res/css/views/messages/_ReactionsRowButton.scss +++ b/res/css/views/messages/_ReactionsRowButton.scss @@ -34,6 +34,10 @@ limitations under the License. border-color: $reaction-row-button-selected-border-color; } + &.mx_AccessibleButton_disabled { + cursor: not-allowed; + } + .mx_ReactionsRowButton_content { max-width: 100px; overflow: hidden; From 2e62b1861766f09e6208a86c4e28879204cd845a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 12:30:14 +0200 Subject: [PATCH 329/330] Revert some changes to avoid re-translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/GroupView.js | 18 +++++++++--------- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index ce5cd65c22..3ab009d7b8 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -43,15 +43,15 @@ import {mediaFromMxc} from "../../customisations/Media"; import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( - "

    HTML for your community's page

    " + - "

    " + - "Use the long description to introduce new members to the community, or distribute" + - "some important links" + - "

    " + - "

    " + - "You can even add images with Matrix URLs " + - "

    ", -); + `

    HTML for your community's page

    +

    + Use the long description to introduce new members to the community, or distribute + some important links +

    +

    + You can even add images with Matrix URLs +

    +`); const RoomSummaryType = PropTypes.shape({ room_id: PropTypes.string.isRequired, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0274e05bcf..85e8e54258 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2508,7 +2508,7 @@ "Attach files from chat or just drag and drop them anywhere in a room.": "Attach files from chat or just drag and drop them anywhere in a room.", "Communities": "Communities", "Create community": "Create community", - "

    HTML for your community's page

    Use the long description to introduce new members to the community, or distributesome important links

    You can even add images with Matrix URLs

    ": "

    HTML for your community's page

    Use the long description to introduce new members to the community, or distributesome important links

    You can even add images with Matrix URLs

    ", + "

    HTML for your community's page

    \n

    \n Use the long description to introduce new members to the community, or distribute\n some important links\n

    \n

    \n You can even add images with Matrix URLs \n

    \n": "

    HTML for your community's page

    \n

    \n Use the long description to introduce new members to the community, or distribute\n some important links\n

    \n

    \n You can even add images with Matrix URLs \n

    \n", "Add rooms to the community summary": "Add rooms to the community summary", "Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?", "Add to summary": "Add to summary", From 43410835a3ff1db8fe7b627c27df75fa06374b5a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 30 Apr 2021 11:53:56 +0100 Subject: [PATCH 330/330] Prevent room list keyboard handling from landing focus on hidden nodes --- src/components/structures/LeftPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index e4762e35ad..44b404bd3a 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -347,7 +347,7 @@ export default class LeftPanel extends React.Component { if (element) { classes = element.classList; } - } while (element && !cssClasses.some(c => classes.contains(c))); + } while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null)); if (element) { element.focus();