From e33f4ba9c0e8c29cf074f566cbb69c27007f6c57 Mon Sep 17 00:00:00 2001 From: Resynth Date: Sun, 25 Oct 2020 22:04:39 +0000 Subject: [PATCH 001/283] Warn on Access Token reveal Signed-off-by: Resynth --- .../views/dialogs/AccessTokenDialog.tsx | 39 +++++++++++++++++++ .../settings/tabs/user/HelpUserSettingsTab.js | 14 ++++++- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/components/views/dialogs/AccessTokenDialog.tsx diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx new file mode 100644 index 0000000000..81c48f219a --- /dev/null +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -0,0 +1,39 @@ + /* +Copyright 2017 Vector Creations 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. +*/ + +import React from 'react'; +import { _t } from '../../../languageHandler'; +import QuestionDialog from './QuestionDialog'; + +type IProps = Exclude< + React.ComponentProps, + "title" | "danger" | "description" + >; + +export default function AccessTokenDialog (props: IProps) { + return ( + + ); +} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index 85ba22a353..585a54ff86 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -27,6 +27,7 @@ import * as sdk from "../../../../../"; import PlatformPeg from "../../../../../PlatformPeg"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import UpdateCheckButton from "../../UpdateCheckButton"; +import AccessTokenDialog from '../../../dialogs/AccessTokenDialog'; export default class HelpUserSettingsTab extends React.Component { static propTypes = { @@ -148,6 +149,17 @@ export default class HelpUserSettingsTab extends React.Component { ); } + onAccessTokenSpoilerClick = async (event) => { + // React throws away the event before we can use it (we are async, after all). + event.persist(); + + // We make the user accept a scary popup to combat Social Engineering. No peeking! + await Modal.createTrackedDialog('Reveal Access Token', '', AccessTokenDialog).finished; + + // Pass it onto the handler. + this._showSpoiler(event); + } + render() { const brand = SdkConfig.get().brand; @@ -266,7 +278,7 @@ export default class HelpUserSettingsTab extends React.Component { {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
{_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
{_t("Access Token:") + ' '} - <{ _t("click to reveal") }> From ae29168e077be47a83628f10b7765d6bcc9a7a0a Mon Sep 17 00:00:00 2001 From: Resynth Date: Sun, 25 Oct 2020 22:05:44 +0000 Subject: [PATCH 002/283] Lint Signed-off-by: Resynth --- src/components/views/dialogs/AccessTokenDialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index 81c48f219a..f95effd523 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -1,4 +1,4 @@ - /* +/* Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,7 @@ type IProps = Exclude< "title" | "danger" | "description" >; -export default function AccessTokenDialog (props: IProps) { +export default function AccessTokenDialog(props: IProps) { return ( ); From 76edd551e5833cb1c94c1025fa069aeb79a9faa2 Mon Sep 17 00:00:00 2001 From: Resynth Date: Mon, 26 Oct 2020 00:34:14 +0000 Subject: [PATCH 003/283] Fix i18n Signed-off-by: Resynth --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index eda69d68ea..ad6593f704 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1585,6 +1585,7 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", + "Do not reveal your Access Token to anyone, under any circumstances. Sharing your Access Token with someone would allow them to login to your account, and access your private information.": "Do not reveal your Access Token to anyone, under any circumstances. Sharing your Access Token with someone would allow them to login to your account, and access your private information.", "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", From a3212c0477ec0fc9166a56a49a9579bae4712d5f Mon Sep 17 00:00:00 2001 From: Resynth Date: Mon, 26 Oct 2020 23:46:53 +0000 Subject: [PATCH 004/283] Fix weird formatting Signed-off-by: Resynth --- src/components/views/dialogs/AccessTokenDialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index f95effd523..2d96d8ec20 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -1,5 +1,6 @@ /* Copyright 2017 Vector Creations Ltd +Copyright 2020 Resynth Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,7 +22,7 @@ import QuestionDialog from './QuestionDialog'; type IProps = Exclude< React.ComponentProps, "title" | "danger" | "description" - >; +>; export default function AccessTokenDialog(props: IProps) { return ( From 34fbed3fbbbbb0ffc17a15631d256b855080b1ee Mon Sep 17 00:00:00 2001 From: Qt Resynth Date: Fri, 20 Nov 2020 01:15:58 +0000 Subject: [PATCH 005/283] Update src/components/views/dialogs/AccessTokenDialog.tsx --- src/components/views/dialogs/AccessTokenDialog.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index 2d96d8ec20..6a943eb5a8 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -31,9 +31,7 @@ export default function AccessTokenDialog(props: IProps) { title="Reveal Access Token" danger={true} description={_t( - "Do not reveal your Access Token to anyone, under any circumstances. " + - "Sharing your Access Token with someone would allow them to login to " + - "your account, and access your private information.", + "Your access token gives full access to your account. Do not share it with anyone." )} > ); From 0e4d656e4bfc0fd16d4628ac44668f6aa2f5ae83 Mon Sep 17 00:00:00 2001 From: Resynth Date: Fri, 20 Nov 2020 18:21:39 +0000 Subject: [PATCH 006/283] Update src/i18n/strings/en_EN.json --- 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 ad6593f704..8005f1cdef 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1585,7 +1585,7 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", - "Do not reveal your Access Token to anyone, under any circumstances. Sharing your Access Token with someone would allow them to login to your account, and access your private information.": "Do not reveal your Access Token to anyone, under any circumstances. Sharing your Access Token with someone would allow them to login to your account, and access your private information.", + "Your access token gives full access to your account. Do not share it with anyone.", "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", From 1b48b08525f75035b9a09993124a12d442d0f2e2 Mon Sep 17 00:00:00 2001 From: Resynth Date: Fri, 20 Nov 2020 18:23:18 +0000 Subject: [PATCH 007/283] Update src/i18n/strings/en_EN.json --- 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 8005f1cdef..a57a68e7b4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1585,7 +1585,7 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", - "Your access token gives full access to your account. Do not share it with anyone.", + "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", From d44aab6d321d04deb6733d9ace5f54b5fe1104bf Mon Sep 17 00:00:00 2001 From: Resynth Date: Mon, 23 Nov 2020 12:57:06 +0000 Subject: [PATCH 008/283] Update src/components/views/dialogs/AccessTokenDialog.tsx Co-authored-by: Michael Telatynski <7t3chguy@googlemail.com> --- src/components/views/dialogs/AccessTokenDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index 6a943eb5a8..c0b1b929df 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -31,7 +31,7 @@ export default function AccessTokenDialog(props: IProps) { title="Reveal Access Token" danger={true} description={_t( - "Your access token gives full access to your account. Do not share it with anyone." + "Your access token gives full access to your account. Do not share it with anyone.", )} >
); 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 009/283] 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 6fdc7a0860131ad5be0950e6e6365d520b801785 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 12 Mar 2021 05:45:22 -0600 Subject: [PATCH 010/283] remove old copyright notice, this is a new file Signed-off-by: Aaron Raimist --- src/components/views/dialogs/AccessTokenDialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index c0b1b929df..220047ac21 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -1,5 +1,4 @@ /* -Copyright 2017 Vector Creations Ltd Copyright 2020 Resynth Licensed under the Apache License, Version 2.0 (the "License"); 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 011/283] 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 012/283] 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 14b8b0f8da14493b685582b6108e8029a6c282ca Mon Sep 17 00:00:00 2001 From: "Sam A. Horvath-Hunt" Date: Tue, 13 Apr 2021 17:22:07 +0100 Subject: [PATCH 013/283] 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: Mon, 19 Apr 2021 20:30:51 +0100 Subject: [PATCH 014/283] 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 015/283] 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 016/283] 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 40ed8fd3420b814790ba496528ddd666d9c1cb52 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 21 Apr 2021 16:47:29 +0100 Subject: [PATCH 017/283] 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 018/283] 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 019/283] 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 020/283] 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 021/283] 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 022/283] 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 023/283] 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 024/283] 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 = ( ); @@ -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 061/283] 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 062/283] 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 063/283] 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 064/283] 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 065/283] 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 066/283] 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 067/283] 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 068/283] 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 069/283] 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 070/283] 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 071/283] 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 072/283] 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 073/283] 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 074/283] 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 075/283] 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 ad4b764096a530217dcca30111e1e2ed7ec267d7 Mon Sep 17 00:00:00 2001 From: libexus Date: Sat, 24 Apr 2021 18:34:02 +0000 Subject: [PATCH 076/283] Translated using Weblate (German) Currently translated at 99.2% (2899 of 2920 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 | 48 +++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 9551f00e55..19df65d5c2 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -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": "Ferngesteuerte Matrixserver können auf diesen Raum nicht zugreifen", + "This room is not accessible by remote Matrix servers": "Nutzer von anderen Homeservern 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", @@ -207,7 +207,7 @@ "Failed to kick": "Rauswurf fehlgeschlagen", "Failed to mute user": "Stummschalten des Nutzers fehlgeschlagen", "Failed to reject invite": "Ablehnen der Einladung ist fehlgeschlagen", - "Failed to set display name": "Anzeigename konnte nicht gesetzt werden", + "Failed to set display name": "Anzeigename konnte nicht geändert werden", "Fill screen": "Fülle Bildschirm", "Incorrect verification code": "Falscher Verifizierungscode", "Join Room": "Raum beitreten", @@ -215,7 +215,7 @@ "not specified": "nicht angegeben", "No more results": "Keine weiteren Ergebnisse", "No results": "Keine Ergebnisse", - "OK": "OK", + "OK": "Ok", "Search": "Suchen", "Search failed": "Suche ist fehlgeschlagen", "Server error": "Serverfehler", @@ -349,7 +349,7 @@ "If you already have a Matrix account you can log in instead.": "Wenn du bereits ein Matrix-Benutzerkonto hast, kannst du dich stattdessen auch direkt anmelden.", "Home": "Startseite", "Username invalid: %(errMessage)s": "Ungültiger Benutzername: %(errMessage)s", - "Accept": "Akzeptieren", + "Accept": "Annehmen", "Active call (%(roomName)s)": "Aktiver Anruf (%(roomName)s)", "Admin Tools": "Administratorwerkzeuge", "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Verbindung zum Heimserver fehlgeschlagen - bitte überprüfe die Internetverbindung und stelle sicher, dass dem SSL-Zertifikat deines Heimservers vertraut wird und dass Anfragen nicht durch eine Browser-Erweiterung blockiert werden.", @@ -362,7 +362,7 @@ "Incoming video call from %(name)s": "Eingehender Videoanruf von %(name)s", "Incoming voice call from %(name)s": "Eingehender Sprachanruf von %(name)s", "Join as voice or video.": "Per Sprachanruf oder Videoanruf beitreten.", - "Last seen": "Zuletzt gesehen", + "Last seen": "Zuletzt gesehen um", "No display name": "Kein Anzeigename", "Private Chat": "Privater Chat", "Public Chat": "Öffentlicher Chat", @@ -430,7 +430,7 @@ "Banned by %(displayName)s": "Verbannt von %(displayName)s", "Description": "Beschreibung", "Unable to accept invite": "Einladung kann nicht angenommen werden", - "Failed to invite users to %(groupId)s": "Benutzer konnten nicht in %(groupId)s eingeladen werden", + "Failed to invite users to %(groupId)s": "Einige Leute konnten nicht in %(groupId)s eingeladen werden", "Unable to reject invite": "Einladung konnte nicht abgelehnt werden", "Who would you like to add to this summary?": "Wen möchtest zu dieser Übersicht hinzufügen?", "Add to summary": "Zur Übersicht hinzufügen", @@ -469,7 +469,7 @@ "Which rooms would you like to add to this community?": "Welche Räume sollen zu dieser Community hinzugefügt werden?", "Add rooms to the community": "Räume zur Community hinzufügen", "Add to community": "Zur Community hinzufügen", - "Failed to invite users to community": "Benutzer konnten nicht in die Community eingeladen werden", + "Failed to invite users to community": "Einige Leute konnten nicht in die Community eingeladen werden", "Communities": "Communities", "Invalid community ID": "Ungültige Community-ID", "'%(groupId)s' is not a valid community ID": "'%(groupId)s' ist keine gültige Community-ID", @@ -504,7 +504,7 @@ "Delete Widget": "Widget löschen", "Mention": "Erwähnen", "Invite": "Einladen", - "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Das Löschen eines Widgets entfernt es für alle Nutzer in diesem Raum. Möchtest du es wirklich löschen?", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Das Löschen des Widgets entfernt es für alle in diesem Raum. Wirklich löschen?", "Mirror local video feed": "Lokalen Video-Feed spiegeln", "Failed to withdraw invitation": "Die Einladung konnte nicht zurückgezogen werden", "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community-IDs dürfen nur die folgenden Zeichen enthalten: a-z, 0-9, or '=_-./'", @@ -588,7 +588,7 @@ "Enable inline URL previews by default": "URL-Vorschau standardmäßig aktivieren", "Enable URL previews for this room (only affects you)": "URL-Vorschau für dich in diesem Raum", "Enable URL previews by default for participants in this room": "URL-Vorschau für Raummitglieder", - "Please note you are logging into the %(hs)s server, not matrix.org.": "Du meldest dich gerade am %(hs)s-Server an, nicht auf matrix.org.", + "Please note you are logging into the %(hs)s server, not matrix.org.": "Du meldest dich gerade am Server von %(hs)s an, nicht auf matrix.org.", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "Sonst ist hier aktuell niemand. Möchtest du Benutzer einladen oder die Warnmeldung bezüglich des leeren Raums deaktivieren?", "URL previews are disabled by default for participants in this room.": "URL-Vorschau ist für Mitglieder des Raumes standardmäßig deaktiviert.", "URL previews are enabled by default for participants in this room.": "URL-Vorschau ist für Mitglieder des Raumes standardmäßig aktiviert.", @@ -647,14 +647,14 @@ "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.": "Wenn du einen Fehler via GitHub meldest, können Fehlerberichte uns helfen um das Problem zu finden. Sie enthalten Anwendungsdaten wie deinen Nutzernamen, Raum- und Gruppen-IDs und Aliase, die du besucht hast sowie Nutzernamen anderer Nutzer mit denen du schreibst. Sie enthalten keine Nachrichten.", "Submit debug logs": "Fehlerberichte einreichen", "Code": "Code", - "Opens the Developer Tools dialog": "Öffnet die Entwicklerwerkzeuge", + "Opens the Developer Tools dialog": "Entwickler-Werkzeuge öffnen", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Von %(displayName)s (%(userName)s) um %(dateTime)s gesehen", "Unable to join community": "Community konnte nicht betreten werden", "Unable to leave community": "Community konnte nicht verlassen werden", "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 Stickerpakete aktiviert", + "You don't currently have any stickerpacks enabled": "Keine Stickerpakete aktiviert", "Hide Stickers": "Sticker ausblenden", "Show Stickers": "Sticker anzeigen", "Who can join this community?": "Wer kann dieser Community beitreten?", @@ -1190,7 +1190,7 @@ "Bulk options": "Sammeloptionen", "Join millions for free on the largest public server": "Schließe dich kostenlos auf dem größten öffentlichen Server Millionen von Menschen an", "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Stellt ¯\\_(ツ)_/¯ einer Klartextnachricht voran", - "Changes your display nickname in the current room only": "Ändert den Anzeigenamen ausschließlich für den aktuellen Raum", + "Changes your display nickname in the current room only": "Ändert deinen Anzeigenamen nur für den aktuellen Raum", "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s hat Abzeichen der Gruppen %(groups)s für diesen Raum aktiviert.", "%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s hat Abzeichen der Gruppen %(groups)s in diesem Raum deaktiviert.", "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s hat Abzeichen von %(newGroups)s aktiviert und von %(oldGroups)s deaktiviert.", @@ -1203,7 +1203,7 @@ "Change room avatar": "Ändere Raumbild", "Change room name": "Ändere Raumname", "Change main address for the room": "Ändere Hauptadresse für den Raum", - "Change history visibility": "Ändere Sichtbarkeit der Historie", + "Change history visibility": "Sichtbarkeit des Verlaufs ändern", "Change permissions": "Ändere Berechtigungen", "Change topic": "Ändere das Thema", "Modify widgets": "Widgets bearbeiten", @@ -3094,7 +3094,7 @@ "New room": "Neuer Raum", "Share invite link": "Einladungslink teilen", "Click to copy": "Klicken um zu kopieren", - "Collapse space panel": "Space-Feld zuklappen", + "Collapse space panel": "Space-Feld einklappen", "Expand space panel": "Space-Feld aufklappen", "Creating...": "Erstelle...", "You can change these at any point.": "Du kannst diese jederzeit ändern.", @@ -3246,5 +3246,23 @@ "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" + "Reset event store": "Ereignis-Speicher zurück setzen", + "Show options to enable 'Do not disturb' mode": "Optionen für den \"Nicht-Stören-Modus\" anzeigen", + "You can add more later too, including already existing ones.": "Natürlich kannst du jederzeit weitere Räume hinzufügen.", + "Let's create a room for each of them.": "Wir erstellen dir für jedes Thema einen Raum.", + "What are some things you want to discuss in %(spaceName)s?": "Welche Themen willst du in %(spaceName)s besprechen?", + "Inviting...": "Einladen...", + "Failed to create initial space rooms": "Fehler beim Initialisieren des Space", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Du bist hier noch alleine. Wenn du den Space verlässt, ist er für immer verloren (eine lange Zeit).", + "Edit settings relating to your space.": "Einstellungen vom Space bearbeiten.", + "Please choose a strong password": "Bitte gib ein sicheres Passwort ein", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Wenn du alles zurücksetzt, gehen alle verifizierten Anmeldungen, Benutzer und verschlüsselte Nachrichten verloren.", + "Only do this if you have no other device to complete verification with.": "Verwende es nur, wenn du kein Gerät, mit dem du dich verifizieren, kannst bei dir hast.", + "Reset everything": "Alles zurücksetzen", + "Forgotten or lost all recovery methods? Reset all": "Hast du alle Wiederherstellungsmethoden vergessen? Setze sie hier zurück", + "View message": "Nachricht anzeigen", + "Zoom in": "Vergrößern", + "Zoom out": "Verkleinern", + "%(seconds)ss left": "%(seconds)s vergangen", + "Change server ACLs": "ACLs des Servers bearbeiten" } From bfabb7f61183d459b09151a9c8b5ec8a7a7de930 Mon Sep 17 00:00:00 2001 From: Hannah Rittich Date: Sat, 24 Apr 2021 18:27:27 +0000 Subject: [PATCH 077/283] Translated using Weblate (German) Currently translated at 99.2% (2899 of 2920 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 19df65d5c2..493de44b7e 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 großartig.", + "Hey you. You're the best!": "Hey du. Du bist großartig!", "Message layout": "Nachrichtenlayout", "Compact": "Kompakt", "Modern": "Modern", From dc93d4c2201f83f1e3d55eefb3f14c54af4a0841 Mon Sep 17 00:00:00 2001 From: Sven Grewe Date: Wed, 21 Apr 2021 21:39:52 +0000 Subject: [PATCH 078/283] Translated using Weblate (German) Currently translated at 99.2% (2899 of 2920 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 493de44b7e..d2bd36144b 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3243,10 +3243,10 @@ "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", + "Reset event store?": "Ereignisspeicher zurück setzen?", + "You most likely do not want to reset your event index store": "Es ist wahrscheinlich, dass du den Ereignis-Indexspeicher 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", + "Reset event store": "Ereignisspeicher zurück setzen", "Show options to enable 'Do not disturb' mode": "Optionen für den \"Nicht-Stören-Modus\" anzeigen", "You can add more later too, including already existing ones.": "Natürlich kannst du jederzeit weitere Räume hinzufügen.", "Let's create a room for each of them.": "Wir erstellen dir für jedes Thema einen Raum.", From e40c822dbfee36f6261bcc51660ef290136cbcf5 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 23 Apr 2021 16:24:20 +0000 Subject: [PATCH 079/283] Translated using Weblate (English (United States)) Currently translated at 19.9% (583 of 2920 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 003da7ef8f..bfc21a81d3 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -657,5 +657,6 @@ "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.", - "Use Single Sign On to continue": "Use Single Sign On to continue" + "Use Single Sign On to continue": "Use Single Sign On to continue", + "Message search initilisation failed": "Message search initialization failed" } From e3e09c6593656a89a6aa99b955cdc070fe86d37d Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Fri, 23 Apr 2021 09:52:32 +0000 Subject: [PATCH 080/283] Translated using Weblate (French) Currently translated at 100.0% (2920 of 2920 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index fcc5ec9afe..5fc5b1d10b 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3262,5 +3262,17 @@ "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é" + "Sends the given message as a spoiler": "Envoie le message flouté", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Vous êtes la seule personne ici. Si vous partez, plus personne ne pourra rejoindre cette conversation, y compris vous.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Si vous réinitialisez tout, vous allez repartir sans session et utilisateur de confiance. Vous pourriez ne pas voir certains messages passés.", + "Only do this if you have no other device to complete verification with.": "Poursuivez seulement si vous n’avez aucun autre appareil avec lequel procéder à la vérification.", + "Reset everything": "Tout réinitialiser", + "Forgotten or lost all recovery methods? Reset all": "Vous avez perdu ou oublié tous vos moyens de récupération ? Tout réinitialiser", + "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": "Si vous le faites, notez 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", + "View message": "Afficher le message", + "Zoom in": "Zoomer", + "Zoom out": "Dé-zoomer", + "%(seconds)ss left": "%(seconds)s secondes restantes", + "Change server ACLs": "Modifier les ACL du serveur", + "Show options to enable 'Do not disturb' mode": "Afficher une option pour activer le mode « Ne pas déranger »" } From db9a06def64a576437f86ac72402f518abc19243 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Fri, 23 Apr 2021 07:34:54 +0000 Subject: [PATCH 081/283] Translated using Weblate (Hungarian) Currently translated at 100.0% (2920 of 2920 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 2ec5af8a17..252badd54a 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3280,5 +3280,17 @@ "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" + "Sends the given message as a spoiler": "A megadott üzenet szpojlerként küldése", + "Change server ACLs": "Kiszolgáló ACL-ek módosítása", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Csak ön van itt. Ha kilép, akkor a jövőben senki nem tud majd ide belépni, beleértve önt is.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Ha mindent alapállapotba helyez, nem lesz megbízható munkamenete, nem lesznek megbízható felhasználók és a régi üzenetekhez sem biztos, hogy hozzáfér majd.", + "Only do this if you have no other device to complete verification with.": "Csak akkor tegye meg, ha nincs egyetlen másik eszköze sem az ellenőrzés elvégzéséhez.", + "Reset everything": "Minden visszaállítása", + "Forgotten or lost all recovery methods? Reset all": "Elfelejtette vagy elveszett minden visszaállítási lehetőség? Mind 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 moments whilst 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", + "View message": "Üzenet megjelenítése", + "Zoom in": "Nagyít", + "Zoom out": "Kicsinyít", + "%(seconds)ss left": "%(seconds)s mp van vissza", + "Show options to enable 'Do not disturb' mode": "Mutassa a lehetőséget a „Ne zavarjanak” módhoz" } From 6ce1c06887978f4d2cf0224215f0026548babc26 Mon Sep 17 00:00:00 2001 From: Nikita Epifanov Date: Sat, 24 Apr 2021 07:34:44 +0000 Subject: [PATCH 082/283] Translated using Weblate (Russian) Currently translated at 97.9% (2859 of 2920 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 7db3758fd8..da42347b49 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -3210,5 +3210,6 @@ "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 комнат и одно пространство" + "%(count)s rooms and 1 space|other": "%(count)s комнат и одно пространство", + "Sends the given message as a spoiler": "Отправить данное сообщение под спойлером" } From afceb7c2e4f1da14f32ca6c691271b0f667af40c Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Thu, 22 Apr 2021 12:52:29 +0000 Subject: [PATCH 083/283] Translated using Weblate (Swedish) Currently translated at 100.0% (2920 of 2920 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 42a7f78268..3517173ded 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3216,5 +3216,17 @@ "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." + "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.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Du är den enda personen här. Om du lämnar så kommer ingen kunna gå med igen, inklusive du.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Om du återställer allt så kommer du att börja om utan betrodda sessioner eller betrodda användare, och kommer kanske inte kunna se gamla meddelanden.", + "Only do this if you have no other device to complete verification with.": "Gör detta endast om du inte har någon annan enhet att slutföra verifikationen med.", + "Reset everything": "Återställ allt", + "Forgotten or lost all recovery methods? Reset all": "Glömt eller förlorat alla återställningsalternativ? Återställ allt", + "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": "Om du gör det, observera att inga av dina meddelanden kommer att raderas, men sökupplevelsen kan degraderas en stund medans registret byggs upp igen", + "View message": "Visa meddelande", + "Zoom in": "Zooma in", + "Zoom out": "Zooma ut", + "%(seconds)ss left": "%(seconds)ss kvar", + "Change server ACLs": "Ändra server-ACLer", + "Show options to enable 'Do not disturb' mode": "Visa alternativ för att aktivera 'Stör ej'-läget" } From 124cba679c0710990023a1c481a596b445ce6f39 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 22 Apr 2021 02:50:39 +0000 Subject: [PATCH 084/283] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2920 of 2920 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index c9bb9bb2d7..cedbb8f52c 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3288,5 +3288,17 @@ "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.": "未經驗證,您將無法存取您的所有訊息,且可能不被其他人信任。" + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "未經驗證,您將無法存取您的所有訊息,且可能不被其他人信任。", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "您是這裡唯一的人。如果您離開,包含您在內的任何人都無法加入。", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "如果您重設所有東西,您將會在沒有受信任的工作階段、沒有受信任的使用者,且可能會看不到過去的訊息。", + "Only do this if you have no other device to complete verification with.": "當您沒有其他裝置可以完成驗證時,才執行此動作。", + "Reset everything": "重設所有東西", + "Forgotten or lost all recovery methods? Reset all": "忘記或遺失了所有復原方法?重設全部", + "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": "如果這樣做,請注意,您的訊息不會被刪除,但在重新建立索引時,搜尋體驗可能會降低片刻", + "View message": "檢視訊息", + "Zoom in": "放大", + "Zoom out": "縮小", + "%(seconds)ss left": "剩%(seconds)s秒", + "Change server ACLs": "變更伺服器 ACL", + "Show options to enable 'Do not disturb' mode": "顯示啟用「勿打擾」模式的選項" } From 04e54ff95a285a05ebb4cb2d90a31debbed7d75b Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Wed, 21 Apr 2021 19:13:07 +0000 Subject: [PATCH 085/283] Translated using Weblate (Czech) Currently translated at 100.0% (2920 of 2920 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 7f9ff9341c..c267e43b89 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3202,5 +3202,17 @@ "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ý." + "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ý.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Jste zde jediný člověk. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Pokud vše resetujete, začnete bez důvěryhodných relací, bez důvěryhodných uživatelů a možná nebudete moci zobrazit minulé zprávy.", + "Only do this if you have no other device to complete verification with.": "Udělejte to, pouze pokud nemáte žádné jiné zařízení, se kterým byste mohli dokončit ověření.", + "Reset everything": "Resetovat vše", + "Forgotten or lost all recovery methods? Reset all": "Zapomněli nebo ztratili jste všechny metody obnovy? Obnovit vše", + "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": "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ů zpomaleno během opětovného vytvoření indexu", + "View message": "Zobrazit zprávu", + "Zoom in": "Přiblížit", + "Zoom out": "Oddálit", + "%(seconds)ss left": "Zbývá %(seconds)ss", + "Change server ACLs": "Změnit seznamy přístupů serveru", + "Show options to enable 'Do not disturb' mode": "Zobrazit možnosti pro povolení režimu „Nerušit“" } From 9995e7a4657945375f5c4cc7d3217a4e0cf1d1c5 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 22 Apr 2021 19:05:21 +0000 Subject: [PATCH 086/283] Translated using Weblate (Galician) Currently translated at 100.0% (2920 of 2920 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index f6ebce684f..1300815a4c 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3285,5 +3285,17 @@ "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" + "Use another login": "Usar outra conexión", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Es a única persoa aquí. Se saes, ninguén poderá unirse no futuro, incluíndote a ti.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Se restableces todo, volverás a comezar sen sesións verificadas, usuarias de confianza, e poderías non poder ver as mensaxes anteriores.", + "Only do this if you have no other device to complete verification with.": "Fai isto únicamente se non tes outro dispositivo co que completar a verificación.", + "Reset everything": "Restablecer todo", + "Forgotten or lost all recovery methods? Reset all": "Perdidos ou esquecidos tódolos métodos de recuperación? Restabléceos", + "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": "Se o fas, ten en conta que non se borrará ningunha das túas mensaxes, mais a experiencia de busca podería degradarse durante uns momentos ata que se recrea o índice", + "View message": "Ver mensaxe", + "Zoom in": "Achegar", + "Zoom out": "Alonxar", + "%(seconds)ss left": "%(seconds)ss restantes", + "Change server ACLs": "Cambiar ACLs do servidor", + "Show options to enable 'Do not disturb' mode": "Mostrar opcións para activar o modo 'Non molestar'" } From 5ddafb96a9bf7be8982b18a1916fada5f13006ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 21 Apr 2021 15:49:57 +0000 Subject: [PATCH 087/283] Translated using Weblate (Estonian) Currently translated at 100.0% (2920 of 2920 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 444475deea..282b3f77cf 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3263,5 +3263,17 @@ "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." + "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.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Sa oled siin viimane osaleja. Kui sa nüüd lahkud, siis mitte keegi, kaasa arvatud sa ise, ei saa hiljem enam liituda.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Kui sa kõik krüptoseosed lähtestad, siis sul esimese hooga pole ühtegi usaldusväärseks tunnistatud sessiooni ega kasutajat ning ilmselt ei saa sa lugeda vanu sõnumeid.", + "Only do this if you have no other device to complete verification with.": "Toimi nii vaid siis, kui sul pole jäänud ühtegi seadet, millega verifitseerimist lõpuni teha.", + "Reset everything": "Alusta kõigega algusest", + "Forgotten or lost all recovery methods? Reset all": "Unustasid või oled kaotanud kõik võimalused ligipääsu taastamiseks? Lähtesta kõik ühe korraga", + "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": "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", + "View message": "Vaata sõnumit", + "Zoom in": "Suumi sisse", + "Zoom out": "Suumi välja", + "%(seconds)ss left": "jäänud %(seconds)s sekundit", + "Change server ACLs": "Muuda serveri ligipääsuõigusi", + "Show options to enable 'Do not disturb' mode": "Näita valikuid „Ära sega“ režiimi sisse lülitamiseks" } 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 088/283] 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 089/283] 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 090/283] 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 091/283] 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 092/283] 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 093/283] 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 094/283] 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 095/283] 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 096/283] 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 097/283] 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 098/283] 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 099/283] 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 100/283] 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 101/283] 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 102/283] 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 103/283] 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 104/283] 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 105/283] 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 106/283] 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 107/283] 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 108/283] 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 109/283] 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 110/283] 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 111/283] 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 112/283] 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 113/283] 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 114/283] 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 115/283] 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 116/283] 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 117/283] 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 118/283] 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 119/283] 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 120/283] 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 121/283] 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 122/283] 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 123/283] 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 124/283] 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 125/283] 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 126/283] 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 127/283] 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 128/283] 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 129/283] 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 130/283] 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 bb4c454d38dac507c392b3294abe71fb864ce262 Mon Sep 17 00:00:00 2001 From: libexus Date: Sun, 25 Apr 2021 17:10:52 +0000 Subject: [PATCH 131/283] Translated using Weblate (German) Currently translated at 99.2% (2898 of 2920 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 | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index d2bd36144b..5cbdd20a36 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -47,7 +47,7 @@ "For security, this session has been signed out. Please sign in again.": "Aus Sicherheitsgründen wurde diese Sitzung beendet. Bitte melde dich erneut an.", "Guests cannot join this room even if explicitly invited.": "Gäste können diesem Raum nicht beitreten, auch wenn sie explizit eingeladen wurden.", "Hangup": "Auflegen", - "Homeserver is": "Der Heimserver ist", + "Homeserver is": "Dein 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-Raumschlüssel importieren", @@ -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 Aktualisierung suchen...", + "Checking for an update...": "Nach Aktualisierungen 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. ", @@ -978,7 +978,7 @@ "%(names)s and %(lastPerson)s are typing …": "%(names)s und %(lastPerson)s tippen…", "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 a placeholder for removed messages": "Platzhalter für gelöschte Nachrichten", "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", @@ -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 und filtere Räume nach eigenen Tags (neu laden um Änderungen zu übernehmen)", + "Group & filter rooms by custom tags (refresh to apply changes)": "[Veraltet] 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", @@ -1205,7 +1205,7 @@ "Change main address for the room": "Ändere Hauptadresse für den Raum", "Change history visibility": "Sichtbarkeit des Verlaufs ändern", "Change permissions": "Ändere Berechtigungen", - "Change topic": "Ändere das Thema", + "Change topic": "Thema ändern", "Modify widgets": "Widgets bearbeiten", "Default role": "Standard-Rolle", "Send messages": "Nachrichten senden", @@ -1446,7 +1446,7 @@ "Go": "Los", "Command Help": "Befehl Hilfe", "To help us prevent this in future, please send us logs.": "Um uns zu helfen, dies in Zukunft zu vermeiden, sende uns bitte die Protokolldateien.", - "Notification settings": "Benachrichtigungseinstellungen", + "Notification settings": "Benachrichtigungen", "Help": "Hilf uns", "Filter": "Filtern", "Filter rooms…": "Räume filtern…", @@ -2107,7 +2107,7 @@ "Failed to re-authenticate due to a homeserver problem": "Erneute Authentifizierung aufgrund eines Problems im Heimserver fehlgeschlagen", "Failed to re-authenticate": "Erneute Authentifizierung fehlgeschlagen", "Command Autocomplete": "Autovervollständigung aktivieren", - "Community Autocomplete": "Community-Auto-Vervollständigung", + "Community Autocomplete": "Community-Autovervollständigung", "DuckDuckGo Results": "DuckDuckGo Ergebnisse", "Great! This recovery passphrase looks strong enough.": "Super! Diese Wiederherstellungspassphrase sieht stark genug aus.", "Enter a recovery passphrase": "Gib eine Wiederherstellungspassphrase ein", @@ -2129,12 +2129,12 @@ "Navigation": "Navigation", "Calls": "Anrufe", "Room List": "Raumliste", - "Autocomplete": "Auto-Vervollständigung", + "Autocomplete": "Autovervollständigung", "Alt": "Alt", - "Toggle microphone mute": "Schalte Mikrofon stumm/an", - "Toggle video on/off": "Schalte Video an/aus", + "Toggle microphone mute": "Mikrofon an-/ausschalten", + "Toggle video on/off": "Video an-/ausschalten", "Jump to room search": "Zur Raumsuche springen", - "Close dialog or context menu": "Schließe Dialog oder Kontextmenü", + "Close dialog or context menu": "Dialog oder Kontextmenü schließen", "Cancel autocomplete": "Autovervollständigung deaktivieren", "Unable to revoke sharing for email address": "Dem Teilen der E-Mail-Adresse kann nicht widerrufen werden", "Unable to validate homeserver/identity server": "Heimserver/Identitätsserver nicht validierbar", @@ -2172,7 +2172,7 @@ "Activate selected button": "Ausgewählten Button aktivieren", "Toggle right panel": "Rechtes Panel ein-/ausblenden", "Toggle this dialog": "Diesen Dialog ein-/ausblenden", - "Move autocomplete selection up/down": "Auto-Vervollständigung nach oben/unten verschieben", + "Move autocomplete selection up/down": "Autovervollständigung nach oben/unten verschieben", "Opens chat with the given user": "Öffnet einen Chat mit diesem Benutzer", "Sends a message to the given user": "Sendet diesem Benutzer eine Nachricht", "Waiting for your other session to verify…": "Warte auf die Verifikation deiner anderen Sitzungen…", @@ -2421,7 +2421,7 @@ "Download logs": "Protokolle herunterladen", "Unexpected server error trying to leave the room": "Unerwarteter Serverfehler beim Versuch den Raum zu verlassen", "Error leaving room": "Fehler beim Verlassen des Raums", - "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 Prototyp. Benötigt einen kompatiblen Heimserver. Höchst experimentell - mit Vorsicht verwenden.", + "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "[Veraltet] Communities v2 Prototyp. Benötigt einen kompatiblen Heimserver. Höchst experimentell - mit Vorsicht verwenden.", "Explore rooms in %(communityName)s": "Räume in %(communityName)s erkunden", "Set up Secure Backup": "Schlüsselsicherung einrichten", "Information": "Information", @@ -3025,7 +3025,7 @@ "Security Key mismatch": "Nicht übereinstimmende Sicherheitsschlüssel", "Set my room layout for everyone": "Diese Raumgestaltung für alle setzen", "%(senderName)s has updated the widget layout": "%(senderName)s hat das Widget-Layout aktualisiert", - "Search (must be enabled)": "Suche (muss aktiviert sein)", + "Search (must be enabled)": "Suchen (muss in den Einstellungen aktiviert sein)", "Remember this": "Dies merken", "The widget will verify your user ID, but won't be able to perform actions for you:": "Das Widget überprüft deine Nutzer-ID, kann jedoch keine Aktionen für dich ausführen:", "Allow this widget to verify your identity": "Erlaube diesem Widget deine Identität zu überprüfen", @@ -3085,7 +3085,7 @@ "Save changes": "Änderungen speichern", "Undo": "Rückgängig", "Save Changes": "Änderungen Speichern", - "View dev tools": "Entwicklerwerkzeuge anzeigen", + "View dev tools": "Entwicklerwerkzeuge", "Apply": "Anwenden", "Create a new room": "Neuen Raum erstellen", "Suggested Rooms": "Vorgeschlagene Räume", From 8afcf8293447854d9703810cd081940c07b14b4f Mon Sep 17 00:00:00 2001 From: jelv Date: Sun, 25 Apr 2021 11:09:13 +0000 Subject: [PATCH 132/283] Translated using Weblate (Dutch) Currently translated at 100.0% (2920 of 2920 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index de98a878e8..10a921b11d 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3171,5 +3171,17 @@ "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" + "Sends the given message as a spoiler": "Verstuurt het bericht als een spoiler", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "U bent de enige persoon hier. Als u weggaat, zal niemand in de toekomst kunnen toetreden, u ook niet.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Als u alles reset, zult u opnieuw opstarten zonder vertrouwde sessies, zonder vertrouwde gebruikers, en zult u misschien geen vroegere berichten meer kunnen zien.", + "Only do this if you have no other device to complete verification with.": "Doe dit alleen als u geen ander apparaat hebt om de verificatie mee uit te voeren.", + "Reset everything": "Alles opnieuw instellen", + "Forgotten or lost all recovery methods? Reset all": "Alles vergeten of alle herstelmethoden verloren? Alles opnieuw instellen", + "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": "Als u dat doet, let wel geen van uw berichten wordt verwijderd, maar de zoekresultaten zullen gedurende enkele ogenblikken verslechteren terwijl de index opnieuw wordt aangemaakt", + "View message": "Bericht bekijken", + "Zoom in": "Inzoomen", + "Zoom out": "Uitzoomen", + "%(seconds)ss left": "%(seconds)s's over", + "Change server ACLs": "Wijzig server ACL's", + "Show options to enable 'Do not disturb' mode": "Toon opties om de 'Niet storen' modus in te schakelen" } From f16a7240a4111a1a02427ee00e59d08a758d67e8 Mon Sep 17 00:00:00 2001 From: random Date: Mon, 26 Apr 2021 14:21:00 +0000 Subject: [PATCH 133/283] Translated using Weblate (Italian) Currently translated at 100.0% (2920 of 2920 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 2d0edb77e6..5e53aced69 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3285,5 +3285,17 @@ "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." + "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.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Sei l'unica persona qui. Se esci, nessuno potrà entrare in futuro, incluso te.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Se reimposti tutto, ricomincerai senza sessioni fidate, senza utenti fidati e potresti non riuscire a vedere i messaggi passati.", + "Only do this if you have no other device to complete verification with.": "Fallo solo se non hai altri dispositivi con cui completare la verifica.", + "Reset everything": "Reimposta tutto", + "Forgotten or lost all recovery methods? Reset all": "Hai dimenticato o perso tutti i metodi di recupero? Reimposta tutto", + "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": "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", + "View message": "Vedi messaggio", + "Zoom in": "Ingrandisci", + "Zoom out": "Rimpicciolisci", + "%(seconds)ss left": "%(seconds)ss rimanenti", + "Change server ACLs": "Modifica le ACL del server", + "Show options to enable 'Do not disturb' mode": "Mostra opzioni per attivare la modalità \"Non disturbare\"" } From 6a517f2764221fce1e9a0e2ed4faaf7a1af36efd Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Mon, 26 Apr 2021 10:22:44 +0000 Subject: [PATCH 134/283] Translated using Weblate (Czech) Currently translated at 100.0% (2920 of 2920 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 | 86 ++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index c267e43b89..ac81b49d95 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -896,7 +896,7 @@ "Prompt before sending invites to potentially invalid matrix IDs": "Potvrdit odeslání pozvánky potenciálně neplatným Matrix ID", "Show avatar changes": "Zobrazovat změny avatarů", "Show join/leave messages (invites/kicks/bans unaffected)": "Zobrazovat zprávy o vstupu/opuštění (netýká se pozvánek/vykopnutí/vykázání)", - "Show a placeholder for removed messages": "Zobrazovat smazané zprávy jako zamazané", + "Show a placeholder for removed messages": "Zobrazovat smazané zprávy", "Show display name changes": "Zobrazovat změny zobrazovaného jména", "Messages containing my username": "Zprávy obsahující moje uživatelské jméno", "Messages containing @room": "Zprávy obsahující @room", @@ -1936,7 +1936,7 @@ "If disabled, messages from encrypted rooms won't appear in search results.": "Když je to zakázané, zprávy v šifrovaných místnostech se nebudou objevovat ve výsledcích vyhledávání.", "Disable": "Zakázat", "%(brand)s is securely caching encrypted messages locally for them to appear in search results:": "%(brand)s si bezpečně uchovává šifrované zprávy lokálně, aby v nich mohl vyhledávat:", - "Space used:": "Použitý prostor:", + "Space used:": "Použité místo:", "Indexed messages:": "Indexované zprávy:", "Indexed rooms:": "Indexované místnosti:", "Message downloading sleep time(ms)": "Čas na stažení zprávy (ms)", @@ -3012,8 +3012,8 @@ "Inviting...": "Pozvání...", "Invite by username": "Pozvat podle uživatelského jména", "Invite your teammates": "Pozvěte své spolupracovníky", - "Failed to invite the following users to your space: %(csvUsers)s": "Nepodařilo se pozvat následující uživatele do vašeho space: %(csvUsers)s", - "A private space for you and your teammates": "Soukromý space pro Vás a vaše spolupracovníky", + "Failed to invite the following users to your space: %(csvUsers)s": "Nepodařilo se pozvat následující uživatele do vašeho prostoru: %(csvUsers)s", + "A private space for you and your teammates": "Soukromý prostor pro Vás a vaše spolupracovníky", "Me and my teammates": "Já a moji spolupracovníci", "A private space just for you": "Soukromý space právě pro vás", "Just Me": "Pouze já", @@ -3022,7 +3022,7 @@ "At the moment only you can see it.": "V tuto chvíli to vidíte jen Vy.", "Creating rooms...": "Vytváření místností...", "Skip for now": "Prozatím přeskočit", - "Failed to create initial space rooms": "Vytvoření počátečních místností ve space se nezdařilo", + "Failed to create initial space rooms": "Vytvoření počátečních místností v prostoru se nezdařilo", "Random": "Náhodný", "Your private space ": "Váš soukromý space ", "Your public space ": "Váš veřejný space ", @@ -3030,9 +3030,9 @@ " invited you to ": " vás pozval do ", "%(count)s members|one": "%(count)s člen", "%(count)s members|other": "%(count)s členů", - "Your server does not support showing space hierarchies.": "Váš server nepodporuje zobrazování hierarchií spaces.", + "Your server does not support showing space hierarchies.": "Váš server nepodporuje zobrazování hierarchií prostorů.", "Default Rooms": "Výchozí místnosti", - "Add existing rooms & spaces": "Přidat stávající místnosti a spaces", + "Add existing rooms & spaces": "Přidat stávající místnosti a prostory", "Accept Invite": "Přijmout pozvání", "Find a room...": "Najít místnost...", "Manage rooms": "Spravovat místnosti", @@ -3044,68 +3044,68 @@ "Remove from Space": "Odebrat ze space", "Undo": "Vrátit", "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.": "Vaše zpráva nebyla odeslána, protože tento domovský server byl zablokován jeho správcem. Kontaktujte svého správce služby, abyste mohli službu nadále používat.", - "Are you sure you want to leave the space '%(spaceName)s'?": "Opravdu chcete opustit space '%(spaceName)s'?", - "This space is not public. You will not be able to rejoin without an invite.": "Tento space není veřejný. Bez pozvánky se nebudete moci znovu připojit.", + "Are you sure you want to leave the space '%(spaceName)s'?": "Opravdu chcete opustit prostor '%(spaceName)s'?", + "This space is not public. You will not be able to rejoin without an invite.": "Tento prostor není veřejný. Bez pozvánky se nebudete moci znovu připojit.", "Start audio stream": "Zahájit audio přenos", "Failed to start livestream": "Nepodařilo spustit živý přenos", "Unable to start audio streaming.": "Nelze spustit streamování zvuku.", "View dev tools": "Zobrazit nástroje pro vývojáře", - "Leave Space": "Opustit space", - "Make this space private": "Nastavit tento space jako soukromý", - "Edit settings relating to your space.": "Upravte nastavení týkající se vašeho space.", - "Space settings": "Nastavení space", - "Failed to save space settings.": "Nastavení space se nepodařilo uložit.", - "Invite someone using their name, username (like ) or share this space.": "Pozvěte někoho pomocí jeho jména, uživatelského jména (například ) nebo sdílejte tento space.", - "Invite someone using their name, email address, username (like ) or share this space.": "Pozvěte někoho pomocí jeho jména, e-mailové adresy, uživatelského jména (například ) nebo sdílejte tento space.", - "Unnamed Space": "Nejmenovaný space", + "Leave Space": "Opustit prostor", + "Make this space private": "Nastavit tento prostor jako soukromý", + "Edit settings relating to your space.": "Upravte nastavení týkající se vašeho prostoru.", + "Space settings": "Nastavení prostoru", + "Failed to save space settings.": "Nastavení prostoru se nepodařilo uložit.", + "Invite someone using their name, username (like ) or share this space.": "Pozvěte někoho pomocí jeho jména, uživatelského jména (například ) nebo sdílejte tento prostor.", + "Invite someone using their name, email address, username (like ) or share this space.": "Pozvěte někoho pomocí jeho jména, e-mailové adresy, uživatelského jména (například ) nebo sdílejte tento prostor.", + "Unnamed Space": "Nejmenovaný prostor", "Invite to %(spaceName)s": "Pozvat do %(spaceName)s", - "Failed to add rooms to space": "Nepodařilo se přidat místnosti do space", + "Failed to add rooms to space": "Nepodařilo se přidat místnosti do prostoru", "Applying...": "Potvrzuji...", "Apply": "Použít", "Create a new room": "Vytvořit novou místnost", "Don't want to add an existing room?": "Nechcete přidat existující místnost?", - "Spaces": "Spaces", - "Filter your rooms and spaces": "Filtrujte své místnosti a spaces", + "Spaces": "Prostory", + "Filter your rooms and spaces": "Filtrujte své místnosti a prostory", "Add existing spaces/rooms": "Přidat existující space/místnost", - "Space selection": "Výběr space", - "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "Tuto změnu nebudete moci vrátit zpět, protože budete degradováni, pokud jste posledním privilegovaným uživatelem v daném space, nebude možné znovu získat oprávnění.", + "Space selection": "Výběr prostoru", + "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the space it will be impossible to regain privileges.": "Tuto změnu nebudete moci vrátit zpět, protože budete degradováni, pokud jste posledním privilegovaným uživatelem v daném prostoru, nebude možné znovu získat oprávnění.", "Empty room": "Prázdná místnost", "Suggested Rooms": "Doporučené místnosti", "Explore space rooms": "Prozkoumat místnosti space", - "You do not have permissions to add rooms to this space": "Nemáte oprávnění k přidávání místností do tohoto space", + "You do not have permissions to add rooms to this space": "Nemáte oprávnění k přidávání místností do tohoto prostoru", "Add existing room": "Přidat existující místnost", - "You do not have permissions to create new rooms in this space": "Nemáte oprávnění k vytváření nových místností v tomto space", + "You do not have permissions to create new rooms in this space": "Nemáte oprávnění k vytváření nových místností v tomto prostoru", "Send message": "Poslat zprávu", - "Invite to this space": "Pozvat do tohoto space", + "Invite to this space": "Pozvat do tohoto prostoru", "Your message was sent": "Zpráva byla odeslána", "Encrypting your message...": "Šifrování zprávy...", "Sending your message...": "Odesílání zprávy...", "Spell check dictionaries": "Slovníky pro kontrolu pravopisu", - "Space options": "Nastavení space", + "Space options": "Nastavení prostoru", "Space Home": "Domov space", "New room": "Nová místnost", - "Leave space": "Opusit space", + "Leave space": "Opusit prostor", "Invite people": "Pozvat lidi", - "Share your public space": "Sdílejte svůj veřejný space", + "Share your public space": "Sdílejte svůj veřejný prostor", "Invite members": "Pozvat členy", "Invite by email or username": "Pozvěte e-mailem nebo uživatelským jménem", "Share invite link": "Sdílet odkaz na pozvánku", "Click to copy": "Kliknutím zkopírujte", - "Expand space panel": "Rozbalit space panel", - "Collapse space panel": "Sbalit space panel", + "Expand space panel": "Rozbalit panel prostoru", + "Collapse space panel": "Sbalit panel prostoru", "Creating...": "Vytváření...", "You can change these at any point.": "Můžete je kdykoli změnit.", "Give it a photo, name and description to help you identify it.": "Přiřaďte mu obrázek, jméno a popis, abyste jej mohli lépe identifikovat.", - "Your private space": "Váš soukromý space", - "Your public space": "Váš veřejný space", + "Your private space": "Váš soukromý prostor", + "Your public space": "Váš veřejný prostor", "You can change this later": "Toto můžete změnit později", "Invite only, best for yourself or teams": "Pouze pozvat, nejlepší pro sebe nebo pro týmy", "Private": "Soukromý", - "Open space for anyone, best for communities": "Otevřený space pro kohokoli, nejlepší pro komunity", + "Open space for anyone, best for communities": "Otevřený prostor pro kohokoli, nejlepší pro komunity", "Public": "Veřejný", - "Create a space": "Vytvořit space", + "Create a space": "Vytvořit prostor", "Jump to the bottom of the timeline when you send a message": "Při odesílání zprávy přeskočit na konec časové osy", - "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototyp. Nejsou kompatibilní se skupinami, skupinami v2 a vlastními štítky. Pro některé funkce je vyžadován kompatibilní domovský server.", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Prototyp prostorů. Nejsou kompatibilní se skupinami, skupinami v2 a vlastními štítky. Pro některé funkce je vyžadován kompatibilní domovský server.", "This homeserver has been blocked by its administrator.": "Tento domovský server byl zablokován jeho správcem.", "You're already in a call with this person.": "S touto osobou již telefonujete.", "Already in call": "Již máte hovor", @@ -3123,13 +3123,13 @@ "We'll create rooms for each of them. You can add more later too, including already existing ones.": "Pro každého z nich vytvoříme místnost. Později můžete přidat další, včetně již existujících.", "Let's create a room for each of them. You can add more later too, including already existing ones.": "Vytvořme místnost pro každého z nich. Později můžete přidat i další, včetně již existujících.", "Make sure the right people have access. You can invite more later.": "Zajistěte přístup pro správné lidi. Další můžete pozvat později.", - "A private space to organise your rooms": "Soukromý space pro uspořádání vašich místností", + "A private space to organise your rooms": "Soukromý prostor pro uspořádání vašich místností", "Make sure the right people have access to %(name)s": "Zajistěte, aby do %(name)s měli přístup správní lidé", "Go to my first room": "Jít do mé první místnosti", "It's just you at the moment, it will be even better with others.": "V tuto chvíli to jste jen vy, s ostatními to bude ještě lepší.", "Share %(name)s": "Sdílet %(name)s", - "Private space": "Soukromý space", - "Public space": "Veřejný space", + "Private space": "Soukromý prostor", + "Public space": "Veřejný prostor", " invites you": " vás zve", "Search names and description": "Prohledat jména a popisy", "Create room": "Vytvořit místnost", @@ -3139,10 +3139,10 @@ "Mark as not suggested": "Označit jako nedoporučené", "Removing...": "Odebírání...", "Failed to remove some rooms. Try again later": "Odebrání některých místností se nezdařilo. Zkuste to později znovu", - "%(count)s rooms and 1 space|one": "%(count)s místnost a 1 space", - "%(count)s rooms and 1 space|other": "%(count)s místností a 1 space", - "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s místnost a %(numSpaces)s space", - "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s místností a %(numSpaces)s spaces", + "%(count)s rooms and 1 space|one": "%(count)s místnost a 1 prostor", + "%(count)s rooms and 1 space|other": "%(count)s místností a 1 prostor", + "%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s místnost a %(numSpaces)s prostorů", + "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s místností a %(numSpaces)s prostorů", "If you can't find the room you're looking for, ask for an invite or create a new room.": "Pokud nemůžete najít místnost, kterou hledáte, požádejte o pozvánku nebo vytvořte novou místnost.", "This room is suggested as a good one to join": "Tato místnost je doporučena jako dobrá pro připojení", "Suggested": "Doporučeno", @@ -3156,7 +3156,7 @@ "Invite with email or username": "Pozvěte e-mailem nebo uživatelským jménem", "You can change these anytime.": "Tyto údaje můžete kdykoli změnit.", "Add some details to help people recognise it.": "Přidejte několik podrobností, aby to lidé lépe rozpoznali.", - "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Spaces jsou nový způsob, jak seskupovat místnosti a lidi. Chcete-li se připojit ke stávajícímu space, budete potřebovat pozvánku.", + "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Prostory jsou nový způsob, jak seskupovat místnosti a lidi. Chcete-li se připojit ke stávajícímu prostoru, budete potřebovat pozvánku.", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "K vašemu účtu přistupuje nové přihlášení: %(name)s (%(deviceID)s) pomocí %(ip)s", "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Z %(deviceName)s (%(deviceId)s) pomocí %(ip)s", "Verify this login to access your encrypted messages and prove to others that this login is really you.": "Ověřte toto přihlášení, abyste získali přístup k šifrovaným zprávám a dokázali ostatním, že jste to opravdu vy.", From 3345ef25f002321088796824a27990b1462070ab Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sat, 24 Apr 2021 20:31:48 +0000 Subject: [PATCH 135/283] Translated using Weblate (Albanian) Currently translated at 99.6% (2909 of 2920 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index ad768b59cb..c09fd5f928 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3272,5 +3272,16 @@ "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" + "Review to ensure your account is safe": "Shqyrtojeni për t’u siguruar se llogaria është e parrezik", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Jeni i vetmi person këtu. Nëse e braktisni, askush s’do të jetë në gjendje të hyjë në të ardhmen, përfshi ju.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Nëse riktheni gjithçka te parazgjedhjet, do të rifilloni pa sesione të besuara, pa përdorues të besuar, dhe mund të mos jeni në gjendje të shihni mesazhe të dikurshëm.", + "Only do this if you have no other device to complete verification with.": "Bëjeni këtë vetëm nëse s’keni pajisje tjetër me të cilën të plotësoni verifikimin.", + "Reset everything": "Kthe gjithçka te parazgjedhjet", + "Forgotten or lost all recovery methods? Reset all": "Harruat, ose humbët krejt metodat e rimarrjes? Riujdisini të gjitha", + "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": "Nëse e bëni, ju lutemi, kini parasysh se s’do të fshihet asnjë nga mesazhet tuaj, por puna me kërkimin mund degradojë për pak çaste, ndërkohë që rikrijohet treguesi", + "View message": "Shihni mesazh", + "%(seconds)ss left": "Edhe %(seconds)ss", + "Change server ACLs": "Ndryshoni ACL-ra shërbyesi", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Po kryhet këshillim me %(transferTarget)s. Shpërngule te %(transferee)s", + "Show options to enable 'Do not disturb' mode": "Shfaq mundësi për aktivizim të mënyrës “Mos më shqetësoni”" } From 26bb7c08c253c059636e441eb28b0cf072fab056 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 16:20:16 +0100 Subject: [PATCH 136/283] 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 137/283] 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 138/283] 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 139/283] 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 140/283] 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 141/283] 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 142/283] 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 143/283] 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 144/283] 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 145/283] 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 146/283] 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 147/283] 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 148/283] 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 149/283] 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 150/283] 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 151/283] 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 152/283] 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 153/283] 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 154/283] 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 155/283] 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 3cd1a28a1d4cd25e8baf58000779293407a80264 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Mon, 26 Apr 2021 16:04:03 +0000 Subject: [PATCH 156/283] Translated using Weblate (Albanian) Currently translated at 99.5% (2912 of 2924 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index c09fd5f928..305e7dc610 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3283,5 +3283,16 @@ "%(seconds)ss left": "Edhe %(seconds)ss", "Change server ACLs": "Ndryshoni ACL-ra shërbyesi", "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Po kryhet këshillim me %(transferTarget)s. Shpërngule te %(transferee)s", - "Show options to enable 'Do not disturb' mode": "Shfaq mundësi për aktivizim të mënyrës “Mos më shqetësoni”" + "Show options to enable 'Do not disturb' mode": "Shfaq mundësi për aktivizim të mënyrës “Mos më shqetësoni”", + "You can select all or individual messages to retry or delete": "Për riprovim ose fshirje mund të përzgjidhni krejt mesazhet, ose të tillë individualë", + "Sending": "Po dërgohet", + "Retry all": "Riprovoji krejt", + "Delete all": "Fshiji krejt", + "Some of your messages have not been sent": "Disa nga mesazhet tuaj s’janë dërguar", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s anëtarë, përfshi %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Prfshi %(commaSeparatedMembers)s", + "View all %(count)s members|one": "Shihni 1 anëtar", + "View all %(count)s members|other": "Shihni krejt %(count)s anëtarët", + "Failed to send": "S’u arrit të dërgohet" } From 417f662ea783a143ebcfca3ac79199d1b3d3137f Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Mon, 26 Apr 2021 17:12:31 +0100 Subject: [PATCH 157/283] 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 158/283] 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 159/283] 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 160/283] 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 161/283] 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 162/283] 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 163/283] 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 164/283] 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 165/283] 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 166/283] 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 167/283] 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 168/283] 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 169/283] 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 170/283] 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 171/283] 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 172/283] 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 173/283] 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 174/283] 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 175/283] 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 176/283] 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 177/283] 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 178/283] 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 179/283] 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 180/283] 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 181/283] 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 182/283] 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 183/283] 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 184/283] 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 79e03332e32b128226a7e61c58857771e2ef8ae3 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Tue, 27 Apr 2021 16:51:49 -0500 Subject: [PATCH 185/283] Another change from recovery passphrase to Security Phrase Signed-off-by: Aaron Raimist --- .../views/dialogs/security/CreateSecretStorageDialog.js | 4 ++-- src/i18n/strings/en_EN.json | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index 84cb58536a..a7d21349a5 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -647,7 +647,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } return

    {_t( - "Enter your recovery passphrase a second time to confirm it.", + "Enter your Security Phrase a second time to confirm it.", )}

    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f1b700540f..7158156105 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2831,8 +2831,7 @@ "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.", - "Enter your recovery passphrase a second time to confirm it.": "Enter your recovery passphrase a second time to confirm it.", - "Confirm your recovery passphrase": "Confirm your recovery passphrase", + "Enter your Security Phrase a second time to confirm it.": "Enter your Security Phrase a second time to confirm it.", "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.", "Unable to query secret storage status": "Unable to query secret storage status", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", From 6754a0b48388d6349c499f9c7b231bf32c0d6fa4 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Tue, 27 Apr 2021 19:12:20 -0500 Subject: [PATCH 186/283] Switch to
    Signed-off-by: Aaron Raimist --- .../tabs/user/_HelpUserSettingsTab.scss | 31 +++++++++++++ .../views/dialogs/AccessTokenDialog.tsx | 38 ---------------- .../settings/tabs/user/HelpUserSettingsTab.js | 44 +++++++++++++------ src/i18n/strings/en_EN.json | 7 ++- 4 files changed, 64 insertions(+), 56 deletions(-) delete mode 100644 src/components/views/dialogs/AccessTokenDialog.tsx diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss index 109edfff81..0f879d209e 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss @@ -22,3 +22,34 @@ limitations under the License. .mx_HelpUserSettingsTab span.mx_AccessibleButton { word-break: break-word; } + +.mx_HelpUserSettingsTab code { + word-break: break-all; + user-select: all; +} + +.mx_HelpUserSettingsTab_accessToken { + display: flex; + justify-content: space-between; + border-radius: 5px; + border: solid 1px $light-fg-color; + margin-bottom: 10px; + margin-top: 10px; + padding: 10px; +} + +.mx_HelpUserSettingsTab_accessToken_copy { + flex-shrink: 0; + cursor: pointer; + margin-left: 20px; + display: inherit; +} + +.mx_HelpUserSettingsTab_accessToken_copy > div { + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; + margin-left: 5px; + width: 20px; + height: 20px; + background-repeat: no-repeat; +} diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx deleted file mode 100644 index c0b1b929df..0000000000 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd -Copyright 2020 Resynth - -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 { _t } from '../../../languageHandler'; -import QuestionDialog from './QuestionDialog'; - -type IProps = Exclude< - React.ComponentProps, - "title" | "danger" | "description" ->; - -export default function AccessTokenDialog(props: IProps) { - return ( - - ); -} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index e52bc1a3fb..cf1a28ef76 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -20,6 +20,7 @@ import PropTypes from 'prop-types'; import {_t, getCurrentLanguage} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; +import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton'; import SdkConfig from "../../../../../SdkConfig"; import createRoom from "../../../../../createRoom"; import Modal from "../../../../../Modal"; @@ -27,8 +28,11 @@ import * as sdk from "../../../../../"; import PlatformPeg from "../../../../../PlatformPeg"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import UpdateCheckButton from "../../UpdateCheckButton"; -import AccessTokenDialog from '../../../dialogs/AccessTokenDialog'; import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import {copyPlaintext} from "../../../../../utils/strings"; +import * as ContextMenu from "../../../../structures/ContextMenu"; +import {toRightOf} from "../../../../structures/ContextMenu"; + @replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab") export default class HelpUserSettingsTab extends React.Component { @@ -151,15 +155,18 @@ export default class HelpUserSettingsTab extends React.Component { ); } - onAccessTokenSpoilerClick = async (event) => { - // React throws away the event before we can use it (we are async, after all). - event.persist(); + onAccessTokenCopyClick = async (e) => { + e.preventDefault(); + const target = e.target; // copy target before we go async and React throws it away - // We make the user accept a scary popup to combat Social Engineering. No peeking! - await Modal.createTrackedDialog('Reveal Access Token', '', AccessTokenDialog).finished; - - // Pass it onto the handler. - this._showSpoiler(event); + const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken()); + const buttonRect = target.getBoundingClientRect(); + const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu'); + const {close} = ContextMenu.createMenu(GenericTextContextMenu, { + ...toRightOf(buttonRect, 2), + message: successful ? _t('Copied!') : _t('Failed to copy'), + }); + target.onmouseleave = close; } render() { @@ -279,11 +286,20 @@ export default class HelpUserSettingsTab extends React.Component {
    {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
    {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
    - {_t("Access Token:") + ' '} - - <{ _t("click to reveal") }> - +
    +
    + {_t("Access Token")}
    + { _t("Your access token gives full access to your account." + + " Do not share it with anyone." ) } +
    + {MatrixClientPeg.get().getAccessToken()} + +
    +

    {_t("Clear cache and reload")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b110af8749..99451dabd6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1251,8 +1251,9 @@ "olm version:": "olm version:", "Homeserver is": "Homeserver is", "Identity Server is": "Identity Server is", - "Access Token:": "Access Token:", - "click to reveal": "click to reveal", + "Access Token": "Access Token", + "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", + "Copy": "Copy", "Clear cache and reload": "Clear cache and reload", "Labs": "Labs", "Customise your experience with experimental labs features. Learn more.": "Customise your experience with experimental labs features. Learn more.", @@ -2016,7 +2017,6 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", - "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", "Space selection": "Space selection", "Add existing rooms": "Add existing rooms", "Filter your rooms and spaces": "Filter your rooms and spaces", @@ -2336,7 +2336,6 @@ "Share Community": "Share Community", "Share Room Message": "Share Room Message", "Link to selected message": "Link to selected message", - "Copy": "Copy", "Command Help": "Command Help", "Failed to save space settings.": "Failed to save space settings.", "Space settings": "Space settings", From 5e646f861cf592e72ae7eb18e64bd65f15de3245 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 27 Apr 2021 18:59:10 -0600 Subject: [PATCH 187/283] 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 188/283] 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 189/283] 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 190/283] 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 191/283] 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 192/283] 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 193/283] 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 194/283] 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 195/283] 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 196/283] 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 197/283] 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 198/283] 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 199/283] 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 200/283] 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 201/283] 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 202/283] 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 58d83d0309a507ed3060c6bf8832a261e95ef05f Mon Sep 17 00:00:00 2001 From: libexus Date: Tue, 27 Apr 2021 07:05:53 +0000 Subject: [PATCH 203/283] Translated using Weblate (German) Currently translated at 99.0% (2896 of 2924 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 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 5cbdd20a36..54afb5c22a 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3264,5 +3264,10 @@ "Zoom in": "Vergrößern", "Zoom out": "Verkleinern", "%(seconds)ss left": "%(seconds)s vergangen", - "Change server ACLs": "ACLs des Servers bearbeiten" + "Change server ACLs": "ACLs des Servers bearbeiten", + "Failed to send": "Fehler beim Senden", + "View all %(count)s members|other": "Alle %(count)s Mitglieder anzeigen", + "View all %(count)s members|one": "Mitglied anzeigen", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "Some of your messages have not been sent": "Einige Nachrichten konnten nicht gesendet werden" } From 021469044111cbc1cb77d34eb3cf506d2a805e35 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 26 Apr 2021 16:31:02 +0000 Subject: [PATCH 204/283] Translated using Weblate (English (United States)) Currently translated at 20.1% (588 of 2924 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 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index bfc21a81d3..4a97b60a2e 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -658,5 +658,12 @@ "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.", "Use Single Sign On to continue": "Use Single Sign On to continue", - "Message search initilisation failed": "Message search initialization failed" + "Message search initilisation failed": "Message search initialization failed", + "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait while we resynchronize with the server!", + "Customise your experience with experimental labs features. Learn more.": "Customize your experience with experimental labs features. Learn more.", + "Customise your appearance": "Customize your appearance", + "Unrecognised command: %(commandText)s": "Unrecognized command: %(commandText)s", + "Add some details to help people recognise it.": "Add some details to help people recognize it.", + "Unrecognised room address:": "Unrecognized room address:", + "A private space to organise your rooms": "A private space to organize your rooms" } From dd0a30c0dc76755e7e70e5ffb545b8451e7fca8d Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Tue, 27 Apr 2021 06:01:45 +0000 Subject: [PATCH 205/283] Translated using Weblate (French) Currently translated at 100.0% (2924 of 2924 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 5fc5b1d10b..3db86ad59b 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3274,5 +3274,16 @@ "Zoom out": "Dé-zoomer", "%(seconds)ss left": "%(seconds)s secondes restantes", "Change server ACLs": "Modifier les ACL du serveur", - "Show options to enable 'Do not disturb' mode": "Afficher une option pour activer le mode « Ne pas déranger »" + "Show options to enable 'Do not disturb' mode": "Afficher une option pour activer le mode « Ne pas déranger »", + "You can select all or individual messages to retry or delete": "Vous pouvez choisir de renvoyer ou supprimer tous les messages ou seulement certains", + "Sending": "Envoi", + "Retry all": "Tout renvoyer", + "Delete all": "Tout supprimer", + "Some of your messages have not been sent": "Certains de vos messages n’ont pas été envoyés", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s membres dont %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Dont %(commaSeparatedMembers)s", + "View all %(count)s members|one": "Afficher le membre", + "View all %(count)s members|other": "Afficher les %(count)s membres", + "Failed to send": "Échec de l’envoi" } From 4661fd2d2ea39a22a545cd21a932573717f9e47c Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 27 Apr 2021 05:48:56 +0000 Subject: [PATCH 206/283] Translated using Weblate (Hungarian) Currently translated at 100.0% (2924 of 2924 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 252badd54a..e6e5575674 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3292,5 +3292,16 @@ "Zoom in": "Nagyít", "Zoom out": "Kicsinyít", "%(seconds)ss left": "%(seconds)s mp van vissza", - "Show options to enable 'Do not disturb' mode": "Mutassa a lehetőséget a „Ne zavarjanak” módhoz" + "Show options to enable 'Do not disturb' mode": "Mutassa a lehetőséget a „Ne zavarjanak” módhoz", + "You can select all or individual messages to retry or delete": "Újraküldéshez vagy törléshez kiválaszthatja az üzeneteket egyenként vagy az összeset együtt", + "Retry all": "Mind újraküldése", + "Sending": "Küldés", + "Delete all": "Mind törlése", + "Some of your messages have not been sent": "Néhány üzenete nem lett elküldve", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s résztvevő beleértve: %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Beleértve: %(commaSeparatedMembers)s", + "View all %(count)s members|one": "1 résztvevő megmutatása", + "View all %(count)s members|other": "Az összes %(count)s résztvevő megmutatása", + "Failed to send": "Küldés sikertelen" } From e3e0de5f36ca50695a38a597fe9a7a63cdc6c2f4 Mon Sep 17 00:00:00 2001 From: jelv Date: Tue, 27 Apr 2021 11:11:59 +0000 Subject: [PATCH 207/283] Translated using Weblate (Dutch) Currently translated at 100.0% (2924 of 2924 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 10a921b11d..8b2dea7885 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3183,5 +3183,16 @@ "Zoom out": "Uitzoomen", "%(seconds)ss left": "%(seconds)s's over", "Change server ACLs": "Wijzig server ACL's", - "Show options to enable 'Do not disturb' mode": "Toon opties om de 'Niet storen' modus in te schakelen" + "Show options to enable 'Do not disturb' mode": "Toon opties om de 'Niet storen' modus in te schakelen", + "You can select all or individual messages to retry or delete": "U kunt alles selecteren of per individueel bericht opnieuw verzenden of verwijderen", + "Sending": "Wordt verstuurd", + "Retry all": "Alles opnieuw proberen", + "Delete all": "Verwijder alles", + "Some of your messages have not been sent": "Enkele van uw berichten zijn niet verstuurd", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s leden inclusief %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Inclusief %(commaSeparatedMembers)s", + "View all %(count)s members|one": "Bekijk 1 lid", + "View all %(count)s members|other": "Bekijk alle %(count)s leden", + "Failed to send": "Verzenden is mislukt" } From bbb1670b509413ec41c601d6b0457db6695a2add Mon Sep 17 00:00:00 2001 From: vintergatan Date: Tue, 27 Apr 2021 16:12:19 +0000 Subject: [PATCH 208/283] Translated using Weblate (Swedish) Currently translated at 99.7% (2916 of 2924 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 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 3517173ded..63db2ccee2 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3228,5 +3228,8 @@ "Zoom out": "Zooma ut", "%(seconds)ss left": "%(seconds)ss kvar", "Change server ACLs": "Ändra server-ACLer", - "Show options to enable 'Do not disturb' mode": "Visa alternativ för att aktivera 'Stör ej'-läget" + "Show options to enable 'Do not disturb' mode": "Visa alternativ för att aktivera 'Stör ej'-läget", + "Delete all": "Radera alla", + "View all %(count)s members|one": "Visa 1 medlem", + "View all %(count)s members|other": "Visa alla %(count)s medlemmar" } From 7430d0db17922e95244e5db6a3fe90bb9766d3df Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Tue, 27 Apr 2021 02:00:20 +0000 Subject: [PATCH 209/283] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2924 of 2924 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index cedbb8f52c..134f61a435 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3300,5 +3300,16 @@ "Zoom out": "縮小", "%(seconds)ss left": "剩%(seconds)s秒", "Change server ACLs": "變更伺服器 ACL", - "Show options to enable 'Do not disturb' mode": "顯示啟用「勿打擾」模式的選項" + "Show options to enable 'Do not disturb' mode": "顯示啟用「勿打擾」模式的選項", + "You can select all or individual messages to retry or delete": "您可以選取全部或單獨的訊息來重試或刪除", + "Sending": "正在傳送", + "Retry all": "重試全部", + "Delete all": "刪除全部", + "Some of your messages have not been sent": "您的部份訊息未傳送", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s 個成員包含 %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "包含 %(commaSeparatedMembers)s", + "View all %(count)s members|one": "檢視 1 個成員", + "View all %(count)s members|other": "檢視全部 %(count)s 個成員", + "Failed to send": "傳送失敗" } From 434cf65598f6ccc87042fe51ffd4f9be3d10d911 Mon Sep 17 00:00:00 2001 From: Tirifto Date: Tue, 27 Apr 2021 12:59:01 +0000 Subject: [PATCH 210/283] Translated using Weblate (Esperanto) Currently translated at 98.6% (2884 of 2924 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 62e8e1f98a..f4d30b40b7 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -3188,5 +3188,35 @@ "Space options": "Agordoj de aro", "Space Home": "Hejmo de aro", "with state key %(stateKey)s": "kun statŝlosilo %(stateKey)s", - "with an empty state key": "kun malplena statŝlosilo" + "with an empty state key": "kun malplena statŝlosilo", + "Invited people will be able to read old messages.": "Invititoj povos legi malnovajn mesaĝojn.", + "Adding...": "Aldonante…", + "Add existing rooms": "Aldoni jamajn ĉambrojn", + "View message": "Montri mesaĝon", + "Zoom in": "Zomi", + "Zoom out": "Malzomi", + "%(count)s people you know have already joined|one": "%(count)s persono, kiun vi konas, jam aliĝis", + "%(count)s people you know have already joined|other": "%(count)s personoj, kiujn vi konas, jam aliĝis", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s anoj inkluzive je %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Inkluzive je %(commaSeparatedMembers)s", + "View all %(count)s members|one": "Montri 1 anon", + "View all %(count)s members|other": "Montri ĉiujn %(count)s anojn", + "Accept on your other login…": "Akceptu per via alia saluto…", + "Stop & send recording": "Ĉesi kaj sendi registrajon", + "Record a voice message": "Registri voĉmesaĝon", + "Quick actions": "Rapidaj agoj", + "Invite to just this room": "Inviti nur al ĉi tiu ĉambro", + "%(seconds)ss left": "%(seconds)s sekundoj restas", + "Failed to send": "Malsukcesis sendi", + "Change server ACLs": "Ŝanĝi servilblokajn listojn", + "Warn before quitting": "Averti antaŭ ĉesigo", + "Workspace: ": "Laborspaco: ", + "Manage & explore rooms": "Administri kaj esplori ĉambrojn", + "unknown person": "nekonata persono", + "Send and receive voice messages (in development)": "Sendi kaj ricevi voĉmesaĝojn (evoluigate)", + "Show options to enable 'Do not disturb' mode": "Montri elekteblojn por ŝalti sendistran reĝimon", + "%(deviceId)s from %(ip)s": "%(deviceId)s de %(ip)s", + "Review to ensure your account is safe": "Kontrolu por certigi sekurecon de via konto", + "Sends the given message as a spoiler": "Sendas la donitan mesaĝon kiel malkaŝon de intrigo" } From 70d0df53256a5081694b43b9a635a6ea391f8296 Mon Sep 17 00:00:00 2001 From: random Date: Tue, 27 Apr 2021 15:57:04 +0000 Subject: [PATCH 211/283] Translated using Weblate (Italian) Currently translated at 100.0% (2924 of 2924 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 5e53aced69..98272f9e49 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3297,5 +3297,16 @@ "Zoom out": "Rimpicciolisci", "%(seconds)ss left": "%(seconds)ss rimanenti", "Change server ACLs": "Modifica le ACL del server", - "Show options to enable 'Do not disturb' mode": "Mostra opzioni per attivare la modalità \"Non disturbare\"" + "Show options to enable 'Do not disturb' mode": "Mostra opzioni per attivare la modalità \"Non disturbare\"", + "You can select all or individual messages to retry or delete": "Puoi selezionare tutti o alcuni messaggi da riprovare o eliminare", + "Sending": "Invio in corso", + "Retry all": "Riprova tutti", + "Delete all": "Elimina tutti", + "Some of your messages have not been sent": "Alcuni tuoi messaggi non sono stati inviati", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s membri inclusi %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Inclusi %(commaSeparatedMembers)s", + "View all %(count)s members|one": "Vedi 1 membro", + "View all %(count)s members|other": "Vedi tutti i %(count)s membri", + "Failed to send": "Invio fallito" } From bd5178d96fdaf7f2d0eece9130d83e2e9a2252b3 Mon Sep 17 00:00:00 2001 From: Hivaa Date: Tue, 27 Apr 2021 11:48:40 +0000 Subject: [PATCH 212/283] Translated using Weblate (Persian) Currently translated at 20.5% (600 of 2924 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 | 322 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 320 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index d036a55c23..5948048561 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -223,7 +223,7 @@ "Home": "خانه", "Hangup": "قطع", "For security, this session has been signed out. Please sign in again.": "برای امنیت، این نشست نامعتبر شده است. لطفاً دوباره وارد سیستم شوید.", - "We couldn't log you in": "ما نتوانستیم شما را وارد حسابتان کنیم", + "We couldn't log you in": "نتوانستیم شما را وارد کنیم", "Trust": "اعتماد کن", "Only continue if you trust the owner of the server.": "تنها در صورتی که به صاحب سرور اطمینان دارید، ادامه دهید.", "Identity server has no terms of service": "سرور هویت‌سنجی، شرایط استفاده از خدمت (terms of service) را مشخص نکرده‌است", @@ -314,5 +314,323 @@ "e.g. %(exampleValue)s": "برای مثال %(exampleValue)s", "Explore rooms": "کاوش اتاق", "Sign In": "ورود", - "Create Account": "ایجاد اکانت" + "Create Account": "ایجاد اکانت", + "Use an identity server": "از سرور هویت‌سنجی استفاده کنید", + "Invites user with given id to current room": "کاربر با شناسه داده شده را به اتاق فعلی دعوت کن", + "Sets the room name": "نام اتاق را تنظیم می کند", + "This room has no topic.": "این اتاق هیچ موضوعی ندارد.", + "Failed to set topic": "تنظیم موضوع موفقیت‌آمیز نبود", + "Gets or sets the room topic": "موضوع اتاق را دریافت یا تنظیم می‌کند", + "Changes your avatar in all rooms": "تصویر نمایه خود را در همه‌ی اتاق‌ها تغییر دهید", + "Changes your avatar in this current room only": "تصویر نمایه خود را تنها در این اتاق تغییر دهید", + "Changes the avatar of the current room": "تصویر نمایه اتاق فعلی را تغییر دهید", + "Changes your display nickname": "نام نمایشی خود را تغییر دهید", + "Changes your display nickname in the current room only": "نام نمایشی خود را تنها در اتاق فعلی تغییر دهید", + "Double check that your server supports the room version chosen and try again.": "بررسی کنید که کارگزار شما از نسخه اتاق انتخاب‌شده پشتیبانی کرده و دوباره امتحان کنید.", + "Error upgrading room": "خطا در ارتقاء نسخه اتاق", + "You do not have the required permissions to use this command.": "شما مجوزهای لازم را برای استفاده از این دستور ندارید.", + "Upgrades a room to a new version": "یک اتاق را به نسخه جدید ارتقا دهید", + "To use it, just wait for autocomplete results to load and tab through them.": "برای استفاده از آن ، لطفا منتظر بمانید تا نتایج تکمیل خودکار بارگیری شده و آنها را مرور کنید.", + "Searches DuckDuckGo for results": "در سایت DuckDuckGo جستجو می‌کند", + "Sends a message as html, without interpreting it as markdown": "پیام را به صورت html می فرستد ، بدون اینکه آن را به عنوان markdown تفسیر کند", + "Sends a message as plain text, without interpreting it as markdown": "پیام را به صورت متن ساده و بدون تفسیر آن به عنوان markdown ارسال می کند", + "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "(͡ ° ͜ʖ ͡ °) را به ابتدای یک پیام متنی ساده اضافه می‌کند", + "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "┬──┬ ノ (゜ - ゜ ノ) را به ابتدای یک پیام متنی ساده اضافه می‌کند", + "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "(╯ ° □ °) ╯︵ ┻━┻) را به ابتدای یک پیام متنی ساده اضافه می‌کند", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "¯ \\ _ (ツ) _ / ¯ را به ابتدای یک پیام متنی ساده اضافه می‌کند", + "Sends the given message as a spoiler": "پیام داده شده را به عنوان اسپویلر ارسال می کند", + "Usage": "استفاده", + "Other": "دیگر", + "Effects": "جلوه‌ها", + "Actions": "اقدامات", + "Messages": "پیام ها", + "Setting up keys": "تنظیم کلیدها", + "Go Back": "برگرد", + "Are you sure you want to cancel entering passphrase?": "آیا مطمئن هستید که می خواهید وارد کردن عبارت امنیتی را لغو کنید؟", + "Cancel entering passphrase?": "وارد کردن عبارت امنیتی لغو شود؟", + "Missing room_id in request": "room_id در صورت درخواست وجود ندارد", + "Missing user_id in request": "user_id در صورت درخواست وجود ندارد", + "Room %(roomId)s not visible": "اتاق %(roomId)s قابل مشاهده نیست", + "You do not have permission to do that in this room.": "شما مجاز به انجام این کار در این اتاق نیستید.", + "You are not in this room.": "شما در این اتاق نیستید.", + "Power level must be positive integer.": "سطح قدرت باید عدد صحیح مثبت باشد.", + "This room is not recognised.": "این اتاق شناخته نشده است.", + "Missing roomId.": "شناسه‌ی اتاق گم‌شده", + "Unable to create widget.": "ایجاد ابزارک امکان پذیر نیست.", + "You need to be able to invite users to do that.": "نیاز است که شما قادر به دعوت کاربران به آن باشید.", + "You need to be logged in.": "شما باید وارد شوید.", + "Failed to invite the following users to the %(roomName)s room:": "دعوت کاربران زیر به اتاق %(roomName)s موفقیت‌آمیز نبود:", + "Failed to invite users to the room:": "دعوت کاربران به اتاق موفقیت‌آمیز نبود:", + "Failed to invite": "دعوت موفقیت‌آمیز نبود", + "Custom (%(level)s)": "%(level)s دلخواه", + "Moderator": "معاون", + "Restricted": "ممنوع", + "Use your account or create a new one to continue.": "برای ادامه کار از حساب کاربری خود استفاده کرده و یا حساب کاربری جدیدی ایجاد کنید.", + "Sign In or Create Account": "وارد شوید یا حساب کاربری بسازید", + "Zimbabwe": "زیمبابوه", + "Zambia": "زامبیا", + "Yemen": "یمن", + "Western Sahara": "صحرای غربی", + "Wallis & Futuna": "والیس و فوتونا", + "Vietnam": "ویتنام", + "Venezuela": "ونزوئلا", + "Vatican City": "شهر واتیکان", + "Vanuatu": "وانواتو", + "Uzbekistan": "ازبکستان", + "Uruguay": "اروگوئه", + "United Arab Emirates": "امارات متحده عربی", + "Ukraine": "اوکراین", + "Uganda": "اوگاندا", + "U.S. Virgin Islands": "جزایر ویرجین ایالات متحده", + "Tuvalu": "تووالو", + "Turks & Caicos Islands": "جزایر ترک و کایکوس", + "Turkmenistan": "ترکمنستان", + "Turkey": "بوقلمون", + "Tunisia": "تونس", + "Trinidad & Tobago": "ترینیداد و توباگو", + "Tonga": "تونگا", + "Tokelau": "توكلائو", + "Togo": "رفتن", + "Timor-Leste": "تیمور-لسته", + "Thailand": "تایلند", + "Tanzania": "تانزانیا", + "Tajikistan": "تاجیکستان", + "Taiwan": "تایوان", + "São Tomé & Príncipe": "سائو تومه و پرنسیپ", + "Syria": "سوریه", + "Switzerland": "سوئیس", + "Sweden": "سوئد", + "Swaziland": "سوازیلند", + "Svalbard & Jan Mayen": "سوالبارد و جان ماین", + "Suriname": "سورینام", + "Sudan": "سودان", + "St. Vincent & Grenadines": "سنت وینسنت و گرنادین ها", + "St. Pierre & Miquelon": "سنت پیر و میکلون", + "St. Martin": "سنت مارتین", + "St. Lucia": "سنت لوسیا", + "St. Kitts & Nevis": "سنت کیتس و نویس", + "St. Helena": "سنت هلنا", + "St. Barthélemy": "سنت بارتلمی", + "Sri Lanka": "سری لانکا", + "Spain": "اسپانیا", + "South Sudan": "سودان جنوبی", + "South Korea": "کره جنوبی", + "South Georgia & South Sandwich Islands": "جزایر جورجیا جنوبی", + "South Africa": "آفریقای جنوبی", + "Somalia": "سومالی", + "Solomon Islands": "جزایر سلیمان", + "Slovenia": "اسلوونی", + "Slovakia": "اسلواکی", + "Sint Maarten": "سینت مارتن", + "Singapore": "سنگاپور", + "Sierra Leone": "سیرا لئون", + "Seychelles": "سیشل", + "Serbia": "صربستان", + "Senegal": "سنگال", + "Saudi Arabia": "عربستان سعودی", + "San Marino": "سان مارینو", + "Samoa": "ساموآ", + "Réunion": "ریونیون", + "Rwanda": "رواندا", + "Russia": "روسیه", + "Romania": "رومانی", + "Qatar": "قطر", + "Puerto Rico": "پورتوریکو", + "Portugal": "کشور پرتغال", + "Poland": "لهستان", + "Pitcairn Islands": "جزایر پیتکرن", + "Philippines": "فیلیپین", + "Peru": "پرو", + "Paraguay": "پاراگوئه", + "Papua New Guinea": "پاپوآ گینه نو", + "Panama": "پاناما", + "Palestine": "فلسطین", + "Palau": "پالائو", + "Pakistan": "پاکستان", + "Oman": "عمان", + "Norway": "نروژ", + "Northern Mariana Islands": "جزایر ماریانای شمالی", + "North Korea": "کره شمالی", + "Norfolk Island": "جزیره نورفولک", + "Niue": "نیوئه", + "Nigeria": "نیجریه", + "Niger": "نیجر", + "Nicaragua": "نیکاراگوئه", + "New Zealand": "نیوزلند", + "New Caledonia": "کالدونیای جدید", + "Netherlands": "هلند", + "Nepal": "نپال", + "Nauru": "نائورو", + "Namibia": "ناميبيا", + "Myanmar": "میانمار", + "Mozambique": "موزامبیک", + "Morocco": "مراکش", + "Montserrat": "مونتسرات", + "Montenegro": "مونته نگرو", + "Mongolia": "مغولستان", + "Monaco": "موناکو", + "Moldova": "مولداوی", + "Micronesia": "میکرونزی", + "Mexico": "مکزیک", + "Mayotte": "مایوت", + "Mauritius": "موریس", + "Mauritania": "موریتانی", + "Martinique": "مارتینیک", + "Marshall Islands": "جزایر مارشال", + "Malta": "مالت", + "Mali": "مالی", + "Maldives": "مالدیو", + "Malaysia": "مالزی", + "Malawi": "مالاوی", + "Madagascar": "ماداگاسکار", + "Macedonia": "مقدونیه", + "Macau": "ماکائو", + "Luxembourg": "لوکزامبورگ", + "Lithuania": "لیتوانی", + "Liechtenstein": "لیختن اشتاین", + "Libya": "لیبی", + "Liberia": "لیبریا", + "Lesotho": "لسوتو", + "Lebanon": "لبنان", + "Latvia": "لتونی", + "Laos": "لائوس", + "Kyrgyzstan": "قرقیزستان", + "Kuwait": "کویت", + "Kosovo": "کوزوو", + "Kiribati": "کیریباتی", + "Kenya": "کنیا", + "Kazakhstan": "قزاقستان", + "Jordan": "اردن", + "Jersey": "جرسی", + "Japan": "ژاپن", + "Jamaica": "جامائیکا", + "Italy": "ایتالیا", + "Israel": "رژیم غاصب صهیونیستی", + "Isle of Man": "جزیره من", + "Ireland": "ایرلند", + "Iraq": "عراق", + "Iran": "ایران", + "Indonesia": "اندونزی", + "India": "هند", + "Iceland": "ایسلند", + "Hungary": "مجارستان", + "Hong Kong": "هنگ کنگ", + "Honduras": "هندوراس", + "Heard & McDonald Islands": "جزایر هرد و مک دونالد", + "Haiti": "هائیتی", + "Guyana": "گویان", + "Guinea-Bissau": "گینه بیسائو", + "Guinea": "گینه", + "Guernsey": "گرنزی", + "Guatemala": "گواتمالا", + "Guam": "گوام", + "Guadeloupe": "گوادلوپ", + "Grenada": "گرنادا", + "Greenland": "گرینلند", + "Greece": "یونان", + "Gibraltar": "جبل الطارق", + "Ghana": "غنا", + "Germany": "آلمان", + "Georgia": "گرجستان", + "Gambia": "گامبیا", + "Gabon": "گابن", + "French Southern Territories": "سرزمین های جنوبی فرانسه", + "French Polynesia": "پلینزی فرانسه", + "French Guiana": "گویان فرانسه", + "France": "فرانسه", + "Finland": "فنلاند", + "Fiji": "فیجی", + "Faroe Islands": "جزایر فارو", + "Falkland Islands": "جزایر فالکلند", + "Ethiopia": "اتیوپی", + "Estonia": "استونی", + "Eritrea": "اریتره", + "Equatorial Guinea": "گینه استوایی", + "El Salvador": "السالوادور", + "Egypt": "مصر", + "Ecuador": "اکوادور", + "Dominican Republic": "جمهوری دومینیکن", + "Dominica": "دومینیکا", + "Djibouti": "جیبوتی", + "Denmark": "دانمارک", + "Côte d’Ivoire": "ساحل عاج", + "Czech Republic": "جمهوری چک", + "Cyprus": "قبرس", + "Curaçao": "کوراسائو", + "Cuba": "کوبا", + "Croatia": "کرواسی", + "Costa Rica": "کاستاریکا", + "Cook Islands": "جزایر کوک", + "Congo - Kinshasa": "کنگو - کینشاسا", + "Congo - Brazzaville": "کنگو - برازاویل", + "Comoros": "کومور", + "Colombia": "کلمبیا", + "Cocos (Keeling) Islands": "جزایر کوکوس (کیلینگ)", + "Christmas Island": "جزیره کریسمس", + "China": "چین", + "Chile": "شیلی", + "Chad": "چاد", + "Central African Republic": "جمهوری آفریقای مرکزی", + "Cayman Islands": "جزایر کیمن", + "Caribbean Netherlands": "کارائیب هلند", + "Cape Verde": "کیپ ورد", + "Canada": "کانادا", + "Cameroon": "کامرون", + "Cambodia": "کامبوج", + "Burundi": "بوروندی", + "Burkina Faso": "بورکینافاسو", + "Bulgaria": "بلغارستان", + "Brunei": "برونئی", + "British Virgin Islands": "جزایر ویرجین بریتانیا", + "British Indian Ocean Territory": "قلمرو اقیانوس هند بریتانیا", + "Brazil": "برزیل", + "Bouvet Island": "جزیره بووت", + "Botswana": "بوتسوانا", + "Bosnia": "بوسنی", + "Bolivia": "بولیوی", + "Bhutan": "بوتان", + "Bermuda": "برمودا", + "Benin": "بنین", + "Belize": "بلیز", + "Belgium": "بلژیک", + "Belarus": "بلاروس", + "Barbados": "باربادوس", + "Bangladesh": "بنگلادش", + "Bahrain": "بحرین", + "Bahamas": "باهاما", + "Azerbaijan": "آذربایجان", + "Austria": "اتریش", + "Australia": "استرالیا", + "Aruba": "آروبا", + "Armenia": "ارمنستان", + "Argentina": "آرژانتین", + "Antigua & Barbuda": "آنتیگوا و باربودا", + "Antarctica": "جنوبگان", + "Anguilla": "آنگویلا", + "Angola": "آنگولا", + "Andorra": "آندورا", + "American Samoa": "ساموآ آمریکایی", + "Algeria": "الجزایر", + "Albania": "آلبانی", + "Åland Islands": "جزایر الند", + "Afghanistan": "افغانستان", + "United States": "ایالات متحده", + "United Kingdom": "انگلستان", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "به نظر نمی‌رسد آدرس ایمیل شما با هیچ کدام از شناسه ماتریکس در این سرور مرتبط باشد.", + "This email address was not found": "این آدرس ایمیل یافت نشد", + "Unable to enable Notifications": "فعال کردن اعلان ها امکان پذیر نیست", + "%(brand)s was not given permission to send notifications - please try again": "به %(brand)s اجازه ارسال اعلان داده نشده است - لطفاً دوباره امتحان کنید", + "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s اجازه ارسال اعلان به شما را ندارد - لطفاً تنظیمات مرورگر خود را بررسی کنید", + "%(name)s is requesting verification": "%(name)s درخواست تائید دارد", + "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.": "سرور درخواست ورود شما را رد کرد. این می‌تواند به خاطر طولانی شدن فرآیندها باشد. لطفا دوباره امتحان کنید. اگر این مشکل ادامه داشت، لطفا با مدیر سرور تماس بگیرید.", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "سرور شما در دسترس نبود و امکان ورود شما میسر نیست. لطفا دوباره امتحان کنید. اگر مشکل ادامه داشت، لطفا با مدیر سرور تماس بگیرید.", + "Try again": "دوباره امتحان کنید", + "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "این که شما از %(brand)s روی دستگاهی استفاده می‌کنید که در آن لامسه مکانیزم اصلی ورودی است", + "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.": "ما از مرورگر خواستیم تا سروری را که شما برای ورود استفاده می‌کنید به خاطر بسپارد، اما متاسفانه مرورگر شما آن را فراموش کرده‌است. به صفحه‌ی ورود بروید و دوباره امتحان کنید.", + "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.": "این اقدام نیاز به دسترسی به سرور هویت‌سنجی پیش‌فرض برای تایید آدرس ایمیل یا شماره تماس دارد، اما کارگزار هیچ گونه شرایط خدماتی (terms of service) ندارد.", + "The remote side failed to pick up": "طرف دیگر قادر به پاسخ‌دادن نیست", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "هرگاه این صفحه شامل اطلاعات قابل شناسایی مانند شناسه‌ی اتاق ، کاربر یا گروه باشد ، این داده‌ها قبل از ارسال به سرور حذف می شوند.", + "Your user agent": "نماینده کاربری شما", + "Whether you're using %(brand)s as an installed Progressive Web App": "این که آیا شما از%(brand)s به عنوان یک PWA استفاده می‌کنید یا نه", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "این که آیا از ویژگی 'breadcrumbs' (نمایه‌ی کاربری بالای فهرست اتاق‌ها) استفاده می‌کنید یا خیر" } From 18bd19c3fb0357d652d368c249eeeacab0cc89fb Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Mon, 26 Apr 2021 17:31:35 +0000 Subject: [PATCH 213/283] Translated using Weblate (Czech) Currently translated at 100.0% (2924 of 2924 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index ac81b49d95..714d34abfa 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3214,5 +3214,16 @@ "Zoom out": "Oddálit", "%(seconds)ss left": "Zbývá %(seconds)ss", "Change server ACLs": "Změnit seznamy přístupů serveru", - "Show options to enable 'Do not disturb' mode": "Zobrazit možnosti pro povolení režimu „Nerušit“" + "Show options to enable 'Do not disturb' mode": "Zobrazit možnosti pro povolení režimu „Nerušit“", + "You can select all or individual messages to retry or delete": "Můžete vybrat všechny nebo jednotlivé zprávy, které chcete zkusit poslat znovu nebo odstranit", + "Sending": "Odesílání", + "Retry all": "Zkusit všechny znovu", + "Delete all": "Smazat všechny", + "Some of your messages have not been sent": "Některé z vašich zpráv nebyly odeslány", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s členů včetně %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Včetně %(commaSeparatedMembers)s", + "View all %(count)s members|one": "Zobrazit jednoho člena", + "View all %(count)s members|other": "Zobrazit všech %(count)s členů", + "Failed to send": "Odeslání se nezdařilo" } From 9c6ae0de8ad5beb346e8ee9b6c73f57d45a8f94a Mon Sep 17 00:00:00 2001 From: XoseM Date: Tue, 27 Apr 2021 05:03:05 +0000 Subject: [PATCH 214/283] Translated using Weblate (Galician) Currently translated at 100.0% (2924 of 2924 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 1300815a4c..7b816e94cd 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3297,5 +3297,16 @@ "Zoom out": "Alonxar", "%(seconds)ss left": "%(seconds)ss restantes", "Change server ACLs": "Cambiar ACLs do servidor", - "Show options to enable 'Do not disturb' mode": "Mostrar opcións para activar o modo 'Non molestar'" + "Show options to enable 'Do not disturb' mode": "Mostrar opcións para activar o modo 'Non molestar'", + "You can select all or individual messages to retry or delete": "Podes elexir todo ou mensaxes individuais para reintentar ou eliminar", + "Sending": "Enviando", + "Retry all": "Reintentar todo", + "Delete all": "Eliminar todo", + "Some of your messages have not been sent": "Algunha das túas mensaxes non se enviou", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s membros, incluíndo a %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Incluíndo a %(commaSeparatedMembers)s", + "View all %(count)s members|one": "Ver 1 membro", + "View all %(count)s members|other": "Ver tódolos %(count)s membros", + "Failed to send": "Fallou o envío" } From d422066a72c22f281e69f9ed2f11f58fcd5a95ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 26 Apr 2021 15:40:44 +0000 Subject: [PATCH 215/283] Translated using Weblate (Estonian) Currently translated at 100.0% (2924 of 2924 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 282b3f77cf..56f456b6e4 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3275,5 +3275,16 @@ "Zoom out": "Suumi välja", "%(seconds)ss left": "jäänud %(seconds)s sekundit", "Change server ACLs": "Muuda serveri ligipääsuõigusi", - "Show options to enable 'Do not disturb' mode": "Näita valikuid „Ära sega“ režiimi sisse lülitamiseks" + "Show options to enable 'Do not disturb' mode": "Näita valikuid „Ära sega“ režiimi sisse lülitamiseks", + "You can select all or individual messages to retry or delete": "Sa võid valida kas kõik või mõned sõnumid kas kustutamiseks või uuesti saatmiseks", + "Sending": "Saadan", + "Retry all": "Proovi kõikidega uuesti", + "Delete all": "Kustuta kõik", + "Some of your messages have not been sent": "Mõned sinu sõnumid on saatmata", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s liiget, sealhulgas %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Sealhulgas %(commaSeparatedMembers)s", + "View all %(count)s members|one": "Vaata üht liiget", + "View all %(count)s members|other": "Vaata kõiki %(count)s liiget", + "Failed to send": "Saatmine ei õnnestunud" } From 22ac0359743dc99cd93a04f61702d255798647e9 Mon Sep 17 00:00:00 2001 From: Hakim Oubouali Date: Tue, 27 Apr 2021 09:35:53 +0000 Subject: [PATCH 216/283] Translated using Weblate (Central Atlas Tamazight) Currently translated at 1.1% (35 of 2924 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/tzm/ --- src/i18n/strings/tzm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/tzm.json b/src/i18n/strings/tzm.json index ba63af5fb0..6b0fa316e6 100644 --- a/src/i18n/strings/tzm.json +++ b/src/i18n/strings/tzm.json @@ -4,7 +4,7 @@ "Actions": "Tugawin", "Messages": "Tuzinin", "Cancel": "Sser", - "Create Account": "Ssenflul amiḍan", + "Create Account": "senflul amiḍan", "Sign In": "Kcem", "Name or Matrix ID": "Isem neɣ ID Matrix", "Dec": "Duj", 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 217/283] 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 218/283] 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 bed52319bcc46c67df9c6d561908faec5a939ffb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 28 Apr 2021 23:39:24 +0100 Subject: [PATCH 219/283] Sort rooms in the add existing to space dialog based on recency --- .../dialogs/AddExistingToSpaceDialog.tsx | 7 +- .../algorithms/tag-sorting/RecentAlgorithm.ts | 140 +++++++++--------- 2 files changed, 77 insertions(+), 70 deletions(-) diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 1f45a76008..2253b525e0 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, {useContext, useState} from "react"; +import React, {useContext, useMemo, useState} from "react"; import classNames from "classnames"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; @@ -34,6 +34,7 @@ import DMRoomMap from "../../../utils/DMRoomMap"; import {calculateRoomVia} from "../../../utils/permalinks/Permalinks"; import StyledCheckbox from "../elements/StyledCheckbox"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -57,6 +58,8 @@ interface IAddExistingToSpaceProps { export const AddExistingToSpace: React.FC = ({ space, selected, onChange }) => { const cli = useContext(MatrixClientContext); + const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]); + const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); @@ -65,7 +68,7 @@ export const AddExistingToSpace: React.FC = ({ space, const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId)); const joinRule = space.getJoinRule(); - const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => { + const [spaces, rooms, dms] = visibleRooms.reduce((arr, room) => { if (room.getMyMembership() !== "join") return arr; if (!room.name.toLowerCase().includes(lcQuery)) return arr; diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts index 7c8c879cf6..49cfd9e520 100644 --- a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts @@ -21,79 +21,83 @@ import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import * as Unread from "../../../../Unread"; import { EffectiveMembership, getEffectiveMembership } from "../../../../utils/membership"; +export const sortRooms = (rooms: Room[]): Room[] => { + // We cache the timestamp lookup to avoid iterating forever on the timeline + // of events. This cache only survives a single sort though. + // We wouldn't need this if `.sort()` didn't constantly try and compare all + // of the rooms to each other. + + // TODO: We could probably improve the sorting algorithm here by finding changes. + // See https://github.com/vector-im/element-web/issues/14459 + // For example, if we spent a little bit of time to determine which elements have + // actually changed (probably needs to be done higher up?) then we could do an + // insertion sort or similar on the limited set of changes. + + // TODO: Don't assume we're using the same client as the peg + // See https://github.com/vector-im/element-web/issues/14458 + let myUserId = ''; + if (MatrixClientPeg.get()) { + myUserId = MatrixClientPeg.get().getUserId(); + } + + const tsCache: { [roomId: string]: number } = {}; + const getLastTs = (r: Room) => { + if (tsCache[r.roomId]) { + return tsCache[r.roomId]; + } + + const ts = (() => { + // Apparently we can have rooms without timelines, at least under testing + // environments. Just return MAX_INT when this happens. + if (!r || !r.timeline) { + return Number.MAX_SAFE_INTEGER; + } + + // If the room hasn't been joined yet, it probably won't have a timeline to + // parse. We'll still fall back to the timeline if this fails, but chances + // are we'll at least have our own membership event to go off of. + const effectiveMembership = getEffectiveMembership(r.getMyMembership()); + if (effectiveMembership !== EffectiveMembership.Join) { + const membershipEvent = r.currentState.getStateEvents("m.room.member", myUserId); + if (membershipEvent && !Array.isArray(membershipEvent)) { + return membershipEvent.getTs(); + } + } + + for (let i = r.timeline.length - 1; i >= 0; --i) { + const ev = r.timeline[i]; + if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?) + + if (ev.getSender() === myUserId || Unread.eventTriggersUnreadCount(ev)) { + return ev.getTs(); + } + } + + // we might only have events that don't trigger the unread indicator, + // in which case use the oldest event even if normally it wouldn't count. + // This is better than just assuming the last event was forever ago. + if (r.timeline.length && r.timeline[0].getTs()) { + return r.timeline[0].getTs(); + } else { + return Number.MAX_SAFE_INTEGER; + } + })(); + + tsCache[r.roomId] = ts; + return ts; + }; + + return rooms.sort((a, b) => { + return getLastTs(b) - getLastTs(a); + }); +}; + /** * Sorts rooms according to the last event's timestamp in each room that seems * useful to the user. */ export class RecentAlgorithm implements IAlgorithm { public async sortRooms(rooms: Room[], tagId: TagID): Promise { - // We cache the timestamp lookup to avoid iterating forever on the timeline - // of events. This cache only survives a single sort though. - // We wouldn't need this if `.sort()` didn't constantly try and compare all - // of the rooms to each other. - - // TODO: We could probably improve the sorting algorithm here by finding changes. - // See https://github.com/vector-im/element-web/issues/14459 - // For example, if we spent a little bit of time to determine which elements have - // actually changed (probably needs to be done higher up?) then we could do an - // insertion sort or similar on the limited set of changes. - - // TODO: Don't assume we're using the same client as the peg - // See https://github.com/vector-im/element-web/issues/14458 - let myUserId = ''; - if (MatrixClientPeg.get()) { - myUserId = MatrixClientPeg.get().getUserId(); - } - - const tsCache: { [roomId: string]: number } = {}; - const getLastTs = (r: Room) => { - if (tsCache[r.roomId]) { - return tsCache[r.roomId]; - } - - const ts = (() => { - // Apparently we can have rooms without timelines, at least under testing - // environments. Just return MAX_INT when this happens. - if (!r || !r.timeline) { - return Number.MAX_SAFE_INTEGER; - } - - // If the room hasn't been joined yet, it probably won't have a timeline to - // parse. We'll still fall back to the timeline if this fails, but chances - // are we'll at least have our own membership event to go off of. - const effectiveMembership = getEffectiveMembership(r.getMyMembership()); - if (effectiveMembership !== EffectiveMembership.Join) { - const membershipEvent = r.currentState.getStateEvents("m.room.member", myUserId); - if (membershipEvent && !Array.isArray(membershipEvent)) { - return membershipEvent.getTs(); - } - } - - for (let i = r.timeline.length - 1; i >= 0; --i) { - const ev = r.timeline[i]; - if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?) - - if (ev.getSender() === myUserId || Unread.eventTriggersUnreadCount(ev)) { - return ev.getTs(); - } - } - - // we might only have events that don't trigger the unread indicator, - // in which case use the oldest event even if normally it wouldn't count. - // This is better than just assuming the last event was forever ago. - if (r.timeline.length && r.timeline[0].getTs()) { - return r.timeline[0].getTs(); - } else { - return Number.MAX_SAFE_INTEGER; - } - })(); - - tsCache[r.roomId] = ts; - return ts; - }; - - return rooms.sort((a, b) => { - return getLastTs(b) - getLastTs(a); - }); + return sortRooms(rooms); } } 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 220/283] 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 7552801103f8ed8a2c1d11673cc6fc10f3e8686f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 29 Apr 2021 09:48:23 +0100 Subject: [PATCH 221/283] Room List respect the prefilter for new rooms --- src/stores/room-list/RoomListStore.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index caab46a0c2..2ed3a2718d 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -426,6 +426,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient { return; // don't do anything on rooms that aren't visible } + if (cause === RoomUpdateCause.NewRoom && !this.prefilterConditions.every(c => c.isVisible(room))) { + return; // don't do anything on new rooms which ought not to be shown + } + const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); if (shouldUpdate) { if (SettingsStore.getValue("advancedRoomListLogging")) { From 286463606c14258975d9355c6b53b920de93a935 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 29 Apr 2021 13:52:48 +0100 Subject: [PATCH 222/283] Spaces fix edge cases around automatic space switching around room navigation --- src/stores/SpaceStore.tsx | 49 +++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index ec6227e45e..8e86253f75 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -410,6 +410,22 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); }, 100, {trailing: true, leading: true}); + private switchToRelatedSpace = (roomId: string) => { + if (this.suggestedRooms.find(r => r.room_id === roomId)) return; + + let parent = this.getCanonicalParent(roomId); + if (!parent) { + parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId)); + } + if (!parent) { + const parents = Array.from(this.parentMap.get(roomId) || []); + parent = parents.find(p => this.matrixClient.getRoom(p)); + } + + // don't trigger a context switch when we are switching a space to match the chosen room + this.setActiveSpace(parent || null, false); + }; + private onRoom = (room: Room, newMembership?: string, oldMembership?: string) => { const membership = newMembership || room.getMyMembership(); @@ -424,6 +440,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (numSuggestedRooms !== this._suggestedRooms.length) { this.emit(SUGGESTED_ROOMS, this._suggestedRooms); } + + // if the room currently being viewed was just joined then switch to its related space + if (newMembership === "join" && room.roomId === RoomViewStore.getRoomId()) { + this.switchToRelatedSpace(room.roomId); + } } return; } @@ -442,7 +463,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (membership === "join" && room.roomId === RoomViewStore.getRoomId()) { // if the user was looking at the space and then joined: select that space - this.setActiveSpace(room); + this.setActiveSpace(room, false); } }; @@ -542,10 +563,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // restore selected state from last session if any and still valid const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY); if (lastSpaceId) { - const space = this.rootSpaces.find(s => s.roomId === lastSpaceId); - if (space) { - this.setActiveSpace(space); - } + this.setActiveSpace(this.matrixClient.getRoom(lastSpaceId)); } } @@ -553,27 +571,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (!SettingsStore.getValue("feature_spaces")) return; switch (payload.action) { 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 (!room || payload.context_switch) break; + if (payload.context_switch) break; - if (room.isSpaceRoom()) { + const roomId = payload.room_id; + const room = this.matrixClient?.getRoom(roomId); + if (room?.isSpaceRoom()) { // 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)) { - let parent = this.getCanonicalParent(room.roomId); - if (!parent) { - parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(room.roomId)); - } - if (!parent) { - const parents = Array.from(this.parentMap.get(room.roomId) || []); - parent = parents.find(p => this.matrixClient.getRoom(p)); - } - // don't trigger a context switch when we are switching a space to match the chosen room - this.setActiveSpace(parent || null, false); + } else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) { + this.switchToRelatedSpace(roomId); } // Persist last viewed room from a space From ce28e5f122a5e03542b73456eef4802d43f83ea0 Mon Sep 17 00:00:00 2001 From: Christian Paul Date: Thu, 29 Apr 2021 14:56:18 +0000 Subject: [PATCH 223/283] Translated using Weblate (German) Currently translated at 99.0% (2895 of 2924 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 54afb5c22a..b17b28245e 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2028,7 +2028,7 @@ "Doesn't look like a valid email address": "Das sieht nicht nach einer gültigen E-Mail-Adresse aus", "Enter phone number (required on this homeserver)": "Telefonnummer eingeben (auf diesem Heimserver erforderlich)", "Doesn't look like a valid phone number": "Das sieht nicht nach einer gültigen Telefonnummer aus", - "Sign in with SSO": "Mit Single-Sign-On anmelden", + "Sign in with SSO": "Mit „Single Sign-On“ anmelden", "Welcome to %(appName)s": "Willkommen bei %(appName)s", "Send a Direct Message": "Direktnachricht senden", "Create a Group Chat": "Gruppenchat erstellen", From d1c07a0e230e9e1d7eb0a9a7165609a2b5899841 Mon Sep 17 00:00:00 2001 From: libexus Date: Thu, 29 Apr 2021 14:55:35 +0000 Subject: [PATCH 224/283] Translated using Weblate (German) Currently translated at 99.0% (2895 of 2924 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 | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index b17b28245e..b1a91c8c8b 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -125,7 +125,7 @@ "Sat": "Sa", "Jan": "Jan", "Feb": "Feb", - "Mar": "Mrz", + "Mar": "Mär", "Apr": "Apr", "May": "Mai", "Jun": "Jun", @@ -708,7 +708,7 @@ "Messages containing keywords": "Nachrichten mit Schlüsselwörtern", "Error saving email notification preferences": "Fehler beim Speichern der E-Mail-Benachrichtigungseinstellungen", "Tuesday": "Dienstag", - "Enter keywords separated by a comma:": "Schlüsselwörter kommagetrennt eingeben:", + "Enter keywords separated by a comma:": "Gib die Schlüsselwörter durch einen Beistrich getrennt ein:", "Forward Message": "Nachricht weiterleiten", "You have successfully set a password and an email address!": "Du hast erfolgreich ein Passwort und eine E-Mail-Adresse gesetzt!", "Remove %(name)s from the directory?": "Soll der Raum %(name)s aus dem Verzeichnis entfernt werden?", @@ -903,7 +903,7 @@ "Avoid repeated words and characters": "Vermeide wiederholte Worte und Zeichen", "Avoid sequences": "Vermeide Sätze", "Avoid recent years": "Vermeide die letzten Jahre", - "Avoid years that are associated with you": "Vermeide Jahre, die mit dir zusammenhängen", + "Avoid years that are associated with you": "Vermeide Jahreszahlen, die mit dir zu tun haben", "Avoid dates and years that are associated with you": "Vermeide Daten und Jahre, die mit dir in Verbindung stehen", "Capitalization doesn't help very much": "Großschreibung hilft nicht viel", "All-uppercase is almost as easy to guess as all-lowercase": "Alles groß zu schreiben ist genauso einfach zu erraten, wie alles klein zu schreiben", @@ -1101,12 +1101,12 @@ "Folder": "Ordner", "Pin": "Anheften", "Timeline": "Chatverlauf", - "Autocomplete delay (ms)": "Verzögerung zur Autovervollständigung (ms)", + "Autocomplete delay (ms)": "Verzögerung vor Autovervollständigung (ms)", "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", - "Once enabled, encryption cannot be disabled.": "Sobald aktiviert, kann die Verschlüsselung nicht mehr deaktiviert werden.", + "Once enabled, encryption cannot be disabled.": "Sobald du die Verschlüsselung aktivierst, kann du sie nicht mehr deaktivieren.", "Encrypted": "Verschlüsselt", "Ignored users": "Blockierte Benutzer", "Key backup": "Schlüsselsicherung", @@ -1249,7 +1249,7 @@ "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.", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s hat die Einladung 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 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.", @@ -2178,7 +2178,7 @@ "Waiting for your other session to verify…": "Warte auf die Verifikation deiner anderen Sitzungen…", "You've successfully verified your device!": "Du hast dein Gerät erfolgreich verifiziert!", "QR Code": "QR-Code", - "To continue, use Single Sign On to prove your identity.": "Zum Fortfahren, nutze Single Sign-On um deine Identität zu bestätigen.", + "To continue, use Single Sign On to prove your identity.": "Zum Fortfahren nutze Single Sign-On, um deine Identität zu bestätigen.", "Confirm to continue": "Bestätige um fortzufahren", "Click the button below to confirm your identity.": "Klicke den Button unten um deine Identität zu bestätigen.", "Confirm encryption setup": "Bestätige die Einrichtung der Verschlüsselung", @@ -2257,7 +2257,7 @@ "Light": "Hell", "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", + "Use custom size": "Andere Schriftgröße verwenden", "Hey you. You're the best!": "Hey du. Du bist großartig!", "Message layout": "Nachrichtenlayout", "Compact": "Kompakt", @@ -2570,7 +2570,7 @@ "Change the avatar of this room": "Icon von diesem Raum ändern", "See when the avatar changes in this room": "Sehen, wenn sich das Icon des Raums ändert", "Change the avatar of your active room": "Den Avatar deines aktiven Raums ändern", - "See when the avatar changes in your active room": "Sehen wenn ein Avatar in deinem aktiven Raum geändert wird", + "See when the avatar changes in your active room": "Sehen, wenn das Icon in deinem aktiven Raum geändert wird", "Send stickers to this room as you": "Einen Sticker in diesen Raum senden", "See when a sticker is posted in this room": "Sehe wenn ein Sticker in diesen Raum gesendet wird", "Send stickers to your active room as you": "Einen Sticker als du in deinen aktiven Raum senden", @@ -3003,7 +3003,7 @@ "Workspace: ": "Arbeitsraum: ", "Dial pad": "Wähltastatur", "There was an error looking up the phone number": "Beim Suchen der Telefonnummer ist ein Fehler aufgetreten", - "Change which room, message, or user you're viewing": "Ändere welchen Raum, Nachricht oder Nutzer du siehst", + "Change which room, message, or user you're viewing": "Ändere den sichtbaren Raum, Nachricht oder Nutzer", "Unable to look up phone number": "Telefonnummer konnte nicht gefunden werden", "This session has detected that your Security Phrase and key for Secure Messages have been removed.": "In dieser Sitzung wurde festgestellt, dass deine Sicherheitsphrase und dein Schlüssel für sichere Nachrichten entfernt wurden.", "A new Security Phrase and key for Secure Messages have been detected.": "Eine neue Sicherheitsphrase und ein neuer Schlüssel für sichere Nachrichten wurden erkannt.", From 284e7a9e6483eac91a89e4ef509915172c2ca3ff Mon Sep 17 00:00:00 2001 From: Sven Grewe Date: Thu, 29 Apr 2021 14:39:27 +0000 Subject: [PATCH 225/283] Translated using Weblate (German) Currently translated at 99.0% (2895 of 2924 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 b1a91c8c8b..739f0d0c57 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -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 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 Communityseite und in der Raumliste 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", 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 226/283] 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 227/283] 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 d2de115b2f5f0aab5adbfb38b80a025a52a2dd1d Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Thu, 29 Apr 2021 21:37:44 +0530 Subject: [PATCH 228/283] Generate room preview even when minimized Signed-off-by: Jaiwanth --- src/components/views/rooms/RoomTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 8521992fa1..07dca95284 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -131,7 +131,7 @@ export default class RoomTile extends React.PureComponent { } private get showMessagePreview(): boolean { - return !this.props.isMinimized && this.props.showMessagePreview; + return this.props.showMessagePreview; } public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { From 70204d6111bef4d640d2b1980f531bd5039c95e9 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Thu, 29 Apr 2021 22:41:57 +0530 Subject: [PATCH 229/283] 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 230/283] 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 231/283] 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 232/283] 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 233/283] 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 234/283] 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(); From 03a098887a66341d3193adaec3aec32b0f7e33bc Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 30 Apr 2021 19:44:39 -0500 Subject: [PATCH 235/283] Change confirmation string in CreateSecretStorageDialog.js to match Signed-off-by: Aaron Raimist --- .../views/dialogs/security/CreateKeyBackupDialog.js | 2 +- src/i18n/strings/en_EN.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js index 863ee2b427..c1adc2c493 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js @@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

    {_t( - "Please enter your Security Phrase a second time to confirm.", + "Enter your Security Phrase a second time to confirm it.", )}

    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7158156105..9cac041548 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2801,7 +2801,7 @@ "Use a different passphrase?": "Use a different passphrase?", "That doesn't match.": "That doesn't match.", "Go back to set it again.": "Go back to set it again.", - "Please enter your Security Phrase a second time to confirm.": "Please enter your Security Phrase a second time to confirm.", + "Enter your Security Phrase a second time to confirm it.": "Enter your Security Phrase a second time to confirm it.", "Repeat your Security Phrase...": "Repeat your Security Phrase...", "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", @@ -2831,7 +2831,6 @@ "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.": "Enter a security phrase only you know, as it’s used to safeguard your data. To be secure, you shouldn’t re-use your account password.", - "Enter your Security Phrase a second time to confirm it.": "Enter your Security Phrase a second time to confirm it.", "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.": "Store your Security Key somewhere safe, like a password manager or a safe, as it’s used to safeguard your encrypted data.", "Unable to query secret storage status": "Unable to query secret storage status", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", From 626a4ccc34c902b1dc234d01ede39318f97d345b Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 30 Apr 2021 21:45:33 -0500 Subject: [PATCH 236/283] Make warning bold, close copied tooltip on escape Signed-off-by: Aaron Raimist --- .../views/settings/tabs/user/HelpUserSettingsTab.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 9046c074e6..45395bd10c 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -60,6 +60,12 @@ export default class HelpUserSettingsTab extends React.Component }); } + componentWillUnmount() { + // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close + // the tooltip otherwise, such as pressing Escape + if (this.closeCopiedTooltip) this.closeCopiedTooltip(); + } + private onClearCacheAndReload = (e) => { if (!PlatformPeg.get()) return; @@ -168,7 +174,7 @@ export default class HelpUserSettingsTab extends React.Component ...toRightOf(buttonRect, 2), message: successful ? _t('Copied!') : _t('Failed to copy'), }); - target.onmouseleave = close; + this.closeCopiedTooltip = target.onmouseleave = close; } render() { @@ -290,8 +296,8 @@ export default class HelpUserSettingsTab extends React.Component
    {_t("Access Token")}
    - { _t("Your access token gives full access to your account." - + " Do not share it with anyone." ) } + {_t("Your access token gives full access to your account." + + " Do not share it with anyone." )}
    {MatrixClientPeg.get().getAccessToken()} Date: Fri, 30 Apr 2021 21:54:57 -0500 Subject: [PATCH 237/283] lint Signed-off-by: Aaron Raimist --- src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 45395bd10c..3fa0be478c 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -42,6 +42,8 @@ interface IState { @replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab") export default class HelpUserSettingsTab extends React.Component { + protected closeCopiedTooltip: () => void; + constructor(props) { super(props); From ee8d688de1819653cf1b69e917776bbb27858589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 1 May 2021 10:59:28 +0200 Subject: [PATCH 238/283] Fix saving room pill part to history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 02c5d64895..b69f255f89 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -287,6 +287,14 @@ abstract class PillPart extends BasePart implements IPillPart { } } + serialize(): ISerializedPillPart { + return { + type: this.type, + text: this.text, + resourceId: this.resourceId, + }; + } + get canEdit() { return false; } @@ -394,14 +402,6 @@ class UserPillPart extends PillPart { get className() { return "mx_UserPill mx_Pill"; } - - serialize(): ISerializedPillPart { - return { - type: this.type, - text: this.text, - resourceId: this.resourceId, - }; - } } class PillCandidatePart extends PlainBasePart implements IPillCandidatePart { @@ -495,7 +495,7 @@ export class PartCreator { case Type.PillCandidate: return this.pillCandidate(part.text); case Type.RoomPill: - return this.roomPill(part.text); + return this.roomPill(part.resourceId); case Type.UserPill: return this.userPill(part.text, part.resourceId); } From c1549dfac58d64f979f937c328f36ed4f8787a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 1 May 2021 11:32:35 +0200 Subject: [PATCH 239/283] Make optional params optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/model.ts b/src/editor/model.ts index 2e70b872e9..f1b6f90957 100644 --- a/src/editor/model.ts +++ b/src/editor/model.ts @@ -158,7 +158,7 @@ export default class EditorModel { } } - reset(serializedParts: SerializedPart[], caret: Caret, inputType: string) { + reset(serializedParts: SerializedPart[], caret?: Caret, inputType?: string) { this._parts = serializedParts.map(p => this._partCreator.deserializePart(p)); if (!caret) { caret = this.getPositionAtEnd(); From 0c869364e961365f6648be64cb9a967dea6084cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 1 May 2021 12:27:52 +0200 Subject: [PATCH 240/283] Don't serialize resourceId in AtRoomPill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index b69f255f89..6e3f35371d 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -34,7 +34,7 @@ interface ISerializedPart { interface ISerializedPillPart { type: Type.AtRoomPill | Type.RoomPill | Type.UserPill; text: string; - resourceId: string; + resourceId?: string; } export type SerializedPart = ISerializedPart | ISerializedPillPart; @@ -374,6 +374,13 @@ class AtRoomPillPart extends RoomPillPart { get type(): IPillPart["type"] { return Type.AtRoomPill; } + + serialize(): ISerializedPillPart { + return { + type: this.type, + text: this.text, + }; + } } class UserPillPart extends PillPart { From 1fe20853095432f3b95a3ec31e6c3bd672b5d10c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 1 May 2021 12:28:18 +0200 Subject: [PATCH 241/283] Fix deserialize test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- test/editor/deserialize-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/editor/deserialize-test.js b/test/editor/deserialize-test.js index 07b75aaae5..7c7a2f84fb 100644 --- a/test/editor/deserialize-test.js +++ b/test/editor/deserialize-test.js @@ -178,7 +178,7 @@ describe('editor/deserialize', function() { const parts = normalize(parseEvent(htmlMessage(html), createPartCreator())); expect(parts.length).toBe(3); expect(parts[0]).toStrictEqual({type: "plain", text: "Try "}); - expect(parts[1]).toStrictEqual({type: "room-pill", text: "#room:hs.tld"}); + expect(parts[1]).toStrictEqual({type: "room-pill", text: "#room:hs.tld", resourceId: "#room:hs.tld"}); expect(parts[2]).toStrictEqual({type: "plain", text: "?"}); }); it('@room pill', function() { From a2ece699767018e49f62453f446bcaea68ef9946 Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 30 Apr 2021 09:11:54 +0000 Subject: [PATCH 242/283] Translated using Weblate (German) Currently translated at 99.2% (2903 of 2924 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 | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 739f0d0c57..7a9c518193 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1459,7 +1459,7 @@ "Your key share request has been sent - please check your other sessions for key share requests.": "Deine Schlüsselanfrage wurde gesendet - sieh in deinen anderen Sitzungen nach der Schlüsselanfrage.", "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.": "Schlüsselanfragen werden automatisch an deine anderen Sitzungen gesendet. Wenn du sie abgelehnt oder ignoriert hast, klicke hier, um die Schlüssel erneut anzufordern.", "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.", + "Re-request encryption keys from your other sessions.": "Schlüssel aus deinen anderen Sitzungen erneut anfordern.", "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 Aktualisierung 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:", @@ -2028,7 +2028,7 @@ "Doesn't look like a valid email address": "Das sieht nicht nach einer gültigen E-Mail-Adresse aus", "Enter phone number (required on this homeserver)": "Telefonnummer eingeben (auf diesem Heimserver erforderlich)", "Doesn't look like a valid phone number": "Das sieht nicht nach einer gültigen Telefonnummer aus", - "Sign in with SSO": "Mit „Single Sign-On“ anmelden", + "Sign in with SSO": "Mit Single Sign-On anmelden", "Welcome to %(appName)s": "Willkommen bei %(appName)s", "Send a Direct Message": "Direktnachricht senden", "Create a Group Chat": "Gruppenchat erstellen", @@ -3222,7 +3222,7 @@ "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", + "Check your devices": "Überprüfe deine Sitzungen", "%(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", @@ -3269,5 +3269,17 @@ "View all %(count)s members|other": "Alle %(count)s Mitglieder anzeigen", "View all %(count)s members|one": "Mitglied anzeigen", "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", - "Some of your messages have not been sent": "Einige Nachrichten konnten nicht gesendet werden" + "Some of your messages have not been sent": "Einige Nachrichten konnten nicht gesendet werden", + "Original event source": "Ursprüngliche Rohdaten", + "Decrypted event source": "Entschlüsselte Rohdaten", + "Sending": "Senden", + "You can select all or individual messages to retry or delete": "Du kannst einzelne oder alle Nachrichten erneut senden oder löschen", + "Delete all": "Alle löschen", + "Retry all": "Alle erneut senden", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Wenn du dich nicht verifizierst, kannst du keine deiner verschlüsselten Nachrichten lesen und andere vertrauen dir möglicherweise nicht.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifiziere diese Anmeldung damit du auf deine verschlüsselten Nachrichten zugreifen kannst und anderen deine Identität zu bestätigen.", + "Use another login": "Mit anderem Gerät verifizeren", + "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": "Falls du es wirklich willst: Es werden keine Nachrichten gelöscht. Außerdem wird die Suche, während der Index erstellt wird, etwas langsamer sein", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s Mitglieder inklusive %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Inklusive%(commaSeparatedMembers)s" } From 8451e4aa4fcfbd12a333290f0c63839c07f48798 Mon Sep 17 00:00:00 2001 From: Hakim Oubouali Date: Thu, 29 Apr 2021 12:29:01 +0000 Subject: [PATCH 243/283] Translated using Weblate (Central Atlas Tamazight) Currently translated at 5.1% (152 of 2924 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/tzm/ --- src/i18n/strings/tzm.json | 119 +++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/tzm.json b/src/i18n/strings/tzm.json index 6b0fa316e6..f9ac9cf574 100644 --- a/src/i18n/strings/tzm.json +++ b/src/i18n/strings/tzm.json @@ -35,5 +35,122 @@ "The version of %(brand)s": "Taleqqemt n %(brand)s", "Add Phone Number": "Rnu uṭṭun n utilifun", "Add Email Address": "Rnu tasna imayl", - "Open": "Ṛẓem" + "Open": "Ṛẓem", + "Permissions": "Tisirag", + "Subscribe": "Zemmem", + "Change": "Senfel", + "Disconnect": "Kkes azday", + "exists": "illa", + "Santa": "Santa", + "Pizza": "Tapizzat", + "Corn": "Akbal", + "Cloud": "Tagut", + "Globe": "Amaḍal", + "Flower": "Ajeǧǧig", + "Butterfly": "Aferteṭṭu", + "Rooster": "Ayaẓiḍ", + "Panda": "Apanda", + "Upgrade": "Leqqem", + "Confirm": "Sentem", + "Brazil": "Brazil", + "Bolivia": "Bulivya", + "Bhutan": "Buṭan", + "Bermuda": "Birmuda", + "Benin": "Binin", + "Belize": "Biliz", + "Belgium": "Beljika", + "Belarus": "Bilarusya", + "Bahamas": "Bahamas", + "Aruba": "Aruba", + "Angola": "Angula", + "Andorra": "Andura", + "Algeria": "Dzayer", + "Albania": "Albanya", + "End": "End", + "Space": "Space", + "Shift": "Shift", + "Super": "Super", + "Ctrl": "Ctrl", + "Esc": "Esc", + "Calls": "Iɣuṛiten", + "Emoji": "Imuji", + "Afghanistan": "Afɣanistan", + "Logout": "Ffeɣ", + "Leave": "Fel", + "Phone": "Atilifun", + "Email": "Imayl", + "Go": "Ddu", + "Send": "Azen", + "example": "amedya", + "Example": "Amedya", + "Hide": "Ffer", + "Name": "Isem", + "Flags": "Icenyalen", + "Join": "Lkem", + "edited": "infel", + "Copied!": "inɣel!", + "Home": "Asnubeg", + "Reply": "Rar", + "Yes": "Yah", + "About": "Xef", + "Search…": "Arezzu…", + "A-Z": "A-Ẓ", + "Settings": "Tisɣal", + "Reject": "Agy", + "Re-join": "als-lkem", + "People": "Midden", + "Search": "Rzu", + "%(duration)sd": "%(duration)sas", + "Loading...": "Azdam...", + "Share": "Bḍu", + "Camera": "Takamiṛa", + "Microphone": "Amikṛu", + "Add": "Rnu", + "Ignore": "Nexxel", + "None": "Walu", + "Account": "Amiḍan", + "Theme": "Asgum", + "Algorithm:": "Talguritmit:", + "Save": "Ḥḍu", + "Profile": "Ifres", + "ID": "ID", + "Remove": "KKes", + "Folder": "Asdaw", + "Guitar": "Agiṭaṛ", + "Ball": "Tacama", + "Flag": "Acenyal", + "Telephone": "Atilifun", + "Key": "Tasarut", + "Book": "Adlis", + "Gift": "Timucit", + "Hat": "Tarazal", + "Robot": "Aṛubu", + "Heart": "Ul", + "Apple": "Tadeffuyt", + "Banana": "Tabanant", + "Fire": "Timessi", + "Moon": "Ayyur", + "Mushroom": "Agursel", + "Tree": "Aseklu", + "Fish": "Aselm", + "Turtle": "Ifker", + "Rabbit": "Agnin", + "Elephant": "Ilew", + "Pig": "Ilef", + "Close": "Rgel", + "Horse": "Ayyis", + "Lion": "Izem", + "Cat": "Amuc", + "Dog": "Aydi", + "or": "neɣ", + "Decline": "Agy", + "Guest": "Anebgi", + "Ok": "Wax", + "Notifications": "Tineɣmisin", + "No": "Uhu", + "Dark": "Adeɣmum", + "Usage": "Asemres", + "Feb": "Bṛa", + "Jan": "Yen", + "Continue": "Kemmel" } From e382684539394fd26e801755e89902a65f4b8764 Mon Sep 17 00:00:00 2001 From: Sebastian Lithgow Date: Fri, 30 Apr 2021 10:00:11 +0000 Subject: [PATCH 244/283] Translated using Weblate (Danish) Currently translated at 21.1% (617 of 2924 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/da.json b/src/i18n/strings/da.json index ca28295c4f..15ce2986da 100644 --- a/src/i18n/strings/da.json +++ b/src/i18n/strings/da.json @@ -71,7 +71,7 @@ "No rooms to show": "Ingen rum at vise", "This email address is already in use": "Denne email adresse er allerede i brug", "This phone number is already in use": "Dette telefonnummer er allerede i brug", - "Failed to verify email address: make sure you clicked the link in the email": "Kunne ikke bekræfte emailaddressen: vær sikker på at klikke på linket i emailen", + "Failed to verify email address: make sure you clicked the link in the email": "Kunne ikke bekræfte emailaddressen: vær sikker på at klikke på linket i e-mailen", "Call Timeout": "Opkalds Timeout", "The remote side failed to pick up": "Den anden side tog den ikke", "Unable to capture screen": "Kunne ikke optage skærm", @@ -473,9 +473,9 @@ "Show a placeholder for removed messages": "Vis en pladsholder for fjernede beskeder", "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "Hvorvidt du benytter %(brand)s på en enhed, hvor touch er den primære input-grænseflade", "Your user agent": "Din user agent", - "Use Single Sign On to continue": "Brug Single Sign On til at fortsætte", + "Use Single Sign On to continue": "Brug engangs login for at fortsætte", "Confirm adding this email address by using Single Sign On to prove your identity.": "Bekræft tilføjelsen af denne email adresse ved at bruge Single Sign On til at bevise din identitet.", - "Single Sign On": "Single Sign On", + "Single Sign On": "Engangs login", "Confirm adding email": "Bekræft tilføjelse af email", "Click the button below to confirm adding this email address.": "Klik på knappen herunder for at bekræfte tilføjelsen af denne email adresse.", "Confirm": "Bekræft", From b96bf63d6f1dfbdd3109857c777d1c9da827b70f Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Thu, 29 Apr 2021 20:20:06 +0000 Subject: [PATCH 245/283] Translated using Weblate (Czech) Currently translated at 100.0% (2924 of 2924 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 | 52 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 714d34abfa..bc1dff54fd 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -323,7 +323,7 @@ "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s odstranil(a) své zobrazované jméno (%(oldDisplayName)s).", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s zrušil(a) pozvání pro uživatele %(targetName)s.", "%(senderName)s made future room history visible to all room members, from the point they are invited.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich pozvání.", - "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy, a to od chvíle jejich vstupu.", + "%(senderName)s made future room history visible to all room members, from the point they joined.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy od chvíle jejich vstupu.", "%(senderName)s made future room history visible to all room members.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv v této místnosti pro všechny její členy.", "%(senderName)s made future room history visible to anyone.": "%(senderName)s nastavil(a) viditelnost budoucích zpráv pro kohokoliv.", "%(senderName)s changed the pinned messages for the room.": "%(senderName)s změnil(a) připíchnuté zprávy této místnosti.", @@ -744,7 +744,7 @@ "e.g. ": "např. ", "Your device resolution": "Rozlišení obrazovky vašeho zařízení", "The information being sent to us to help make %(brand)s better includes:": "Abychom mohli %(brand)s zlepšovat, posíláte nám následující informace:", - "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "V případě, že se na stránce vyskytují identifikační údaje, jako například název místnosti, ID uživatele, místnosti a nebo skupiny, jsou tyto údaje před odesláním na server odstraněny.", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Pokud tato stránka obsahuje identifikovatelné údaje, například ID místnosti, uživatele nebo skupiny, jsou tyto údaje před odesláním na server odstraněny.", "Call in Progress": "Probíhající hovor", "A call is currently being placed!": "Právě probíhá jiný hovor!", "A call is already in progress!": "Jeden hovor už probíhá!", @@ -990,7 +990,7 @@ "Straight rows of keys are easy to guess": "Řádky na klávesnici je moc jednoduché uhodnout", "Short keyboard patterns are easy to guess": "Krátké sekvence kláves je moc jednoduché uhodnout", "There was an error joining the room": "Při vstupu do místnosti došlo k chybě", - "Sorry, your homeserver is too old to participate in this room.": "Jejda, váš domovský server je moc zastaralý abyste do této místnosti mohli vstoupit.", + "Sorry, your homeserver is too old to participate in this room.": "Omlouváme se, ale váš domovský server je příliš zastaralý pro vstup do této místnosti.", "Please contact your homeserver administrator.": "Kontaktujte prosím správce domovského serveru.", "The other party cancelled the verification.": "Druhá strana ověření zrušila.", "Verified!": "Ověřeno!", @@ -1130,9 +1130,9 @@ "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Bez nastavení bezpečného obnovení zpráv přijdete po odhlášení o historii šifrované komunikace.", "Show a reminder to enable Secure Message Recovery in encrypted rooms": "V šifrovaných konverzacích zobrazovat upozornění na možnost aktivovat bezpečné obnovení zpráv", "Gets or sets the room topic": "Nastaví nebo zjistí téma místnosti", - "Forces the current outbound group session in an encrypted room to be discarded": "Zahodí aktuálně používanou kryptografickou session na odchozí zprávy", + "Forces the current outbound group session in an encrypted room to be discarded": "Vynutí zahození aktuálně používané relace skupiny v zašifrované místnosti", "Custom user status messages": "Vlastní statusy", - "Group & filter rooms by custom tags (refresh to apply changes)": "Skupinkování a filtování místností podle štítků (vyžaduje znovunačtení stránky)", + "Group & filter rooms by custom tags (refresh to apply changes)": "Seskupování a filtrování místností podle vlastních štítků (vyžaduje znovunačtení stránky)", "Render simple counters in room header": "Zobrazovat stavová počítadla v hlavičce místnosti", "Enable Community Filter Panel": "Povolit panel Filtr skupiny", "Show developer tools": "Zobrazit nástroje pro vývojáře", @@ -1205,7 +1205,7 @@ "Remove messages": "Mazat zprávy", "Notify everyone": "Oznámení pro celou místnost", "Enable encryption?": "Povolit šifrování?", - "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.": "Po zapnutí už nelze šifrování v této místnosti vypnout. Zprávy v šifrovaných místnostech mohou číst jenom členové místnosti, server se k obsahu nedostane. Šifrování místností nepodporuje většina botů a propojení. Více informací o šifrování.", + "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.": "Po zapnutí již nelze šifrování v této místnosti vypnout. Zprávy v šifrovaných místnostech mohou číst jen členové místnosti, server se k obsahu nedostane. Šifrování místností nepodporuje většina botů a propojení. Více informací o šifrování.", "Error updating main address": "Nepovedlo se změnit hlavní adresu", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Nastala chyba při pokusu o nastavení hlavní adresy místnosti. Mohl to zakázat server, nebo to může být dočasná chyba.", "Power level": "Úroveň oprávnění", @@ -1219,7 +1219,7 @@ "Your Matrix account on %(serverName)s": "Váš účet Matrix na serveru %(serverName)s", "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Zda používáte funkci „breadcrumb“ (ikony nad seznamem místností)", "Replying With Files": "Odpovídání souborem", - "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Aktuálně nelze odpovědět souborem. Chcete soubor nahrát a poslat bez odpovídání?", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "V tuto chvíli není možné odpovědět souborem. Chcete tento soubor nahrát bez odpovědi?", "The file '%(fileName)s' failed to upload.": "Soubor '%(fileName)s' se nepodařilo nahrát.", "The server does not support the room version specified.": "Server nepodporuje určenou verzi místnosti.", "Name or Matrix ID": "Jméno nebo Matrix ID", @@ -1354,12 +1354,12 @@ "Unexpected error resolving identity server configuration": "Chyba při hledání konfigurace serveru identity", "Use lowercase letters, numbers, dashes and underscores only": "Používejte pouze malá písmena, čísla, pomlčky a podtržítka", "Cannot reach identity server": "Nelze se připojit k serveru identity", - "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.": "Můžete se zaregistrovat, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje pořád, tak zkontrolujte svojí konfiguraci a nebo kontaktujte správce serveru.", - "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.": "Můžete si změnit heslo, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje pořád, tak zkontrolujte svojí konfiguraci a nebo kontaktujte správce serveru.", - "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.": "Můžete se přihlásit, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje pořád, tak zkontrolujte svojí konfiguraci a nebo kontaktujte správce serveru.", + "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.": "Můžete se zaregistrovat, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje i nadále, zkontrolujte svojí konfiguraci nebo kontaktujte správce serveru.", + "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.": "Můžete si změnit heslo, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se toto varování zobrazuje i nadále, zkontrolujte svojí konfiguraci nebo kontaktujte správce serveru.", + "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.": "Můžete se přihlásit, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje i nadále, zkontrolujte svojí konfiguraci nebo kontaktujte správce serveru.", "Call failed due to misconfigured server": "Volání selhalo, protože je rozbitá konfigurace serveru", - "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Zeptejte se správce (%(homeserverDomain)s) jestli by nemohl nakonfigurovat server TURN, aby začalo fungoval volání.", - "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.": "Případně můžete zkusit použít veřejný server turn.matrix.org, což nemusí fungovat tak spolehlivě a řekne to tomu cizímu serveru vaší IP adresu. Můžete to udělat v Nastavení.", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Požádejte správce svého homeserveru (%(homeserverDomain)s) jestli by nemohl nakonfigurovat TURN server, aby volání fungovala spolehlivě.", + "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.": "Můžete také zkusit použít veřejný server na adrese turn.matrix.org, ale ten nebude tak spolehlivý a bude sdílet vaši IP adresu s tímto serverem. To můžete spravovat také v Nastavení.", "Try using turn.matrix.org": "Zkuste použít turn.matrix.org", "Messages": "Zprávy", "Actions": "Akce", @@ -1368,7 +1368,7 @@ "Changes the avatar of the current room": "Změní avatar této místnosti", "Changes your avatar in all rooms": "Změní váš avatar pro všechny místnosti", "Use an identity server": "Používat server identit", - "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Použít server identit k odeslání e-mailové pozvánky. Pokračováním použijete výchozí server identit (%(defaultIdentityServerName)s) nebo ho můžete změnit v Nastavení.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "K pozvání e-mailem použijte server identit. Pokračováním použijete výchozí server identit (%(defaultIdentityServerName)s) nebo ho můžete změnit v Nastavení.", "Use an identity server to invite by email. Manage in Settings.": "Použít server identit na odeslání e-mailové pozvánky. Můžete spravovat v Nastavení.", "Displays list of commands with usages and descriptions": "Zobrazuje seznam příkazu s popiskem", "%(senderName)s made no change.": "%(senderName)s neudělal žádnou změnu.", @@ -1721,8 +1721,8 @@ "Unknown (user, session) pair:": "Neznámý pár (uživatel, relace):", "Session already verified!": "Relace je už ověřená!", "WARNING: Session already verified, but keys do NOT MATCH!": "VAROVÁNÍ: Relace je už ověřená, ale klíče NEODPOVÍDAJÍ!", - "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!": "VAROVÁNÍ: OVĚŘENÍ KLÍČŮ SELHALO! Podpisový klíč pro uživatele %(userId)s a relaci %(deviceId)s je „%(fprint)s“, což neodpovídá klíči „%(fingerprint)s“. Může to znamenat, že je vaše komunikace rušena!", - "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "Zadaný podpisový klíč odpovídá klíči relace %(deviceId)s od uživatele %(userId)s. Relace byla označena za platnou.", + "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!": "VAROVÁNÍ: OVĚŘENÍ KLÍČE SE NEZDAŘILO! Podpisový klíč pro uživatele %(userId)s a relaci %(deviceId)s je „%(fprint)s“, což neodpovídá klíči „%(fingerprint)s“. To by mohlo znamenat, že vaše komunikace je zachycována!", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "Zadaný podpisový klíč odpovídá klíči relace %(deviceId)s od uživatele %(userId)s. Relace byla označena jako ověřená.", "a few seconds ago": "před pár vteřinami", "about a minute ago": "před minutou", "%(num)s minutes ago": "před %(num)s minutami", @@ -1917,8 +1917,8 @@ "Compare unique emoji": "Porovnejte jedinečnou kombinaci emoji", "Compare a unique set of emoji if you don't have a camera on either device": "Pokud na žádném zařízení nemáte kameru, porovnejte jedinečnou kombinaci emoji", "Not Trusted": "Nedůvěryhodné", - "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) se přihlásil do nové relace a neověřil ji:", - "Ask this user to verify their session, or manually verify it below.": "Poproste tohoto uživatele aby svojí relaci ověřil a nebo jí níže můžete ověřit manuálně.", + "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) se přihlásil do nové relace bez ověření:", + "Ask this user to verify their session, or manually verify it below.": "Požádejte tohoto uživatele, aby ověřil svou relaci, nebo jí níže můžete ověřit manuálně.", "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "Relace, kterou se snažíte ověřit, neumožňuje ověření QR kódem ani pomocí emoji, což je to, co %(brand)s podporuje. Zkuste použít jiného klienta.", "Verify by scanning": "Ověřte naskenováním", "You declined": "Odmítli jste", @@ -2032,8 +2032,8 @@ "Could not find user in room": "Nepovedlo se najít uživatele v místnosti", "Please supply a widget URL or embed code": "Zadejte prosím URL widgetu nebo jeho kód", "Send a bug report with logs": "Zaslat hlášení o chybě", - "You signed in to a new session without verifying it:": "Přihlásili jste se do nové relace, ale neoveřili jste ji:", - "Verify your other session using one of the options below.": "Ověřte ostatní relací jedním z následujících způsobů.", + "You signed in to a new session without verifying it:": "Přihlásili jste se do nové relace, aniž byste ji ověřili:", + "Verify your other session using one of the options below.": "Ověřte další relaci jedním z následujících způsobů.", "Click the button below to confirm deleting these sessions.|other": "Zmáčknutím tlačítka potvrdíte smazání těchto relací.", "Click the button below to confirm deleting these sessions.|one": "Zmáčknutím tlačítka potvrdíte smazání této relace.", "Delete sessions|other": "Smazat relace", @@ -2093,7 +2093,7 @@ "Please verify the room ID or address and try again.": "Ověřte prosím, že ID místnosti je správné a zkuste to znovu.", "Room ID or address of ban list": "ID nebo adresa seznamu zablokovaných", "Help us improve %(brand)s": "Pomozte nám zlepšovat %(brand)s", - "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Zasílat anonymní data o použití aplikace, která nám pomáhají %(brand)s zlepšovat. Bedeme na to používat soubory cookie.", + "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Zasílat anonymní údaje o použití aplikace, která nám pomáhají %(brand)s zlepšovat. K tomu se použije soubor cookie.", "I want to help": "Chci pomoci", "Your homeserver has exceeded its user limit.": "Na vašem domovském serveru byl překročen limit počtu uživatelů.", "Your homeserver has exceeded one of its resource limits.": "Na vašem domovském serveru byl překročen limit systémových požadavků.", @@ -2166,8 +2166,8 @@ "Signature upload failed": "Podpis se nepodařilo nahrát", "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Je-li jiná verze programu %(brand)s stále otevřená na jiné kartě, tak ji prosím zavřete, neboť užívání programu %(brand)s stejným hostitelem se zpožděným nahráváním současně povoleným i zakázaným bude působit problémy.", "Unexpected server error trying to leave the room": "Neočekávaná chyba serveru při odcházení z místnosti", - "The person who invited you already left the room.": "Uživatel který vás pozval už místnosti není.", - "The person who invited you already left the room, or their server is offline.": "Uživatel který vás pozvat už odešel z místnosti a nebo je jeho server offline.", + "The person who invited you already left the room.": "Uživatel, který vás pozval, již opustil místnost.", + "The person who invited you already left the room, or their server is offline.": "Uživatel, který vás pozval, již opustil místnost nebo je jeho server offline.", "You left the call": "Odešli jste z hovoru", "%(senderName)s left the call": "%(senderName)s opustil/a hovor", "Call ended": "Hovor skončil", @@ -2954,13 +2954,13 @@ "Mobile experience": "Zážitek na mobilních zařízeních", "Element Web is currently experimental on mobile. The native apps are recommended for most people.": "Element Web je v současné době experimentální na mobilních zařízeních. Nativní aplikace se doporučují pro většinu lidí.", "Use app for a better experience": "Pro lepší zážitek použijte aplikaci", - "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web je experimentální na mobilních zařízeních. Pro lepší zážitek a nejnovější funkce použijte naši bezplatnou nativní aplikaci.", + "Element Web is experimental on mobile. For a better experience and the latest features, use our free native app.": "Element Web je v mobilní verzi experimentální. Chcete-li získat lepší zážitek a nejnovější funkce, použijte naši bezplatnou nativní aplikaci.", "Use app": "Použijte aplikaci", "Something went wrong in confirming your identity. Cancel and try again.": "Při ověřování vaší identity se něco pokazilo. Zrušte to a zkuste to znovu.", - "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.": "Váš domovský server odmítl váš pokus o přihlášení. Může to být způsobeno tím, že věci trvají příliš dlouho. Prosím zkuste to znovu. Pokud to bude pokračovat, obraťte se na správce domovského serveru.", + "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.": "Váš domovský server odmítl váš pokus o přihlášení. To může to být způsobeno tím, že vše trvá příliš dlouho. Zkuste to prosím znovu. Pokud tento problém přetrvává, obraťte se na správce domovského serveru.", "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Váš domovský server nebyl dosažitelný a nemohl vás přihlásit. Zkuste to prosím znovu. Pokud to bude pokračovat, obraťte se na správce domovského serveru.", "Try again": "Zkuste to znovu", - "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.": "Požádali jsme prohlížeč, aby si pamatoval, který domovský server používáte k přihlášení, ale váš prohlížeč to bohužel zapomněl. Přejděte na přihlašovací stránku a zkuste to znovu.", + "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.": "Požádali jsme prohlížeč, aby si zapamatoval, který domovský server používáte k přihlášení, ale váš prohlížeč to bohužel zapomněl. Přejděte na přihlašovací stránku a zkuste to znovu.", "We couldn't log you in": "Nemohli jsme vás přihlásit", "Show stickers button": "Tlačítko Zobrazit nálepky", "Windows": "Okna", @@ -3203,7 +3203,7 @@ "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ý.", - "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Jste zde jediný člověk. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Jste zde jediná osoba. Pokud odejdete, nikdo se v budoucnu nebude moci připojit, včetně vás.", "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Pokud vše resetujete, začnete bez důvěryhodných relací, bez důvěryhodných uživatelů a možná nebudete moci zobrazit minulé zprávy.", "Only do this if you have no other device to complete verification with.": "Udělejte to, pouze pokud nemáte žádné jiné zařízení, se kterým byste mohli dokončit ověření.", "Reset everything": "Resetovat vše", From 1f1f7decdfe57a702fc52da88f92711a558db403 Mon Sep 17 00:00:00 2001 From: iaiz Date: Sat, 1 May 2021 10:46:38 +0000 Subject: [PATCH 246/283] Translated using Weblate (Spanish) Currently translated at 100.0% (2924 of 2924 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 d396cc318f..155aed321b 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3225,5 +3225,28 @@ "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.", - "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." + "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.", + "You can select all or individual messages to retry or delete": "Puedes seleccionar uno o todos los mensajes para reintentar o eliminar", + "Sending": "Enviando", + "Retry all": "Reintentar todo", + "Delete all": "Borrar todo", + "Some of your messages have not been sent": "Algunos de tus mensajes no se han enviado", + "You are the only person here. If you leave, no one will be able to join in the future, including you.": "Eres la única persona aquí. Si te vas, no podrá unirse nadie en el futuro, incluyéndote a ti.", + "Forgotten or lost all recovery methods? Reset all": "¿Has olvidado o perdido todos los métodos de recuperación? Restablecer todo", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Si restableces todo, volverás a empezar sin sesiones ni usuarios de confianza, y puede que no puedas ver mensajes anteriores.", + "Only do this if you have no other device to complete verification with.": "Solo haz esto si no tienes ningún otro dispositivo con el que completar la verificación.", + "Reset everything": "Restablecer todo", + "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": "Si lo haces, ten en cuenta que ninguno de tus mensajes serán eliminados, pero la experiencia de búsqueda será peor durante unos momentos mientras recreamos el índice", + "View message": "Ver mensaje", + "Zoom in": "Acercar", + "Zoom out": "Alejar", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s miembros, incluyendo a %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Incluyendo %(commaSeparatedMembers)s", + "View all %(count)s members|one": "Ver 1 miembro", + "View all %(count)s members|other": "Ver los %(count)s miembros", + "%(seconds)ss left": "%(seconds)ss restantes", + "Failed to send": "No se ha podido mandar", + "Change server ACLs": "Cambiar los ACLs del servidor", + "Show options to enable 'Do not disturb' mode": "Mostrar opciones para activar el modo «no molestar»" } From 31596167d4f5a046b1ffca0e8564139e9666c4ea Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Sun, 2 May 2021 10:18:19 +0000 Subject: [PATCH 247/283] Translated using Weblate (Swedish) Currently translated at 100.0% (2924 of 2924 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 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 63db2ccee2..64cd8ae6ac 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3231,5 +3231,13 @@ "Show options to enable 'Do not disturb' mode": "Visa alternativ för att aktivera 'Stör ej'-läget", "Delete all": "Radera alla", "View all %(count)s members|one": "Visa 1 medlem", - "View all %(count)s members|other": "Visa alla %(count)s medlemmar" + "View all %(count)s members|other": "Visa alla %(count)s medlemmar", + "You can select all or individual messages to retry or delete": "Du kan välja alla eller individuella meddelanden att försöka igen eller radera", + "Sending": "Skickar", + "Retry all": "Försök alla igen", + "Some of your messages have not been sent": "Vissa av dina meddelanden har inte skickats", + "%(count)s members including %(commaSeparatedMembers)s|one": "%(commaSeparatedMembers)s", + "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s medlemmar inklusive %(commaSeparatedMembers)s", + "Including %(commaSeparatedMembers)s": "Inklusive %(commaSeparatedMembers)s", + "Failed to send": "Misslyckades att skicka" } From 6127281c11a0863539268d099577ff46eee7e80f Mon Sep 17 00:00:00 2001 From: Sven Grewe Date: Sat, 1 May 2021 13:53:23 +0000 Subject: [PATCH 248/283] Translated using Weblate (German) Currently translated at 99.3% (2904 of 2924 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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 7a9c518193..2bb593f09c 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -2028,7 +2028,7 @@ "Doesn't look like a valid email address": "Das sieht nicht nach einer gültigen E-Mail-Adresse aus", "Enter phone number (required on this homeserver)": "Telefonnummer eingeben (auf diesem Heimserver erforderlich)", "Doesn't look like a valid phone number": "Das sieht nicht nach einer gültigen Telefonnummer aus", - "Sign in with SSO": "Mit Single Sign-On anmelden", + "Sign in with SSO": "Einmalanmeldung verwenden", "Welcome to %(appName)s": "Willkommen bei %(appName)s", "Send a Direct Message": "Direktnachricht senden", "Create a Group Chat": "Gruppenchat erstellen", @@ -2178,7 +2178,7 @@ "Waiting for your other session to verify…": "Warte auf die Verifikation deiner anderen Sitzungen…", "You've successfully verified your device!": "Du hast dein Gerät erfolgreich verifiziert!", "QR Code": "QR-Code", - "To continue, use Single Sign On to prove your identity.": "Zum Fortfahren nutze Single Sign-On, um deine Identität zu bestätigen.", + "To continue, use Single Sign On to prove your identity.": "Zum Fortfahren nutze die Einmalanmeldung um deine Identität zu bestätigen.", "Confirm to continue": "Bestätige um fortzufahren", "Click the button below to confirm your identity.": "Klicke den Button unten um deine Identität zu bestätigen.", "Confirm encryption setup": "Bestätige die Einrichtung der Verschlüsselung", From d7ed2d3486718d83ea42555946177ed7f3e3ab52 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 10:27:15 -0600 Subject: [PATCH 249/283] Switch to m.audio with extensible events --- src/components/views/rooms/VoiceRecordComposerTile.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index bab95291ba..981c1499ab 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 RecordingPlayback from "../voice_messages/RecordingPlayback"; +import {MsgType} from "matrix-js-sdk/lib/@types/event"; interface IProps { room: Room; @@ -64,8 +65,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent Math.round(v * 1024)), }, + "org.matrix.msc2516.voice": {}, // No content, this is a rendering hint }); await this.disposeRecording(); } From 3f4ee9742abe9543a44ee3caa1dba31e6fb6e475 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 11:41:14 -0600 Subject: [PATCH 250/283] Simple rendering of voice messages in the timeline Fixes https://github.com/vector-im/element-web/issues/17138 --- res/css/_components.scss | 1 + .../views/messages/_MVoiceMessageBody.scss | 19 ++++ .../views/messages/MVoiceMessageBody.tsx | 106 ++++++++++++++++++ .../views/messages/MVoiceOrAudioBody.tsx | 39 +++++++ src/components/views/messages/MessageEvent.js | 6 +- src/i18n/strings/en_EN.json | 1 + src/voice/Playback.ts | 2 +- 7 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 res/css/views/messages/_MVoiceMessageBody.scss create mode 100644 src/components/views/messages/MVoiceMessageBody.tsx create mode 100644 src/components/views/messages/MVoiceOrAudioBody.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 0057f8a8fc..ec9592f3a1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -161,6 +161,7 @@ @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MVideoBody.scss"; +@import "./views/messages/_MVoiceMessageBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MjolnirBody.scss"; diff --git a/res/css/views/messages/_MVoiceMessageBody.scss b/res/css/views/messages/_MVoiceMessageBody.scss new file mode 100644 index 0000000000..3dfb98f778 --- /dev/null +++ b/res/css/views/messages/_MVoiceMessageBody.scss @@ -0,0 +1,19 @@ +/* +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_MVoiceMessageBody { + display: inline-block; // makes the playback controls magically line up +} diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx new file mode 100644 index 0000000000..4a2a83465d --- /dev/null +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -0,0 +1,106 @@ +/* +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 {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {Playback} from "../../../voice/Playback"; +import MFileBody from "./MFileBody"; +import InlineSpinner from '../elements/InlineSpinner'; +import {_t} from "../../../languageHandler"; +import {mediaFromContent} from "../../../customisations/Media"; +import {decryptFile} from "../../../utils/DecryptFile"; +import RecordingPlayback from "../voice_messages/RecordingPlayback"; + +interface IProps { + mxEvent: MatrixEvent; +} + +interface IState { + error?: Error; + playback?: Playback; + decryptedBlob?: Blob; +} + +@replaceableComponent("views.messages.MVoiceMessageBody") +export default class MVoiceMessageBody extends React.PureComponent { + constructor(props: IProps) { + super(props); + + this.state = {}; + } + + public async componentDidMount() { + let buffer: ArrayBuffer; + const content = this.props.mxEvent.getContent(); + const media = mediaFromContent(content); + if (media.isEncrypted) { + try { + const blob = await decryptFile(content.file); + buffer = await blob.arrayBuffer(); + this.setState({decryptedBlob: blob}); + } catch (e) { + this.setState({error: e}); + console.warn("Unable to decrypt voice message", e); + return; // stop processing the audio file + } + } else { + try { + buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer()); + } catch (e) { + this.setState({error: e}); + console.warn("Unable to download voice message", e); + return; // stop processing the audio file + } + } + + const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024); + + // We should have a buffer to work with now: let's set it up + const playback = new Playback(buffer, waveform); + this.setState({playback}); + // Note: the RecordingPlayback component will handle preparing the Playback class for us. + } + + public render() { + if (this.state.error) { + // TODO: @@TR: Verify error state + return ( + + + { _t("Error processing voice message") } + + ); + } + + if (!this.state.playback) { + // TODO: @@TR: Verify loading/decrypting state + return ( + + + + ); + } + + // At this point we should have a playable state + return ( + + + + + ) + } +} diff --git a/src/components/views/messages/MVoiceOrAudioBody.tsx b/src/components/views/messages/MVoiceOrAudioBody.tsx new file mode 100644 index 0000000000..0cebcf3440 --- /dev/null +++ b/src/components/views/messages/MVoiceOrAudioBody.tsx @@ -0,0 +1,39 @@ +/* +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 {MatrixEvent} from "matrix-js-sdk/src/models/event"; +import MAudioBody from "./MAudioBody"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import SettingsStore from "../../../settings/SettingsStore"; +import MVoiceMessageBody from "./MVoiceMessageBody"; + +interface IProps { + mxEvent: MatrixEvent; +} + +@replaceableComponent("views.messages.MVoiceOrAudioBody") +export default class MVoiceOrAudioBody extends React.PureComponent { + public render() { + const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']; + const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages"); + if (isVoiceMessage && voiceMessagesEnabled) { + return ; + } else { + return ; + } + } +} diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 60f7631c8e..78e0dc422d 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -72,12 +72,8 @@ export default class MessageEvent extends React.Component { 'm.emote': sdk.getComponent('messages.TextualBody'), 'm.image': sdk.getComponent('messages.MImageBody'), 'm.file': sdk.getComponent('messages.MFileBody'), - 'm.audio': sdk.getComponent('messages.MAudioBody'), + 'm.audio': sdk.getComponent('messages.MVoiceOrAudioBody'), 'm.video': sdk.getComponent('messages.MVideoBody'), - - // TODO: @@ TravisR: Use labs flag determination. - // MSC: https://github.com/matrix-org/matrix-doc/pull/2516 - 'org.matrix.msc2516.voice': sdk.getComponent('messages.MAudioBody'), }; const evTypes = { 'm.sticker': sdk.getComponent('messages.MStickerBody'), diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5863f2a834..312e98e6d1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1848,6 +1848,7 @@ "%(name)s wants to verify": "%(name)s wants to verify", "You sent a verification request": "You sent a verification request", "Error decrypting video": "Error decrypting video", + "Error processing voice message": "Error processing voice message", "Show all": "Show all", "Reactions": "Reactions", " reacted with %(content)s": " reacted with %(content)s", diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index 99b1f62866..49a80a35d1 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -49,7 +49,7 @@ export class Playback extends EventEmitter implements IDestroyable { constructor(private buf: ArrayBuffer, seedWaveform = DEFAULT_WAVEFORM) { super(); this.context = new AudioContext(); - this.resampledWaveform = arrayFastResample(seedWaveform, PLAYBACK_WAVEFORM_SAMPLES); + this.resampledWaveform = arrayFastResample(seedWaveform ?? DEFAULT_WAVEFORM, PLAYBACK_WAVEFORM_SAMPLES); this.waveformObservable.update(this.resampledWaveform); this.clock = new PlaybackClock(this.context); From 8abd251ec56f229137cda107da5759ea74fc3927 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 11:43:46 -0600 Subject: [PATCH 251/283] delib --- src/components/views/rooms/VoiceRecordComposerTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 981c1499ab..f0acc4fb09 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -27,7 +27,7 @@ import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import RecordingPlayback from "../voice_messages/RecordingPlayback"; -import {MsgType} from "matrix-js-sdk/lib/@types/event"; +import {MsgType} from "matrix-js-sdk/src/@types/event"; interface IProps { room: Room; From 54e139a6adc7604411f041912ec5b97fb4903dba Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Mon, 3 May 2021 22:02:33 +0530 Subject: [PATCH 252/283] Generate previews when the room tile is maximised --- src/components/structures/LeftPanel.tsx | 2 +- src/components/views/rooms/RoomList.tsx | 1 + src/components/views/rooms/RoomSublist.tsx | 4 +++- src/components/views/rooms/RoomTile.tsx | 16 +++++++++++++++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 44b404bd3a..7f9ef7516e 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -416,7 +416,7 @@ export default class LeftPanel extends React.Component { const roomList = { onResize={this.props.onResize} showSkeleton={showSkeleton} extraTiles={extraTiles} + resizeNotifier={this.props.resizeNotifier} alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)} /> }); diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index fb367349a2..dd80e9d40a 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -44,6 +44,7 @@ import { ActionPayload } from "../../../dispatcher/payloads"; import { Enable, Resizable } from "re-resizable"; import { Direction } from "re-resizable/lib/resizer"; import { polyfillTouchEvent } from "../../../@types/polyfill"; +import { ResizeNotifier } from "../../../utils/ResizeNotifier"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays"; @@ -75,7 +76,7 @@ interface IProps { onResize: () => void; showSkeleton?: boolean; alwaysVisible?: boolean; - + resizeNotifier: ResizeNotifier; extraTiles?: ReactComponentElement[]; // TODO: Account for https://github.com/vector-im/element-web/issues/14179 @@ -528,6 +529,7 @@ export default class RoomSublist extends React.Component { tiles.push(; @@ -102,6 +104,9 @@ export default class RoomTile extends React.PureComponent { }; this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); this.roomProps = EchoChamber.forRoom(this.props.room); + if (this.props.resizeNotifier) { + this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); + } } private countUnsentEvents(): number { @@ -116,6 +121,12 @@ export default class RoomTile extends React.PureComponent { this.forceUpdate(); // notification state changed - update }; + private onResize = () => { + if (this.showMessagePreview && !this.state.messagePreview) { + this.setState({messagePreview: this.generatePreview()}); + } + }; + private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => { if (!room?.roomId === this.props.room.roomId) return; this.setState({hasUnsentEvents: this.countUnsentEvents() > 0}); @@ -131,7 +142,7 @@ export default class RoomTile extends React.PureComponent { } private get showMessagePreview(): boolean { - return this.props.showMessagePreview; + return !this.props.isMinimized && this.props.showMessagePreview; } public componentDidUpdate(prevProps: Readonly, prevState: Readonly) { @@ -195,6 +206,9 @@ export default class RoomTile extends React.PureComponent { ); this.props.room.off("Room.name", this.onRoomNameUpdate); } + if (this.props.resizeNotifier) { + this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); + } ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); defaultDispatcher.unregister(this.dispatcherRef); this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); From 0806af0f4d4f276ff38857ab7fb71b43ee923e9d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 15:07:52 -0600 Subject: [PATCH 253/283] Calculate the real waveform in the Playback class --- src/voice/Playback.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index 99b1f62866..a927ad3104 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -20,6 +20,7 @@ import {arrayFastResample, arraySeed} from "../utils/arrays"; import {SimpleObservable} from "matrix-widget-api"; import {IDestroyable} from "../utils/IDestroyable"; import {PlaybackClock} from "./PlaybackClock"; +import {clamp} from "../utils/numbers"; export enum PlaybackState { Decoding = "decoding", @@ -52,8 +53,6 @@ export class Playback extends EventEmitter implements IDestroyable { 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[] { @@ -93,6 +92,11 @@ export class Playback extends EventEmitter implements IDestroyable { public async prepare() { this.audioBuf = await this.context.decodeAudioData(this.buf); + + const waveform = Array.from(this.audioBuf.getChannelData(0)).map(v => clamp(v, 0, 1)); + this.resampledWaveform = arrayFastResample(waveform, PLAYBACK_WAVEFORM_SAMPLES); + this.waveformObservable.update(this.resampledWaveform); + this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore this.clock.durationSeconds = this.audioBuf.duration; } From 7f56ec7bd263a67139e91583b102fc8a1c1bba99 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 15:16:50 -0600 Subject: [PATCH 254/283] docs --- src/voice/Playback.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index a927ad3104..954aa662c3 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -93,6 +93,8 @@ export class Playback extends EventEmitter implements IDestroyable { public async prepare() { this.audioBuf = await this.context.decodeAudioData(this.buf); + // Update the waveform to the real waveform once we have channel data to use. We don't + // exactly trust the user-provided waveform to be accurate... const waveform = Array.from(this.audioBuf.getChannelData(0)).map(v => clamp(v, 0, 1)); this.resampledWaveform = arrayFastResample(waveform, PLAYBACK_WAVEFORM_SAMPLES); this.waveformObservable.update(this.resampledWaveform); From 5dbe882c778e710917296319205f7a195189b00a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 15:20:29 -0600 Subject: [PATCH 255/283] Don't recurse on arrayFastResample Fixes https://github.com/vector-im/element-web/issues/17136 --- src/utils/arrays.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts index f7e693452b..1e130bd605 100644 --- a/src/utils/arrays.ts +++ b/src/utils/arrays.ts @@ -36,14 +36,12 @@ export function arrayFastResample(input: number[], points: number): number[] { } } 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. + // end up overshooting the target length in doing this, but we're not looking to + // be super accurate so we'll let the sanity trims do their job. 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 From ba2da6826faf8a8da99a35aa0b8b86fa1c774784 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 16:08:38 -0600 Subject: [PATCH 256/283] Support a dark theme for voice messages Fixes https://github.com/vector-im/element-web/issues/17137 --- res/css/views/rooms/_VoiceRecordComposerTile.scss | 2 +- res/css/views/voice_messages/_PlayPauseButton.scss | 4 ++-- res/themes/dark/css/_dark.scss | 8 ++++++++ res/themes/legacy-dark/css/_legacy-dark.scss | 9 +++++++++ res/themes/legacy-light/css/_legacy-light.scss | 3 +++ res/themes/light/css/_light.scss | 3 +++ 6 files changed, 26 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index e99e1a00e1..15daf81672 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -40,7 +40,7 @@ limitations under the License. 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; + background-color: $voice-record-icon-color; mask-repeat: no-repeat; mask-size: contain; mask-image: url('$(res)/img/element-icons/trashcan.svg'); diff --git a/res/css/views/voice_messages/_PlayPauseButton.scss b/res/css/views/voice_messages/_PlayPauseButton.scss index c8ab162694..6caedafa29 100644 --- a/res/css/views/voice_messages/_PlayPauseButton.scss +++ b/res/css/views/voice_messages/_PlayPauseButton.scss @@ -19,12 +19,12 @@ limitations under the License. width: 32px; height: 32px; border-radius: 32px; - background-color: $primary-bg-color; + background-color: $voice-playback-button-bg-color; &::before { content: ''; position: absolute; // sizing varies by icon - background-color: $muted-fg-color; + background-color: $voice-playback-button-fg-color; mask-repeat: no-repeat; mask-size: contain; } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index f9983dce9e..cfdda41619 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -42,6 +42,14 @@ $preview-bar-bg-color: $header-panel-bg-color; $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); $inverted-bg-color: $base-color; +$voice-record-stop-border-color: #6F7882; // "Quarterly" +$voice-record-waveform-bg-color: #394049; // "Dark Tile" +$voice-record-waveform-fg-color: $tertiary-fg-color; +$voice-record-waveform-incomplete-fg-color: #5b646d; +$voice-record-icon-color: $tertiary-fg-color; +$voice-playback-button-bg-color: $tertiary-fg-color; +$voice-playback-button-fg-color: $bg-color; + // used by AddressSelector $selected-color: $room-highlight-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 194e89e548..6413a99ce0 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -124,6 +124,15 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%) $groupFilterPanel-divider-color: $roomlist-header-color; +// See non-legacy dark for variable information +$voice-record-stop-border-color: #6F7882; +$voice-record-waveform-bg-color: #394049; +$voice-record-waveform-fg-color: $tertiary-fg-color; +$voice-record-waveform-incomplete-fg-color: #5b646d; +$voice-record-icon-color: $tertiary-fg-color; +$voice-playback-button-bg-color: $tertiary-fg-color; +$voice-playback-button-fg-color: $bg-color; + $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #1A1D23; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index d7352e5684..2151724071 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -198,6 +198,9 @@ $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; +$voice-record-icon-color: $muted-fg-color; +$voice-playback-button-bg-color: $primary-bg-color; +$voice-playback-button-fg-color: $muted-fg-color; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 20ccc2ee41..1763fcdd48 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -188,6 +188,9 @@ $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 +$voice-record-icon-color: $muted-fg-color; +$voice-playback-button-bg-color: $primary-bg-color; +$voice-playback-button-fg-color: $muted-fg-color; $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; From 87a57ec7e5c7f180f91681cc45a87a4f2162eee9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 3 May 2021 16:41:39 -0600 Subject: [PATCH 257/283] Handle no/blocked microphones in voice messages Fixes https://github.com/vector-im/element-web/issues/17139 --- .../views/rooms/VoiceRecordComposerTile.tsx | 62 ++++++++++++++++--- src/i18n/strings/en_EN.json | 4 ++ 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index bab95291ba..f7e72000c9 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -27,6 +27,9 @@ import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import RecordingPlayback from "../voice_messages/RecordingPlayback"; +import Modal from "../../../Modal"; +import ErrorDialog from "../dialogs/ErrorDialog"; +import CallMediaHandler from "../../../CallMediaHandler"; interface IProps { room: Room; @@ -115,16 +118,59 @@ export default class VoiceRecordComposerTile extends React.PureComponent { - if (ev === RecordingState.EndingSoon) return; // ignore this state: it has no UI purpose here - this.setState({recordingPhase: ev}); - }); + // The "microphone access error" dialogs are used a lot, so let's functionify them + const accessError = () => { + Modal.createTrackedDialog('Microphone Access Error', '', ErrorDialog, { + title: _t("Unable to access your microphone"), + description: <> +

    {_t( + "We were unable to access your microphone. Please check your browser settings and try again.", + )}

    + , + }); + }; - this.setState({recorder, recordingPhase: RecordingState.Started}); + // Do a sanity test to ensure we're about to grab a valid microphone reference. Things might + // change between this and recording, but at least we will have tried. + try { + const devices = await CallMediaHandler.getDevices(); + if (!devices?.['audioinput']?.length) { + Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, { + title: _t("No microphone found"), + description: <> +

    {_t( + "We didn't find a microphone on your device. Please check your settings and try again.", + )}

    + , + }); + return; + } + // else we probably have a device that is good enough + } catch (e) { + console.error("Error getting devices: ", e); + accessError(); + return; + } + + try { + const recorder = VoiceRecordingStore.instance.startRecording(); + await recorder.start(); + + // 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}); + } catch (e) { + console.error("Error starting recording: ", e); + accessError(); + + // noinspection ES6MissingAwait - if this goes wrong we don't want it to affect the call stack + VoiceRecordingStore.instance.disposeRecording(); + } }; private renderWaveformArea(): ReactNode { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5863f2a834..e194f93f1b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1649,6 +1649,10 @@ "Invited by %(sender)s": "Invited by %(sender)s", "Jump to first unread message.": "Jump to first unread message.", "Mark all as read": "Mark all as read", + "Unable to access your microphone": "Unable to access your microphone", + "We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.", + "No microphone found": "No microphone found", + "We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.", "Record a voice message": "Record a voice message", "Stop the recording": "Stop the recording", "Delete recording": "Delete recording", From db2070a83232ca0207de5377c5dae2bbc8f3688b Mon Sep 17 00:00:00 2001 From: c-cal Date: Mon, 3 May 2021 17:31:37 +0000 Subject: [PATCH 258/283] Translated using Weblate (French) Currently translated at 100.0% (2924 of 2924 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 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 3db86ad59b..2c0d1a5f32 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3043,7 +3043,7 @@ "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Votre serveur d’accueil n’est pas accessible, nous n’avons pas pu vous connecter. Merci de réessayer. Si cela persiste, merci de contacter l’administrateur de votre serveur d’accueil.", "Try again": "Réessayez", "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.": "Nous avons demandé à votre navigateur de mémoriser votre serveur d’accueil, mais il semble l’avoir oublié. Rendez-vous à la page de connexion et réessayez.", - "We couldn't log you in": "Impossible de vous déconnecter", + "We couldn't log you in": "Nous n'avons pas pu vous connecter", "Upgrade to %(hostSignupBrand)s": "Mettre à jour vers %(hostSignupBrand)s", "Edit Values": "Modifier les valeurs", "Values at explicit levels in this room:": "Valeurs pour les rangs explicites de ce salon :", @@ -3071,7 +3071,7 @@ "Original event source": "Événement source original", "Decrypted event source": "Événement source déchiffré", "We'll create rooms for each of them. You can add existing rooms after setup.": "Nous allons créer un salon pour chacun d’entre eux. Vous pourrez ajouter des salons après l’initialisation.", - "What projects are you working on?": "Sur quels projets travaillez vous ?", + "What projects are you working on?": "Sur quels projets travaillez-vous ?", "We'll create rooms for each topic.": "Nous allons créer un salon pour chaque sujet.", "What are some things you want to discuss?": "De quoi voulez vous discuter ?", "Inviting...": "Invitation…", @@ -3083,7 +3083,7 @@ "A private space just for you": "Un espace privé seulement pour vous", "Just Me": "Seulement moi", "Ensure the right people have access to the space.": "Vérifiez que les bonnes personnes ont accès à cet espace.", - "Who are you working with?": "Avec qui travaillez vous ?", + "Who are you working with?": "Avec qui travaillez-vous ?", "Finish": "Finir", "At the moment only you can see it.": "Pour l’instant vous seul pouvez le voir.", "Creating rooms...": "Création des salons…", @@ -3112,7 +3112,7 @@ "Remove from Space": "Supprimer de l’espace", "Undo": "Annuler", "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.": "Votre message n’a pas été envoyé car ce serveur d’accueil a été banni par son administrateur. Merci de contacter votre administrateur de service pour poursuivre l’usage de ce service.", - "Are you sure you want to leave the space '%(spaceName)s'?": "Êtes vous sûr de vouloir quitter l’espace « %(spaceName)s » ?", + "Are you sure you want to leave the space '%(spaceName)s'?": "Êtes-vous sûr de vouloir quitter l’espace « %(spaceName)s » ?", "This space is not public. You will not be able to rejoin without an invite.": "Cet espace n’est pas public. Vous ne pourrez pas le rejoindre sans invitation.", "Start audio stream": "Démarrer une diffusion audio", "Failed to start livestream": "Échec lors du démarrage de la diffusion en direct", @@ -3188,7 +3188,7 @@ "This room is suggested as a good one to join": "Ce salon recommandé peut être intéressant à rejoindre", "Verify this login to access your encrypted messages and prove to others that this login is really you.": "Vérifiez cette connexion pour accéder à vos messages chiffrés et prouver aux autres qu’il s’agit bien de vous.", "Verify with another session": "Vérifier avec une autre session", - "We'll create rooms for each of them. You can add more later too, including already existing ones.": "Nous allons créer un salon pour chaque. Vous pourrez en ajouter plus tard, y compris certains déjà existant.", + "We'll create rooms for each of them. You can add more later too, including already existing ones.": "Nous allons créer un salon pour chacun d'entre eux. Vous pourrez aussi en ajouter plus tard, y compris certains déjà existant.", "Let's create a room for each of them. You can add more later too, including already existing ones.": "Créons un salon pour chacun d’entre eux. Vous pourrez en ajouter plus tard, y compris certains déjà existant.", "Make sure the right people have access. You can invite more later.": "Assurez-vous que les accès sont accordés aux bonnes personnes. Vous pourrez en inviter d’autres plus tard.", "A private space to organise your rooms": "Un espace privé pour organiser vos salons", @@ -3232,7 +3232,7 @@ "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 ?", + "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", From 29aec4abdbeea85b9517bcb1b8b554c9bd4e1cad Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Tue, 4 May 2021 15:39:46 +0530 Subject: [PATCH 259/283] Use middlePanelResized instead of noisy --- src/components/views/rooms/RoomTile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 108e0979cb..7303f36489 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -105,7 +105,7 @@ export default class RoomTile extends React.PureComponent { this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room); this.roomProps = EchoChamber.forRoom(this.props.room); if (this.props.resizeNotifier) { - this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); + this.props.resizeNotifier.on("middlePanelResized", this.onResize); } } @@ -207,7 +207,7 @@ export default class RoomTile extends React.PureComponent { this.props.room.off("Room.name", this.onRoomNameUpdate); } if (this.props.resizeNotifier) { - this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); + this.props.resizeNotifier.off("middlePanelResized", this.onResize); } ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); defaultDispatcher.unregister(this.dispatcherRef); From 0fe6a389d40c24deec0f9b427836df89011ae87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 May 2021 12:36:22 +0200 Subject: [PATCH 260/283] Add a note about sharing your IP with P2P calls 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 | 2 +- src/settings/Settings.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 85e8e54258..43df51981a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -833,7 +833,7 @@ "Match system theme": "Match system theme", "Use a system font": "Use a system font", "System font name": "System font name", - "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)", "Send analytics data": "Send analytics data", "Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session", "Never send encrypted messages to unverified sessions in this room from this session": "Never send encrypted messages to unverified sessions in this room from this session", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 2a26eeac13..1497a2208d 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -438,7 +438,10 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, - displayName: _td('Allow Peer-to-Peer for 1:1 calls'), + displayName: _td( + "Allow Peer-to-Peer for 1:1 calls " + + "(if you enable this, the other party might be able to see your IP address)", + ), default: true, invertedSettingName: 'webRtcForceTURN', }, From 3eea1b836927a5bfc15313a80feb4ff5d35c1c30 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Tue, 4 May 2021 16:42:22 +0530 Subject: [PATCH 261/283] Add cleanup functions for image view --- 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 fcacae2d39..05d487a9eb 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -114,6 +114,8 @@ export default class ImageView extends React.Component { componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); + window.removeEventListener("resize", this.calculateZoom); + this.image.current.removeEventListener("load", this.calculateZoom); } private calculateZoom = () => { From ac61c8eca8c7d22f16a2fae6e0b2fb75b11abcf5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 May 2021 12:27:27 +0100 Subject: [PATCH 262/283] Adhere to updated sort order for space children --- .../structures/SpaceRoomDirectory.tsx | 7 +++++- src/stores/SpaceStore.tsx | 22 +++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 8d6c9f0a70..db7fd13753 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -39,6 +39,7 @@ import {mediaFromMxc} from "../../customisations/Media"; import InfoTooltip from "../views/elements/InfoTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip"; import {useStateToggle} from "../../hooks/useStateToggle"; +import {getOrder} from "../../stores/SpaceStore"; interface IHierarchyProps { space: Room; @@ -254,7 +255,11 @@ export const HierarchyLevel = ({ const space = cli.getRoom(spaceId); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null); + const children = Array.from(relations.get(spaceId)?.values() || []); + const sortedChildren = sortBy(children, ev => { + // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting + return getOrder(ev.content.order, null, ev.state_key); + }); const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const roomId = ev.state_key; if (!rooms.has(roomId)) return result; diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index ec6227e45e..db93a23216 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {sortBy, throttle} from "lodash"; +import {ListIteratee, Many, sortBy, throttle} from "lodash"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixEvent} from "matrix-js-sdk/src/models/event"; @@ -61,15 +61,18 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, }, [[], []]); }; -const getOrder = (ev: MatrixEvent): string | null => { - const content = ev.getContent(); - if (typeof content.order === "string" && Array.from(content.order).every((c: string) => { +// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` +export const getOrder = (order: string, creationTs: number, roomId: string): Array>> => { + let validatedOrder: string = null; + + if (typeof order === "string" && Array.from(order).every((c: string) => { const charCode = c.charCodeAt(0); return charCode >= 0x20 && charCode <= 0x7F; })) { - return content.order; + validatedOrder = order; } - return null; + + return [validatedOrder, creationTs, roomId]; } const getRoomFn: FetchRoomFn = (room: Room) => { @@ -193,7 +196,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private getChildren(spaceId: string): Room[] { const room = this.matrixClient?.getRoom(spaceId); const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via); - return sortBy(childEvents, getOrder) + return sortBy(childEvents, ev => { + const roomId = ev.getStateKey(); + const childRoom = this.matrixClient?.getRoom(roomId); + const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs(); + return getOrder(ev.getContent().order, createTs, roomId); + }) .map(ev => this.matrixClient.getRoom(ev.getStateKey())) .filter(room => room?.getMyMembership() === "join" || room?.getMyMembership() === "invite") || []; } From c5cbca7c57d2ef9007927492861e05e4d7eeddf8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 May 2021 12:38:55 +0100 Subject: [PATCH 263/283] Fix edge case with space panel alignment with subspaces on ff --- res/css/structures/_SpacePanel.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 59f2ea947c..0c5833b176 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -79,6 +79,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceItem { display: inline-flex; flex-flow: wrap; + align-self: baseline; } .mx_SpaceItem.collapsed { From d14ea1bd58f7ae53a496c5e3c4f0135250dfe4e6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 May 2021 14:01:10 +0100 Subject: [PATCH 264/283] Fix layout issue with expanded space panel --- res/css/structures/_SpacePanel.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 0c5833b176..8f277806cb 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -80,6 +80,7 @@ $activeBorderColor: $secondary-fg-color; display: inline-flex; flex-flow: wrap; align-self: baseline; + width: 100%; } .mx_SpaceItem.collapsed { From 4489f8a6ead924f8124c2ee95f508f36d6e66c8e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 May 2021 14:01:26 +0100 Subject: [PATCH 265/283] Fix hover behaviour of space invites --- res/css/structures/_SpacePanel.scss | 2 +- src/components/views/spaces/SpaceTreeLevel.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 8f277806cb..dbb18d02fd 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -277,7 +277,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton:hover, .mx_SpaceButton:focus-within, .mx_SpaceButton_hasMenuOpen { - &:not(.mx_SpaceButton_home) { + &:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) { // Hide the badge container on hover because it'll be a menu button .mx_SpacePanel_badgeContainer { width: 0; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 6825d84013..9bdbced0b5 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -300,12 +300,15 @@ export class SpaceItem extends React.PureComponent { "collapsed": collapsed, "hasSubSpaces": childSpaces && childSpaces.length, }); + + const isInvite = space.getMyMembership() === "invite"; const classes = classNames("mx_SpaceButton", { mx_SpaceButton_active: isActive, mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, mx_SpaceButton_narrow: isNarrow, + mx_SpaceButton_invite: isInvite, }); - const notificationState = space.getMyMembership() === "invite" + const notificationState = isInvite ? StaticNotificationState.forSymbol("!", NotificationColor.Red) : SpaceStore.instance.getNotificationState(space.roomId); From caa772348fd3d303d903ea7c1c05d1ebd8dd6f16 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 May 2021 14:14:06 +0100 Subject: [PATCH 266/283] Actually fix the layout bug without re-regressing the prior issue --- res/css/structures/_SpacePanel.scss | 6 ++++-- src/components/views/spaces/SpaceTreeLevel.tsx | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index dbb18d02fd..9264e82ea5 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -79,8 +79,10 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceItem { display: inline-flex; flex-flow: wrap; - align-self: baseline; - width: 100%; + + &.mx_SpaceItem_narrow { + align-self: baseline; + } } .mx_SpaceItem.collapsed { diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 9bdbced0b5..e48e1d5dc2 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -297,6 +297,7 @@ export class SpaceItem extends React.PureComponent { const isActive = activeSpaces.includes(space); const itemClasses = classNames({ "mx_SpaceItem": true, + "mx_SpaceItem_narrow": isNarrow, "collapsed": collapsed, "hasSubSpaces": childSpaces && childSpaces.length, }); From 9f5285d9f89994cccd1926bffba8299c62134960 Mon Sep 17 00:00:00 2001 From: Thibault Martin Date: Tue, 4 May 2021 06:07:34 +0000 Subject: [PATCH 267/283] Translated using Weblate (French) Currently translated at 100.0% (2924 of 2924 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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 2c0d1a5f32..984dce8595 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3043,7 +3043,7 @@ "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Votre serveur d’accueil n’est pas accessible, nous n’avons pas pu vous connecter. Merci de réessayer. Si cela persiste, merci de contacter l’administrateur de votre serveur d’accueil.", "Try again": "Réessayez", "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.": "Nous avons demandé à votre navigateur de mémoriser votre serveur d’accueil, mais il semble l’avoir oublié. Rendez-vous à la page de connexion et réessayez.", - "We couldn't log you in": "Nous n'avons pas pu vous connecter", + "We couldn't log you in": "Nous n’avons pas pu vous connecter", "Upgrade to %(hostSignupBrand)s": "Mettre à jour vers %(hostSignupBrand)s", "Edit Values": "Modifier les valeurs", "Values at explicit levels in this room:": "Valeurs pour les rangs explicites de ce salon :", @@ -3188,7 +3188,7 @@ "This room is suggested as a good one to join": "Ce salon recommandé peut être intéressant à rejoindre", "Verify this login to access your encrypted messages and prove to others that this login is really you.": "Vérifiez cette connexion pour accéder à vos messages chiffrés et prouver aux autres qu’il s’agit bien de vous.", "Verify with another session": "Vérifier avec une autre session", - "We'll create rooms for each of them. You can add more later too, including already existing ones.": "Nous allons créer un salon pour chacun d'entre eux. Vous pourrez aussi en ajouter plus tard, y compris certains déjà existant.", + "We'll create rooms for each of them. You can add more later too, including already existing ones.": "Nous allons créer un salon pour chacun d’entre eux. Vous pourrez aussi en ajouter plus tard, y compris certains déjà existant.", "Let's create a room for each of them. You can add more later too, including already existing ones.": "Créons un salon pour chacun d’entre eux. Vous pourrez en ajouter plus tard, y compris certains déjà existant.", "Make sure the right people have access. You can invite more later.": "Assurez-vous que les accès sont accordés aux bonnes personnes. Vous pourrez en inviter d’autres plus tard.", "A private space to organise your rooms": "Un espace privé pour organiser vos salons", From 5954bf0a411db335ca02d4c6c000cb61e1c48701 Mon Sep 17 00:00:00 2001 From: Marek Date: Tue, 4 May 2021 13:29:46 +0000 Subject: [PATCH 268/283] Translated using Weblate (German) Currently translated at 99.5% (2912 of 2924 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 | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 2bb593f09c..7f0d6a2747 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -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": "Nutzer von anderen Homeservern können auf diesen Raum nicht zugreifen", + "This room is not accessible by remote Matrix servers": "Dieser Raum ist von Personen auf anderen Matrix-Servern nicht betretbar", "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", @@ -2597,7 +2597,7 @@ "See videos posted to your active room": "In deinen aktiven Raum gesendete Videos anzeigen", "See videos posted to this room": "In diesen Raum gesendete Videos anzeigen", "Send images as you in this room": "Bilder als du in diesen Raum senden", - "Send images as you in your active room": "Bilder als du in deinem aktiven Raum senden", + "Send images as you in your active room": "Sende Bilder als deine Person in den aktiven Raum.", "See images posted to this room": "In diesen Raum gesendete Bilder anzeigen", "See images posted to your active room": "In deinen aktiven Raum gesendete Bilder anzeigen", "Send videos as you in this room": "Videos als du in diesen Raum senden", @@ -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 ladet jemanden Neues ein.", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Nur ihr beide nehmt an dieser Konversation teil, es sei denn, ihr ladet jemanden 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 ", @@ -2945,7 +2945,7 @@ "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.": "Du kannst in den benutzerdefinierten Serveroptionen eine andere Heimserver-URL angeben, um dich bei anderen Matrixservern anzumelden.", "Server Options": "Servereinstellungen", "No other application is using the webcam": "keine andere Anwendung auf die Webcam zugreift", - "Permission is granted to use the webcam": "auf die Webcam zugegriffen werden darf", + "Permission is granted to use the webcam": "Zugriff auf die Webcam ist gestattet.", "A microphone and webcam are plugged in and set up correctly": "Mikrofon und Webcam eingesteckt und richtig eingerichtet sind", "Call failed because no microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon zugegriffen werden konnte. Stelle sicher, dass das Mikrofon richtig eingesteckt und eingerichtet ist.", "Call failed because no webcam or microphone could not be accessed. Check that:": "Der Anruf ist fehlgeschlagen weil nicht auf das Mikrofon oder die Webcam zugegriffen werden konnte. Stelle sicher, dass:", @@ -2954,7 +2954,7 @@ "Host account on": "Benutzer*innenkonto betreiben an", "Hold": "Halten", "Resume": "Fortsetzen", - "We call the places where you can host your account ‘homeservers’.": "Den Ort, an dem du dein Benutzer*innenkonto betreibst, nennen wir „Heimserver“.", + "We call the places where you can host your account ‘homeservers’.": "Den Ort, an dem du dein Konto betreibst, nennen wir „Heimserver“.", "Invalid URL": "Ungültiger Link", "Unable to validate homeserver": "Überprüfung des Heimservers nicht möglich", "%(name)s paused": "%(name)s hat pausiert", @@ -3161,7 +3161,7 @@ "Invite to %(spaceName)s": "Leute zu %(spaceName)s einladen", "Spaces": "Spaces", "Invite People": "Personen einladen", - "Invite with email or username": "Personen mit E-Mail oder Benutzername einladen", + "Invite with email or username": "Personen mit E-Mail oder Benutzernamen einladen", "You can change these anytime.": "Du kannst diese jederzeit ändern.", "From %(deviceName)s (%(deviceId)s) at %(ip)s": "Von %(deviceName)s (%(deviceId)s) mit der Adresse %(ip)s", "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "Neue Anmeldung: %(name)s (%(deviceID)s) mit der IP-Adresse %(ip)s", @@ -3276,10 +3276,11 @@ "You can select all or individual messages to retry or delete": "Du kannst einzelne oder alle Nachrichten erneut senden oder löschen", "Delete all": "Alle löschen", "Retry all": "Alle erneut senden", - "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Wenn du dich nicht verifizierst, kannst du keine deiner verschlüsselten Nachrichten lesen und andere vertrauen dir möglicherweise nicht.", - "Verify your identity to access encrypted messages and prove your identity to others.": "Verifiziere diese Anmeldung damit du auf deine verschlüsselten Nachrichten zugreifen kannst und anderen deine Identität zu bestätigen.", + "Without verifying, you won’t have access to all your messages and may appear as untrusted to others.": "Ohne deine Verifikation kannst du keine deiner verschlüsselten Nachrichten lesen und andere vertrauen dir möglicherweise nicht.", + "Verify your identity to access encrypted messages and prove your identity to others.": "Verifiziere diese Anmeldung, um auf verschlüsselte Nachrichten zuzugreifen und dich anderen gegenüber zu identifizieren.", "Use another login": "Mit anderem Gerät verifizeren", "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": "Falls du es wirklich willst: Es werden keine Nachrichten gelöscht. Außerdem wird die Suche, während der Index erstellt wird, etwas langsamer sein", "%(count)s members including %(commaSeparatedMembers)s|other": "%(count)s Mitglieder inklusive %(commaSeparatedMembers)s", - "Including %(commaSeparatedMembers)s": "Inklusive%(commaSeparatedMembers)s" + "Including %(commaSeparatedMembers)s": "Inklusive%(commaSeparatedMembers)s", + "Consulting with %(transferTarget)s. Transfer to %(transferee)s": "Beratung mit %(transferTarget)s. Übertragung zu %(transferee)s" } From ddc8f56b470fb498b5a74a163a72827412598483 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Tue, 4 May 2021 05:06:13 +0000 Subject: [PATCH 269/283] Translated using Weblate (Czech) Currently translated at 100.0% (2924 of 2924 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index bc1dff54fd..8633146420 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -1708,7 +1708,7 @@ "Backup has a invalid signature from this user": "Záloha má neplatný podpis od tohoto uživatele", "Backup has a signature from unknown user with ID %(deviceId)s": "Záloha je podepsaná neznámým uživatelem %(deviceId)s", "Close preview": "Zavřít náhled", - "Hide verified sessions": "Schovat ověřené relace", + "Hide verified sessions": "Skrýt ověřené relace", "%(count)s verified sessions|other": "%(count)s ověřených relací", "%(count)s verified sessions|one": "1 ověřená relace", "Language Dropdown": "Menu jazyků", From 48237949ad6821e61c839fff14bb86738fab0575 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 May 2021 16:44:29 +0100 Subject: [PATCH 270/283] Only aggregate DM notifications on the Space Panel in the Home Space --- src/stores/SpaceStore.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index ec6227e45e..22307d9f2e 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -406,7 +406,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.spaceFilteredRooms.forEach((roomIds, s) => { // Update NotificationStates - this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => roomIds.has(room.roomId))); + this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { + if (roomIds.has(room.roomId)) { + // Don't aggregate notifications for DMs except in the Home Space + if (s !== HOME_SPACE) { + return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) + || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); + } + + return true; + } + + return false; + })); }); }, 100, {trailing: true, leading: true}); From 69604480fd9a8c439f04b8f4b614e50e36895499 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 May 2021 21:17:15 +0100 Subject: [PATCH 271/283] Add comment --- src/stores/SpaceStore.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 8e86253f75..43822007c9 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -112,6 +112,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._suggestedRooms; } + /** + * Sets the active space, updates room list filters, + * optionally switches the user's room back to where they were when they last viewed that space. + * @param space which space to switch to. + * @param contextSwitch whether to switch the user's context, + * should not be done when the space switch is done implicitly due to another event like switching room. + */ public async setActiveSpace(space: Room | null, contextSwitch = true) { if (space === this.activeSpace || (space && !space?.isSpaceRoom())) return; @@ -305,7 +312,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // if the currently selected space no longer exists, remove its selection if (this._activeSpace && detachedNodes.has(this._activeSpace)) { - this.setActiveSpace(null); + this.setActiveSpace(null, false); } this.onRoomsUpdate(); // TODO only do this if a change has happened @@ -593,7 +600,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } case "after_leave_room": if (this._activeSpace && payload.room_id === this._activeSpace.roomId) { - this.setActiveSpace(null); + this.setActiveSpace(null, false); } break; } From a94c1a90c15c5fb29c31869b3a15ddde31ece576 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 4 May 2021 20:45:15 -0600 Subject: [PATCH 272/283] Update colours and sizing for voice messages Fixes https://github.com/vector-im/element-web/issues/17162 --- .../views/rooms/_VoiceRecordComposerTile.scss | 10 ++++++---- .../voice_messages/_PlaybackContainer.scss | 15 +++++++-------- res/themes/dark/css/_dark.scss | 10 +++++----- res/themes/legacy-dark/css/_legacy-dark.scss | 8 ++++---- .../legacy-light/css/_legacy-light.scss | 12 ++++++------ res/themes/light/css/_light.scss | 19 +++++++++++-------- .../voice_messages/LiveRecordingWaveform.tsx | 7 +++---- src/voice/Playback.ts | 2 +- src/voice/VoiceRecording.ts | 2 ++ 9 files changed, 45 insertions(+), 40 deletions(-) diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index 15daf81672..c0775b8371 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -39,7 +39,7 @@ limitations under the License. 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) + margin-right: 11px; // distance from left edge of waveform container (container has some margin too) background-color: $voice-record-icon-color; mask-repeat: no-repeat; mask-size: contain; @@ -55,7 +55,9 @@ limitations under the License. position: relative; // important for the live circle &.mx_VoiceRecordComposerTile_recording { - padding-left: 16px; // +10px for the live circle, +6px for regular padding + // We are putting the circle in this padding, so we need +10px from the regular + // padding on the left side. + padding-left: 22px; &::before { animation: recording-pulse 2s infinite; @@ -65,8 +67,8 @@ limitations under the License. width: 10px; height: 10px; position: absolute; - left: 8px; - top: 16px; // vertically center + left: 12px; // 12px from the left edge for container padding + top: 18px; // vertically center (middle align with clock) border-radius: 10px; } } diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/voice_messages/_PlaybackContainer.scss index 49bd81ef81..64e8f445e1 100644 --- a/res/css/views/voice_messages/_PlaybackContainer.scss +++ b/res/css/views/voice_messages/_PlaybackContainer.scss @@ -19,8 +19,9 @@ limitations under the License. // 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 + // 7px top and bottom for visual design. 12px left & right, but the waveform (right) + // has a 1px padding on it that we want to account for. + padding: 7px 12px 7px 11px; background-color: $voice-record-waveform-bg-color; border-radius: 12px; @@ -30,11 +31,9 @@ limitations under the License. color: $voice-record-waveform-fg-color; font-size: $font-14px; + line-height: $font-24px; .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-incomplete-fg-color; @@ -47,8 +46,8 @@ limitations under the License. } .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 + width: 42px; // we're not using a monospace font, so fake it + padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended. + padding-left: 8px; // isolate from recording circle / play control } } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index cfdda41619..334bf581f3 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -42,13 +42,13 @@ $preview-bar-bg-color: $header-panel-bg-color; $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); $inverted-bg-color: $base-color; -$voice-record-stop-border-color: #6F7882; // "Quarterly" +$voice-record-stop-border-color: #6F7882; // "Quartary" $voice-record-waveform-bg-color: #394049; // "Dark Tile" -$voice-record-waveform-fg-color: $tertiary-fg-color; -$voice-record-waveform-incomplete-fg-color: #5b646d; -$voice-record-icon-color: $tertiary-fg-color; +$voice-record-waveform-fg-color: $secondary-fg-color; +$voice-record-waveform-incomplete-fg-color: #6F7882; // "Quartary" +$voice-record-icon-color: #6F7882; // "Quartary" $voice-playback-button-bg-color: $tertiary-fg-color; -$voice-playback-button-fg-color: $bg-color; +$voice-playback-button-fg-color: #21262C; // "Separator" // used by AddressSelector $selected-color: $room-highlight-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 6413a99ce0..6a162bbda7 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -127,11 +127,11 @@ $groupFilterPanel-divider-color: $roomlist-header-color; // See non-legacy dark for variable information $voice-record-stop-border-color: #6F7882; $voice-record-waveform-bg-color: #394049; -$voice-record-waveform-fg-color: $tertiary-fg-color; -$voice-record-waveform-incomplete-fg-color: #5b646d; -$voice-record-icon-color: $tertiary-fg-color; +$voice-record-waveform-fg-color: $secondary-fg-color; +$voice-record-waveform-incomplete-fg-color: #6F7882; +$voice-record-icon-color: #6F7882; $voice-playback-button-bg-color: $tertiary-fg-color; -$voice-playback-button-fg-color: $bg-color; +$voice-playback-button-fg-color: #21262C; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 2151724071..cc0f54853e 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -192,15 +192,15 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) $groupFilterPanel-divider-color: $roomlist-header-color; // See non-legacy _light for variable information -$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; -$voice-record-icon-color: $muted-fg-color; +$voice-record-stop-border-color: #E3E8F0; +$voice-record-waveform-bg-color: #E3E8F0; +$voice-record-waveform-fg-color: $secondary-fg-color; +$voice-record-waveform-incomplete-fg-color: #C1C6CD; +$voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $primary-bg-color; -$voice-playback-button-fg-color: $muted-fg-color; +$voice-playback-button-fg-color: $secondary-fg-color; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 1763fcdd48..193b7f816c 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -182,15 +182,18 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) $groupFilterPanel-divider-color: $roomlist-header-color; -$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 -$voice-record-icon-color: $muted-fg-color; +// These two don't change between themes. They are the $warning-color, but we don't +// want custom themes to affect them by accident. +$voice-record-stop-symbol-color: #ff4b55; +$voice-record-live-circle-color: #ff4b55; + +$voice-record-stop-border-color: #E3E8F0; // "Separator" +$voice-record-waveform-bg-color: #E3E8F0; // "Separator" +$voice-record-waveform-fg-color: $secondary-fg-color; +$voice-record-waveform-incomplete-fg-color: #C1C6CD; // "Quartary" +$voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $primary-bg-color; -$voice-playback-button-fg-color: $muted-fg-color; +$voice-playback-button-fg-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx index e7c34c9177..aab89f6ab1 100644 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/voice_messages/LiveRecordingWaveform.tsx @@ -15,12 +15,11 @@ limitations under the License. */ import React from "react"; -import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; +import {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {arrayFastResample, arraySeed} from "../../../utils/arrays"; import {percentageOf} from "../../../utils/numbers"; import Waveform from "./Waveform"; -import {PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback"; interface IProps { recorder: VoiceRecording; @@ -38,14 +37,14 @@ export default class LiveRecordingWaveform extends React.PureComponent { // 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), PLAYBACK_WAVEFORM_SAMPLES); + const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_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/voice/Playback.ts b/src/voice/Playback.ts index ca48680ebd..b2525c2719 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -29,7 +29,7 @@ export enum PlaybackState { Playing = "playing", // active progress through timeline } -export const PLAYBACK_WAVEFORM_SAMPLES = 35; +export const PLAYBACK_WAVEFORM_SAMPLES = 38; const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); export class Playback extends EventEmitter implements IDestroyable { diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index eb705200ca..62496ce3d9 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -33,6 +33,8 @@ 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 const RECORDING_PLAYBACK_SAMPLES = 43; + export interface IRecordingUpdate { waveform: number[]; // floating points between 0 (low) and 1 (high). timeSeconds: number; // float From ed43d9257937b6e205dd939a60a453f1bd9fbefc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 4 May 2021 20:55:35 -0600 Subject: [PATCH 273/283] Match designs more closely with +1 sample --- src/voice/Playback.ts | 2 +- src/voice/VoiceRecording.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index b2525c2719..caa5241e1a 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -29,7 +29,7 @@ export enum PlaybackState { Playing = "playing", // active progress through timeline } -export const PLAYBACK_WAVEFORM_SAMPLES = 38; +export const PLAYBACK_WAVEFORM_SAMPLES = 39; const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); export class Playback extends EventEmitter implements IDestroyable { diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 62496ce3d9..c4a0a78ce5 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -33,7 +33,7 @@ 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 const RECORDING_PLAYBACK_SAMPLES = 43; +export const RECORDING_PLAYBACK_SAMPLES = 44; export interface IRecordingUpdate { waveform: number[]; // floating points between 0 (low) and 1 (high). From ccdc9fbef6af3e3fd674517bde732168b3e7cfa8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 4 May 2021 21:15:22 -0600 Subject: [PATCH 274/283] Fix issue where composer styles were being applied to the timeline --- res/css/views/rooms/_VoiceRecordComposerTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index c0775b8371..b87211a847 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -46,7 +46,7 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/trashcan.svg'); } -.mx_VoiceMessagePrimaryContainer { +.mx_VoiceRecordComposerTile_recording.mx_VoiceMessagePrimaryContainer { // Note: remaining class properties are in the PlayerContainer CSS. margin: 6px; // force the composer area to put a gutter around us From 374d8c1c4c44c57df0691db531b13efcea968260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 5 May 2021 08:43:24 +0200 Subject: [PATCH 275/283] Update link to Android SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73afe34df0..81a4a00515 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Platform Targets: * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. * Mobile Web is not currently a target platform - instead please use the native iOS (https://github.com/matrix-org/matrix-ios-kit) and Android - (https://github.com/matrix-org/matrix-android-sdk) SDKs. + (https://github.com/vector-im/element-android) SDKs. All code lands on the `develop` branch - `master` is only used for stable releases. **Please file PRs against `develop`!!** From 07f5b6e8c48bd11d593408b72b27e443e754288c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 11:45:12 +0100 Subject: [PATCH 276/283] Add retry mechanism and progress bar to add existing to space dialog --- .../dialogs/_AddExistingToSpaceDialog.scss | 77 ++++++++-- .../dialogs/AddExistingToSpaceDialog.tsx | 134 ++++++++++++------ src/i18n/strings/en_EN.json | 9 +- 3 files changed, 162 insertions(+), 58 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 84f965e6bd..c29c9791a6 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -101,7 +101,7 @@ limitations under the License. .mx_BaseAvatar { display: inline-flex; - margin: 5px 16px 5px 5px; + margin: auto 16px auto 5px; vertical-align: middle; } @@ -160,31 +160,32 @@ limitations under the License. } } - .mx_AddExistingToSpaceDialog_errorText { - font-weight: $font-semi-bold; - font-size: $font-12px; - line-height: $font-15px; - color: $notice-primary-color; - margin-bottom: 28px; - } - .mx_AddExistingToSpace { display: contents; } .mx_AddExistingToSpaceDialog_footer { display: flex; - margin-top: 32px; + margin-top: 20px; > span { flex-grow: 1; - font-size: $font-14px; + font-size: $font-12px; line-height: $font-15px; - font-weight: $font-semi-bold; + color: $secondary-fg-color; - .mx_AccessibleButton { - font-size: inherit; - display: inline-block; + .mx_ProgressBar { + height: 8px; + width: 100%; + + @mixin ProgressBarBorderRadius "8px"; + } + + .mx_AddExistingToSpaceDialog_progressText { + margin-top: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; } > * { @@ -192,8 +193,54 @@ limitations under the License. } } + .mx_AddExistingToSpaceDialog_error { + padding-left: 12px; + + > img { + align-self: center; + } + + .mx_AddExistingToSpaceDialog_errorHeading { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $notice-primary-color; + } + + .mx_AddExistingToSpaceDialog_errorCaption { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $primary-fg-color; + } + } + .mx_AccessibleButton { display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 36px; + } + + .mx_AddExistingToSpaceDialog_retryButton { + margin-left: 12px; + padding-left: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + background-color: $primary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + left: 0; + } } .mx_AccessibleButton_kind_link { diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 2253b525e0..a33248200c 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -29,12 +29,13 @@ import RoomAvatar from "../avatars/RoomAvatar"; import {getDisplayAliasForRoom} from "../../../Rooms"; import AccessibleButton from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {allSettled} from "../../../utils/promise"; +import {sleep} from "../../../utils/promise"; import DMRoomMap from "../../../utils/DMRoomMap"; import {calculateRoomVia} from "../../../utils/permalinks/Permalinks"; import StyledCheckbox from "../elements/StyledCheckbox"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; +import ProgressBar from "../elements/ProgressBar"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -46,7 +47,11 @@ const Entry = ({ room, checked, onChange }) => { return ; }; @@ -104,9 +109,9 @@ export const AddExistingToSpace: React.FC = ({ space, key={room.roomId} room={room} checked={selected.has(room)} - onChange={(checked) => { + onChange={onChange ? (checked) => { onChange(checked, room); - }} + } : null} />; }) }
    @@ -120,9 +125,9 @@ export const AddExistingToSpace: React.FC = ({ space, key={space.roomId} room={space} checked={selected.has(space)} - onChange={(checked) => { + onChange={onChange ? (checked) => { onChange(checked, space); - }} + } : null} />; }) }
    @@ -136,9 +141,9 @@ export const AddExistingToSpace: React.FC = ({ space, key={room.roomId} room={room} checked={selected.has(room)} - onChange={(checked) => { + onChange={onChange ? (checked) => { onChange(checked, room); - }} + } : null} />; }) }
    @@ -156,8 +161,8 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); const [selectedToAdd, setSelectedToAdd] = useState(new Set()); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); + const [progress, setProgress] = useState(null); + const [error, setError] = useState(null); let spaceOptionSection; if (existingSubspaces.length > 0) { @@ -197,6 +202,82 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space,
    ; + const addRooms = async () => { + setError(null); + setProgress(0); + + let error; + + for (const room of selectedToAdd) { + const via = calculateRoomVia(room); + try { + await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { + if (e.errcode === "M_LIMIT_EXCEEDED") { + await sleep(e.data.retry_after_ms); + return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry + } + + throw e; + }); + setProgress(i => i + 1); + } catch (e) { + console.error("Failed to add rooms to space", e); + setError(error = e); + break; + } + } + + if (!error) { + onFinished(true); + } + }; + + const busy = progress !== null; + + let footer; + if (error) { + footer = <> + + + +
    { _t("Not all selected were added") }
    +
    { _t("Try again") }
    +
    + + + { _t("Retry") } + + ; + } else if (busy) { + footer = + +
    + { _t("Adding rooms... (%(progress)s out of %(count)s)", { + count: selectedToAdd.size, + progress, + }) } +
    +
    ; + } else { + footer = <> + +
    { _t("Want to add a new room instead?") }
    + onCreateRoomClick(cli, space)} kind="link"> + { _t("Create a new room") } + +
    + + + { _t("Add") } + + ; + } + return = ({ matrixClient: cli, space, onFinished={onFinished} fixedWidth={false} > - { error &&
    { error }
    } - { + onChange={!busy && !error ? (checked, room) => { if (checked) { selectedToAdd.add(room); } else { selectedToAdd.delete(room); } setSelectedToAdd(new Set(selectedToAdd)); - }} + } : null} />
    - -
    { _t("Don't want to add an existing room?") }
    - onCreateRoomClick(cli, space)} kind="link"> - { _t("Create a new room") } - -
    - - { - // 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); - }} - > - { busy ? _t("Adding...") : _t("Add") } - + { footer }
    ; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2e5431620d..6888148873 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2033,10 +2033,11 @@ "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?", + "Not all selected were added": "Not all selected were added", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", + "Want to add a new room instead?": "Want to add a new room instead?", "Create a new room": "Create a new room", - "Failed to add rooms to space": "Failed to add rooms to space", - "Adding...": "Adding...", "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", @@ -2675,6 +2676,8 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", + "Failed to add rooms to space": "Failed to add rooms to space", + "Adding...": "Adding...", "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", From acce9a4548c449f7347f0c077f333375b5ec253b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 11:50:55 +0100 Subject: [PATCH 277/283] Fix rounded progress bars --- res/css/views/dialogs/_AddExistingToSpaceDialog.scss | 2 +- res/css/views/elements/_ProgressBar.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index c29c9791a6..524f107165 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -178,7 +178,7 @@ limitations under the License. height: 8px; width: 100%; - @mixin ProgressBarBorderRadius "8px"; + @mixin ProgressBarBorderRadius 8px; } .mx_AddExistingToSpaceDialog_progressText { diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss index 770978e921..c075ac74ff 100644 --- a/res/css/views/elements/_ProgressBar.scss +++ b/res/css/views/elements/_ProgressBar.scss @@ -21,7 +21,7 @@ progress.mx_ProgressBar { appearance: none; border: none; - @mixin ProgressBarBorderRadius "6px"; + @mixin ProgressBarBorderRadius 6px; @mixin ProgressBarColour $progressbar-fg-color; @mixin ProgressBarBgColour $progressbar-bg-color; ::-webkit-progress-value { From 886959f32df0170ad65c1a95c3f8cdd9f56e325e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 11:54:14 +0100 Subject: [PATCH 278/283] port rate limiting code over to space creation wizard's add existing rooms --- src/components/structures/SpaceRoomView.tsx | 27 ++++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index cdf9dc02d3..a7a95d711c 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -52,7 +52,7 @@ 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 {sleep} from "../../utils/promise"; import {calculateRoomVia} from "../../utils/permalinks/Permalinks"; interface IProps { @@ -389,15 +389,24 @@ const SpaceAddExistingRooms = ({ space, 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")); + + for (const room of selectedToAdd) { + const via = calculateRoomVia(room); + try { + await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { + if (e.errcode === "M_LIMIT_EXCEEDED") { + await sleep(e.data.retry_after_ms); + return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry + } + + throw e; + }); + } catch (e) { + console.error("Failed to add rooms to space", e); + setError(_t("Failed to add rooms to space")); + break; + } } setBusy(false); }; From 4279e99e4c2df546fea8536b8c6a46ea34d748a2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 15:27:31 +0100 Subject: [PATCH 279/283] Improve performance of search all spaces and space switching --- src/stores/room-list/RoomListStore.ts | 12 +++++++----- src/stores/room-list/algorithms/Algorithm.ts | 1 - 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 77beeb4ba1..58eb6ed317 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -668,7 +668,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { * and thus might not cause an update to the store immediately. * @param {IFilterCondition} filter The filter condition to add. */ - public addFilter(filter: IFilterCondition): void { + public async addFilter(filter: IFilterCondition): Promise { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log("Adding filter condition:", filter); @@ -680,12 +680,14 @@ export class RoomListStoreClass extends AsyncStoreWithClient { promise = this.recalculatePrefiltering(); } else { this.filterConditions.push(filter); - 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(); + // this has to be awaited so that `setKnownRooms` is called in time for the `addFilterCondition` below + // this way the runtime filters are only evaluated on one dataset and not both. + await this.recalculatePrefiltering(); + } + if (this.algorithm) { + this.algorithm.addFilterCondition(filter); } } promise.then(() => this.updateFn.trigger()); diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 83ee803115..c0268d57a8 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -579,7 +579,6 @@ export class Algorithm extends EventEmitter { this.cachedRooms = newTags; this.updateTagsFromCache(); - this.recalculateFilteredRooms(); // Now that we've finished generation, we need to update the sticky room to what // it was. It's entirely possible that it changed lists though, so if it did then From 7f396bedd03d52036ba123a28672651ba02d7698 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 15:59:02 +0100 Subject: [PATCH 280/283] add comment --- src/stores/room-list/algorithms/Algorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index c0268d57a8..f3f0b178dd 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -577,7 +577,7 @@ export class Algorithm extends EventEmitter { await this.generateFreshTags(newTags); - this.cachedRooms = newTags; + this.cachedRooms = newTags; // this recalculates the filtered rooms for us this.updateTagsFromCache(); // Now that we've finished generation, we need to update the sticky room to what From 570c082573942a65fd1cf6052d498138c97f5f15 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 May 2021 09:58:45 -0600 Subject: [PATCH 281/283] Promote colour to a variable I refuse to try and type this variable name freehand. --- res/themes/dark/css/_dark.scss | 7 ++++--- res/themes/light/css/_light.scss | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 334bf581f3..d31b67d4ab 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6; $header-panel-text-secondary-color: #c8c8cd; $text-primary-color: #ffffff; $text-secondary-color: #B9BEC6; +$quaternary-fg-color: #6F7882; $search-bg-color: #181b21; $search-placeholder-color: #61708b; $room-highlight-color: #343a46; @@ -42,11 +43,11 @@ $preview-bar-bg-color: $header-panel-bg-color; $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); $inverted-bg-color: $base-color; -$voice-record-stop-border-color: #6F7882; // "Quartary" +$voice-record-stop-border-color: $quaternary-fg-color; $voice-record-waveform-bg-color: #394049; // "Dark Tile" $voice-record-waveform-fg-color: $secondary-fg-color; -$voice-record-waveform-incomplete-fg-color: #6F7882; // "Quartary" -$voice-record-icon-color: #6F7882; // "Quartary" +$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; +$voice-record-icon-color: $quaternary-fg-color; $voice-playback-button-bg-color: $tertiary-fg-color; $voice-playback-button-fg-color: #21262C; // "Separator" diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 193b7f816c..240e78b1e2 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16); $primary-fg-color: #2e2f32; $secondary-fg-color: #737D8C; $tertiary-fg-color: #8D99A5; +$quaternary-fg-color: #C1C6CD; $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) @@ -190,7 +191,7 @@ $voice-record-live-circle-color: #ff4b55; $voice-record-stop-border-color: #E3E8F0; // "Separator" $voice-record-waveform-bg-color: #E3E8F0; // "Separator" $voice-record-waveform-fg-color: $secondary-fg-color; -$voice-record-waveform-incomplete-fg-color: #C1C6CD; // "Quartary" +$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; $voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $primary-bg-color; $voice-playback-button-fg-color: $secondary-fg-color; From 39ef376a793a26efcb34576a66f3aee21976826a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 5 May 2021 18:06:42 +0200 Subject: [PATCH 282/283] Use Android SDK instead Co-authored-by: J. Ryan Stinnett --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81a4a00515..b3e96ef001 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Platform Targets: * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. * Mobile Web is not currently a target platform - instead please use the native iOS (https://github.com/matrix-org/matrix-ios-kit) and Android - (https://github.com/vector-im/element-android) SDKs. + (https://github.com/matrix-org/matrix-android-sdk2) SDKs. All code lands on the `develop` branch - `master` is only used for stable releases. **Please file PRs against `develop`!!** From 2b703e8574a9d46b75ce75706ff00dec33dd0801 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 17:30:14 +0100 Subject: [PATCH 283/283] tweak code style --- src/stores/SpaceStore.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index db93a23216..5c5870a993 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -201,9 +201,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const childRoom = this.matrixClient?.getRoom(roomId); const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs(); return getOrder(ev.getContent().order, createTs, roomId); - }) - .map(ev => this.matrixClient.getRoom(ev.getStateKey())) - .filter(room => room?.getMyMembership() === "join" || room?.getMyMembership() === "invite") || []; + }).map(ev => { + return this.matrixClient.getRoom(ev.getStateKey()); + }).filter(room => { + return room?.getMyMembership() === "join" || room?.getMyMembership() === "invite"; + }) || []; } public getChildRooms(spaceId: string): Room[] {