From f420c8598516b4f939b7ab90f5fa263db2aaa94a Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Mon, 15 Feb 2021 19:13:09 +0530 Subject: [PATCH 001/183] Added invite option to room's context menu Signed-off-by: Jaiwanth --- res/css/views/rooms/_RoomTile.scss | 4 ++++ src/components/views/rooms/RoomTile.tsx | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 8eca3f1efa..377b207490 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -188,6 +188,10 @@ limitations under the License. .mx_RoomTile_iconSettings::before { mask-image: url('$(res)/img/element-icons/settings.svg'); } + + .mx_RoomTile_iconInvite::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } .mx_RoomTile_iconSignOut::before { mask-image: url('$(res)/img/element-icons/leave.svg'); diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 835447dc18..3a34a0daaa 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -331,6 +331,17 @@ export default class RoomTile extends React.PureComponent { this.setState({generalMenuPosition: null}); // hide the menu }; + private onInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + dis.dispatch({ + action: 'view_invite', + roomId: this.props.room.roomId, + }); + this.setState({generalMenuPosition: null}); // hide the menu + }; + private async saveNotifState(ev: ButtonEvent, newState: Volume) { ev.preventDefault(); ev.stopPropagation(); @@ -470,7 +481,11 @@ export default class RoomTile extends React.PureComponent { label={lowPriorityLabel} iconClassName="mx_RoomTile_iconArrowDown" /> - + Date: Mon, 15 Feb 2021 20:52:19 +0530 Subject: [PATCH 002/183] Check whether user has permission to invite Signed-off-by: Jaiwanth --- res/css/views/rooms/_RoomTile.scss | 2 +- src/components/views/rooms/RoomTile.tsx | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 377b207490..72d29dfd4c 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -188,7 +188,7 @@ limitations under the License. .mx_RoomTile_iconSettings::before { mask-image: url('$(res)/img/element-icons/settings.svg'); } - + .mx_RoomTile_iconInvite::before { mask-image: url('$(res)/img/element-icons/room/invite.svg'); } diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 3a34a0daaa..f168235335 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -462,6 +462,16 @@ export default class RoomTile extends React.PureComponent { const isLowPriority = roomTags.includes(DefaultTagID.LowPriority); const lowPriorityLabel = _t("Low Priority"); + const inRoom = this.props.room && this.props.room.getMyMembership() === "join"; + const userId = MatrixClientPeg.get().getUserId(); + let canInvite = inRoom; + const powerLevels = this.props.room.currentState + .getStateEvents("m.room.power_levels", "") + ?.getContent(); + const me = this.props.room.getMember(userId); + if (powerLevels && me && powerLevels.invite > me.powerLevel) { + canInvite = false; + } contextMenu = { label={lowPriorityLabel} iconClassName="mx_RoomTile_iconArrowDown" /> - + {canInvite ? ( + + ) : null} Date: Tue, 16 Feb 2021 18:59:22 +0530 Subject: [PATCH 003/183] Update src/components/views/rooms/RoomTile.tsx --- 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 f168235335..3894f557fc 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -462,7 +462,7 @@ export default class RoomTile extends React.PureComponent { const isLowPriority = roomTags.includes(DefaultTagID.LowPriority); const lowPriorityLabel = _t("Low Priority"); - const inRoom = this.props.room && this.props.room.getMyMembership() === "join"; + const inRoom = this.props.room.getMyMembership() === "join"; const userId = MatrixClientPeg.get().getUserId(); let canInvite = inRoom; const powerLevels = this.props.room.currentState From 21b9ab9d8595ffbdb881e8c4e4ae33d9829d0dcb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 16 Feb 2021 15:17:51 -0700 Subject: [PATCH 004/183] Add an optional config option to make the welcome page the login page This is commonly requested by deployments with custom setups or those using SSO. Instead of having them all ship the same welcome.html with redirect code in it, we can offer a built-in redirect. Note that this doesn't actually redirect and instead just replaces the view. This is to make the change less invasive as otherwise it involves changing the routing layers. --- src/components/structures/MatrixChat.tsx | 6 ++++-- src/utils/{pages.js => pages.ts} | 17 +++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) rename src/utils/{pages.js => pages.ts} (68%) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 5045e44182..35e08e8d37 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -82,6 +82,7 @@ import {UIFeature} from "../../settings/UIFeature"; import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import DialPadModal from "../views/voip/DialPadModal"; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; +import { shouldUseLoginForWelcome } from "../../utils/pages"; /** constants for MatrixChat.state.view */ export enum Views { @@ -1988,7 +1989,7 @@ export default class MatrixChat extends React.PureComponent { ); } - } else if (this.state.view === Views.WELCOME) { + } else if (this.state.view === Views.WELCOME && !shouldUseLoginForWelcome(SdkConfig.get())) { const Welcome = sdk.getComponent('auth.Welcome'); view = ; } else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) { @@ -2020,7 +2021,8 @@ export default class MatrixChat extends React.PureComponent { {...this.getServerProperties()} /> ); - } else if (this.state.view === Views.LOGIN) { + } else if (this.state.view === Views.LOGIN + || (this.state.view === Views.WELCOME && shouldUseLoginForWelcome(SdkConfig.get()))) { const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset); const Login = sdk.getComponent('structures.auth.Login'); view = ( diff --git a/src/utils/pages.js b/src/utils/pages.ts similarity index 68% rename from src/utils/pages.js rename to src/utils/pages.ts index d63ca3f2c7..bae76be29d 100644 --- a/src/utils/pages.js +++ b/src/utils/pages.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function getHomePageUrl(appConfig) { +import { ConfigOptions } from "../SdkConfig"; + +export function getHomePageUrl(appConfig: ConfigOptions): string | null { const pagesConfig = appConfig.embeddedPages; - let pageUrl = null; - if (pagesConfig) { - pageUrl = pagesConfig.homeUrl; - } + let pageUrl = pagesConfig?.homeUrl; + if (!pageUrl) { // This is a deprecated config option for the home page // (despite the name, given we also now have a welcome @@ -29,3 +29,8 @@ export function getHomePageUrl(appConfig) { return pageUrl; } + +export function shouldUseLoginForWelcome(appConfig: ConfigOptions): boolean { + const pagesConfig = appConfig.embeddedPages; + return pagesConfig?.loginForWelcome === true; +} From 563620484df6fa79f666ef72f414838cf0e342f1 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 25 Feb 2021 14:39:20 -0500 Subject: [PATCH 005/183] Support replying with a message command Signed-off-by: Robin Townsend --- src/SlashCommands.tsx | 28 +++++++----- .../views/rooms/SendMessageComposer.js | 44 +++++++++++++------ 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 6b5f261374..06468c135e 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -20,6 +20,7 @@ limitations under the License. import * as React from 'react'; +import { ContentHelpers } from 'matrix-js-sdk'; import {MatrixClientPeg} from './MatrixClientPeg'; import dis from './dispatcher/dispatcher'; import * as sdk from './index'; @@ -126,10 +127,10 @@ export class Command { return this.getCommand() + " " + this.args; } - run(roomId: string, args: string, cmd: string) { + run(roomId: string, args: string) { // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` if (!this.runFn) return reject(_t("Command error")); - return this.runFn.bind(this)(roomId, args, cmd); + return this.runFn.bind(this)(roomId, args); } getUsage() { @@ -163,7 +164,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(MatrixClientPeg.get().sendTextMessage(roomId, message)); + return success(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -176,7 +177,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(MatrixClientPeg.get().sendTextMessage(roomId, message)); + return success(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -189,7 +190,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(MatrixClientPeg.get().sendTextMessage(roomId, message)); + return success(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -202,7 +203,7 @@ export const Commands = [ if (args) { message = message + ' ' + args; } - return success(MatrixClientPeg.get().sendTextMessage(roomId, message)); + return success(ContentHelpers.makeTextMessage(message)); }, category: CommandCategories.messages, }), @@ -211,7 +212,7 @@ export const Commands = [ args: '', description: _td('Sends a message as plain text, without interpreting it as markdown'), runFn: function(roomId, messages) { - return success(MatrixClientPeg.get().sendTextMessage(roomId, messages)); + return success(ContentHelpers.makeTextMessage(messages)); }, category: CommandCategories.messages, }), @@ -220,7 +221,7 @@ export const Commands = [ args: '', description: _td('Sends a message as html, without interpreting it as markdown'), runFn: function(roomId, messages) { - return success(MatrixClientPeg.get().sendHtmlMessage(roomId, messages, messages)); + return success(ContentHelpers.makeHtmlMessage(messages, messages)); }, category: CommandCategories.messages, }), @@ -966,7 +967,7 @@ export const Commands = [ args: '', runFn: function(roomId, args) { if (!args) return reject(this.getUserId()); - return success(MatrixClientPeg.get().sendHtmlMessage(roomId, args, textToHtmlRainbow(args))); + return success(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args))); }, category: CommandCategories.messages, }), @@ -976,7 +977,7 @@ export const Commands = [ args: '', runFn: function(roomId, args) { if (!args) return reject(this.getUserId()); - return success(MatrixClientPeg.get().sendHtmlEmote(roomId, args, textToHtmlRainbow(args))); + return success(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args))); }, category: CommandCategories.messages, }), @@ -1201,10 +1202,13 @@ export function parseCommandString(input: string) { * processing the command, or 'promise' if a request was sent out. * Returns null if the input didn't match a command. */ -export function getCommand(roomId: string, input: string) { +export function getCommand(input: string) { const {cmd, args} = parseCommandString(input); if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) { - return () => CommandMap.get(cmd).run(roomId, args, cmd); + return { + cmd: CommandMap.get(cmd), + args, + }; } } diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 62c474e417..d1482c0df6 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -33,7 +33,7 @@ import ReplyThread from "../elements/ReplyThread"; import {parseEvent} from '../../../editor/deserialize'; import {findEditableEvent} from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import {getCommand} from '../../../SlashCommands'; +import {CommandCategories, getCommand} from '../../../SlashCommands'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; import {_t, _td} from '../../../languageHandler'; @@ -287,15 +287,22 @@ export default class SendMessageComposer extends React.Component { } return text + part.text; }, ""); - return [getCommand(this.props.room.roomId, commandText), commandText]; + const {cmd, args} = getCommand(commandText); + return [cmd, args, commandText]; } - async _runSlashCommand(fn) { - const cmd = fn(); - let error = cmd.error; - if (cmd.promise) { + async _runSlashCommand(cmd, args) { + const result = cmd.run(this.props.room.roomId, args); + let messageContent; + let error = result.error; + if (result.promise) { try { - await cmd.promise; + if (cmd.category === CommandCategories.messages) { + // The command returns a modified message that we need to pass on + messageContent = await result.promise; + } else { + await result.promise; + } } catch (err) { error = err; } @@ -304,7 +311,7 @@ export default class SendMessageComposer extends React.Component { console.error("Command failure: %s", error); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); // assume the error is a server error when the command is async - const isServerError = !!cmd.promise; + const isServerError = !!result.promise; const title = isServerError ? _td("Server error") : _td("Command error"); let errText; @@ -322,6 +329,7 @@ export default class SendMessageComposer extends React.Component { }); } else { console.log("Command success."); + if (messageContent) return messageContent; } } @@ -330,13 +338,22 @@ export default class SendMessageComposer extends React.Component { return; } + const replyToEvent = this.props.replyToEvent; let shouldSend = true; + let content; if (!containsEmote(this.model) && this._isSlashCommand()) { - const [cmd, commandText] = this._getSlashCommand(); + const [cmd, args, commandText] = this._getSlashCommand(); if (cmd) { - shouldSend = false; - this._runSlashCommand(cmd); + if (cmd.category === CommandCategories.messages) { + content = await this._runSlashCommand(cmd, args); + if (replyToEvent) { + addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator); + } + } else { + this._runSlashCommand(cmd, args); + shouldSend = false; + } } else { // ask the user if their unknown command should be sent as a message const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -371,11 +388,12 @@ export default class SendMessageComposer extends React.Component { this._sendQuickReaction(); } - const replyToEvent = this.props.replyToEvent; if (shouldSend) { const startTime = CountlyAnalytics.getTimestamp(); const {roomId} = this.props.room; - const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent); + if (!content) { + content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent); + } // don't bother sending an empty message if (!content.body.trim()) return; From af5cfff51db48c4a96c5393a25c76675839e864d Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Thu, 4 Mar 2021 23:17:29 +0200 Subject: [PATCH 006/183] feat: edit button on View Source dialog reuse component SendCustomEvent swap it in place in the View Source dialog the Back button takes you to the View Source dialog, not the DevTools dialog do not display the flip toggle box for changing between State Event and Normal Event --- src/components/structures/ViewSource.js | 180 ++++++++++++++---- .../views/context_menus/MessageContextMenu.js | 14 +- .../views/dialogs/DevtoolsDialog.js | 7 +- 3 files changed, 152 insertions(+), 49 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index ca6c0d4226..7fe862cff5 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -16,12 +16,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import SyntaxHighlight from '../views/elements/SyntaxHighlight'; -import {_t} from "../../languageHandler"; +import React from "react"; +import PropTypes from "prop-types"; +import SyntaxHighlight from "../views/elements/SyntaxHighlight"; +import { _t } from "../../languageHandler"; import * as sdk from "../../index"; - +import MatrixClientContext from "../../contexts/MatrixClientContext"; +import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; export default class ViewSource extends React.Component { static propTypes = { @@ -31,48 +32,157 @@ export default class ViewSource extends React.Component { eventId: PropTypes.string.isRequired, isEncrypted: PropTypes.bool.isRequired, decryptedContent: PropTypes.object, + event: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu }; + constructor(props) { + super(props); + + this.state = { + editComponent: null, + }; + } + + onBack() { + this.setState({ editComponent: null }); + } + + editEvent() { + const isStateEvent = this.props.event.isState(); + console.log("isStateEvent", isStateEvent); + if (isStateEvent) { + this.setState({ + editComponent: ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: this.props.event.getType(), + evContent: JSON.stringify( + this.props.event.getContent(), + null, + "\t" + ), + stateKey: this.props.event.getStateKey(), + }} + /> + )} + + ), + }); + } else { + // send an edit-message event + // prefill the "m.new_content" field + const originalContent = this.props.event.getContent(); + const originalEventId = this.props.eventId; + const content = { + ...originalContent, + "m.new_content": originalContent, + "m.relates_to": { + rel_type: "m.replace", + event_id: originalEventId, + }, + }; + this.setState({ + editComponent: ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: this.props.event.getType(), + evContent: JSON.stringify( + content, + null, + "\t" + ), + }} + /> + )} + + ), + }); + } + } + render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); let content; if (this.props.isEncrypted) { - content = <> -
- - {_t("Decrypted event source")} - - - { JSON.stringify(this.props.decryptedContent, null, 2) } - -
-
- - {_t("Original event source")} - - - { JSON.stringify(this.props.content, null, 2) } - -
- ; + content = ( + <> +
+ + + {_t("Decrypted event source")} + + + + {JSON.stringify( + this.props.decryptedContent, + null, + 2 + )} + +
+
+ + + {_t("Original event source")} + + + + {JSON.stringify(this.props.content, null, 2)} + +
+ + ); } else { - content = <> -
{_t("Original event source")}
- - { JSON.stringify(this.props.content, null, 2) } - - ; + content = ( + <> +
+ {_t("Original event source")} +
+ + {JSON.stringify(this.props.content, null, 2)} + + + ); } + const isEditing = this.state.editComponent !== null; + console.log(isEditing); + return ( - -
-
Room ID: { this.props.roomId }
-
Event ID: { this.props.eventId }
+ +
+
+ Room ID: {this.props.roomId} +
+
+ Event ID: {this.props.eventId} +
- { content } + {isEditing ? this.state.editComponent : content}
+ {!isEditing && ( +
+ +
+ )} ); } diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index b002d1ec62..a1c111b19c 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -130,20 +130,10 @@ export default class MessageContextMenu extends React.Component { roomId: ev.getRoomId(), eventId: ev.getId(), content: ev.event, + event: ev, isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(), - decryptedContent: ev._clearEvent, - }, 'mx_Dialog_viewsource'); - this.closeMenu(); - }; - - onViewClearSourceClick = () => { - const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent; - const ViewSource = sdk.getComponent('structures.ViewSource'); - Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, { - roomId: ev.getRoomId(), - eventId: ev.getId(), // FIXME: _clearEvent is private - content: ev._clearEvent, + decryptedContent: ev._clearEvent, }, 'mx_Dialog_viewsource'); this.closeMenu(); }; diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 814378bb51..5d571461fc 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -73,13 +73,14 @@ class GenericEditor extends React.PureComponent { } } -class SendCustomEvent extends GenericEditor { +export class SendCustomEvent extends GenericEditor { static getLabel() { return _t('Send Custom Event'); } static propTypes = { onBack: PropTypes.func.isRequired, room: PropTypes.instanceOf(Room).isRequired, forceStateEvent: PropTypes.bool, + forceGeneralEvent: PropTypes.bool, inputs: PropTypes.object, }; @@ -140,6 +141,8 @@ class SendCustomEvent extends GenericEditor {
; } + const showTglFlip = !this.state.message && !this.props.forceStateEvent && !this.props.forceGeneralEvent; + return
@@ -155,7 +158,7 @@ class SendCustomEvent extends GenericEditor {
{ !this.state.message && } - { !this.state.message && !this.props.forceStateEvent &&
+ { showTglFlip &&
} From 288d98daede9f1ea6b1045e6de930c61f5f14c0e Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Fri, 5 Mar 2021 00:07:59 +0200 Subject: [PATCH 007/183] chore: format, lint --- src/components/structures/ViewSource.js | 58 ++++--------------- .../views/dialogs/DevtoolsDialog.js | 2 +- 2 files changed, 13 insertions(+), 47 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 7fe862cff5..a31876ea76 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -61,11 +61,7 @@ export default class ViewSource extends React.Component { onBack={() => this.onBack()} inputs={{ eventType: this.props.event.getType(), - evContent: JSON.stringify( - this.props.event.getContent(), - null, - "\t" - ), + evContent: JSON.stringify(this.props.event.getContent(), null, "\t"), stateKey: this.props.event.getStateKey(), }} /> @@ -97,11 +93,7 @@ export default class ViewSource extends React.Component { onBack={() => this.onBack()} inputs={{ eventType: this.props.event.getType(), - evContent: JSON.stringify( - content, - null, - "\t" - ), + evContent: JSON.stringify(content, null, "\t"), }} /> )} @@ -120,39 +112,23 @@ export default class ViewSource extends React.Component { <>
- - {_t("Decrypted event source")} - + {_t("Decrypted event source")} - - {JSON.stringify( - this.props.decryptedContent, - null, - 2 - )} - + {JSON.stringify(this.props.decryptedContent, null, 2)}
- - {_t("Original event source")} - + {_t("Original event source")} - - {JSON.stringify(this.props.content, null, 2)} - + {JSON.stringify(this.props.content, null, 2)}
); } else { content = ( <> -
- {_t("Original event source")} -
- - {JSON.stringify(this.props.content, null, 2)} - +
{_t("Original event source")}
+ {JSON.stringify(this.props.content, null, 2)} ); } @@ -161,26 +137,16 @@ export default class ViewSource extends React.Component { console.log(isEditing); return ( - +
-
- Room ID: {this.props.roomId} -
-
- Event ID: {this.props.eventId} -
+
Room ID: {this.props.roomId}
+
Event ID: {this.props.eventId}
{isEditing ? this.state.editComponent : content}
{!isEditing && (
- +
)} diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 5d571461fc..82f2df6534 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -142,7 +142,7 @@ export class SendCustomEvent extends GenericEditor { } const showTglFlip = !this.state.message && !this.props.forceStateEvent && !this.props.forceGeneralEvent; - + return
From 51ac5421c9848be775c977009454e7f55c79d155 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sat, 6 Mar 2021 11:30:31 +0200 Subject: [PATCH 008/183] chore: refactor code pass only the mxEvent object to ViewSource derive the necessary values inside the component --- src/components/structures/ViewSource.js | 161 +++++++++--------- .../views/context_menus/MessageContextMenu.js | 9 +- .../views/messages/EditHistoryMessage.js | 6 +- 3 files changed, 86 insertions(+), 90 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index a31876ea76..369a0a1ddd 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -26,127 +26,134 @@ import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; export default class ViewSource extends React.Component { static propTypes = { - content: PropTypes.object.isRequired, onFinished: PropTypes.func.isRequired, - roomId: PropTypes.string.isRequired, - eventId: PropTypes.string.isRequired, - isEncrypted: PropTypes.bool.isRequired, - decryptedContent: PropTypes.object, - event: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu + mxEvent: PropTypes.object.isRequired, // the MatrixEvent associated with the context menu }; constructor(props) { super(props); this.state = { - editComponent: null, + isEditing: false, }; } onBack() { - this.setState({ editComponent: null }); + this.setState({ isEditing: false }); } - editEvent() { - const isStateEvent = this.props.event.isState(); - console.log("isStateEvent", isStateEvent); - if (isStateEvent) { - this.setState({ - editComponent: ( - - {(cli) => ( - this.onBack()} - inputs={{ - eventType: this.props.event.getType(), - evContent: JSON.stringify(this.props.event.getContent(), null, "\t"), - stateKey: this.props.event.getStateKey(), - }} - /> - )} - - ), - }); - } else { - // send an edit-message event - // prefill the "m.new_content" field - const originalContent = this.props.event.getContent(); - const originalEventId = this.props.eventId; - const content = { - ...originalContent, - "m.new_content": originalContent, - "m.relates_to": { - rel_type: "m.replace", - event_id: originalEventId, - }, - }; - this.setState({ - editComponent: ( - - {(cli) => ( - this.onBack()} - inputs={{ - eventType: this.props.event.getType(), - evContent: JSON.stringify(content, null, "\t"), - }} - /> - )} - - ), - }); - } + onEdit() { + this.setState({ isEditing: true }); } - render() { - const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + // returns the dialog body for viewing the event source + viewSourceContent() { + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(); + const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private + const originalEventSource = mxEvent.event; - let content; - if (this.props.isEncrypted) { - content = ( + if (isEncrypted) { + return ( <>
{_t("Decrypted event source")} - {JSON.stringify(this.props.decryptedContent, null, 2)} + {JSON.stringify(decryptedEventSource, null, 2)}
{_t("Original event source")} - {JSON.stringify(this.props.content, null, 2)} + {JSON.stringify(originalEventSource, null, 2)}
); } else { - content = ( + return ( <>
{_t("Original event source")}
- {JSON.stringify(this.props.content, null, 2)} + {JSON.stringify(originalEventSource, null, 2)} ); } + } - const isEditing = this.state.editComponent !== null; - console.log(isEditing); + // returns the SendCustomEvent component prefilled with the correct details + editSourceContent() { + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + const isStateEvent = mxEvent.isState(); + console.log("isStateEvent", isStateEvent); + const roomId = mxEvent.getRoomId(); + const eventId = mxEvent.getId(); + const originalContent = mxEvent.getContent(); + if (isStateEvent) { + return ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: mxEvent.getType(), + evContent: JSON.stringify(originalContent, null, "\t"), + stateKey: mxEvent.getStateKey(), + }} + /> + )} + + ); + } else { + // send an edit-message event + // prefill the "m.new_content" field + const newContent = { + ...originalContent, + "m.new_content": originalContent, + "m.relates_to": { + rel_type: "m.replace", + event_id: eventId, + }, + }; + return ( + + {(cli) => ( + this.onBack()} + inputs={{ + eventType: mxEvent.getType(), + evContent: JSON.stringify(newContent, null, "\t"), + }} + /> + )} + + ); + } + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + + const isEditing = this.state.isEditing; + const roomId = mxEvent.getRoomId(); + const eventId = mxEvent.getId(); return (
-
Room ID: {this.props.roomId}
-
Event ID: {this.props.eventId}
+
Room ID: {roomId}
+
Event ID: {eventId}
- {isEditing ? this.state.editComponent : content} + {isEditing ? this.editSourceContent() : this.viewSourceContent()}
{!isEditing && (
- +
)} diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index a1c111b19c..6809d28e36 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -124,16 +124,9 @@ export default class MessageContextMenu extends React.Component { }; onViewSourceClick = () => { - const ev = this.props.mxEvent.replacingEvent() || this.props.mxEvent; const ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createTrackedDialog('View Event Source', '', ViewSource, { - roomId: ev.getRoomId(), - eventId: ev.getId(), - content: ev.event, - event: ev, - isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(), - // FIXME: _clearEvent is private - decryptedContent: ev._clearEvent, + mxEvent: this.props.mxEvent, }, 'mx_Dialog_viewsource'); this.closeMenu(); }; diff --git a/src/components/views/messages/EditHistoryMessage.js b/src/components/views/messages/EditHistoryMessage.js index 68a3c95745..3bd9dfbd21 100644 --- a/src/components/views/messages/EditHistoryMessage.js +++ b/src/components/views/messages/EditHistoryMessage.js @@ -74,11 +74,7 @@ export default class EditHistoryMessage extends React.PureComponent { _onViewSourceClick = () => { const ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createTrackedDialog('View Event Source', 'Edit history', ViewSource, { - roomId: this.props.mxEvent.getRoomId(), - eventId: this.props.mxEvent.getId(), - content: this.props.mxEvent.event, - isEncrypted: this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(), - decryptedContent: this.props.mxEvent._clearEvent, + mxEvent: this.props.mxEvent, }, 'mx_Dialog_viewsource'); }; From 29b95e60833fef1bed598897dad2deff4e875ff4 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sat, 6 Mar 2021 16:47:29 +0200 Subject: [PATCH 009/183] fix: make edit prefill work correctly from EditHistory handle encrypted and unencrypted events get the correct event_id (the base message) when called from EditHistoryMessage keep only the `body` and `msgtype` fields when prefilling --- src/components/structures/ViewSource.js | 33 +++++++++++++++++++------ 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 369a0a1ddd..4ee70ee2a7 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -39,6 +39,7 @@ export default class ViewSource extends React.Component { } onBack() { + // TODO: refresh the "Event ID:" modal header this.setState({ isEditing: false }); } @@ -80,15 +81,28 @@ export default class ViewSource extends React.Component { } } + // returns the id of the initial message, not the id of the previous edit + getBaseEventId() { + const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit + const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(); + const baseMxEvent = this.props.mxEvent; + + if (isEncrypted) { + // `relates_to` field is inside the encrypted event + return mxEvent.event.content["m.relates_to"]?.event_id ?? baseMxEvent.getId(); + } else { + return mxEvent.getContent()["m.relates_to"]?.event_id ?? baseMxEvent.getId(); + } + } + // returns the SendCustomEvent component prefilled with the correct details editSourceContent() { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit const isStateEvent = mxEvent.isState(); - console.log("isStateEvent", isStateEvent); const roomId = mxEvent.getRoomId(); - const eventId = mxEvent.getId(); const originalContent = mxEvent.getContent(); + if (isStateEvent) { return ( @@ -107,14 +121,19 @@ export default class ViewSource extends React.Component { ); } else { - // send an edit-message event - // prefill the "m.new_content" field + // prefill an edit-message event + // keep only the `body` and `msgtype` fields of originalContent + const bodyToStartFrom = originalContent["m.new_content"]?.body ?? originalContent.body; // prefill the last edit body, to start editing from there const newContent = { - ...originalContent, - "m.new_content": originalContent, + "body": ` * ${bodyToStartFrom}`, + "msgtype": originalContent.msgtype, + "m.new_content": { + body: bodyToStartFrom, + msgtype: originalContent.msgtype, + }, "m.relates_to": { rel_type: "m.replace", - event_id: eventId, + event_id: this.getBaseEventId(), }, }; return ( From df52ec28d60ecdf9719f3b5339a805f6a643f753 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Sat, 6 Mar 2021 17:09:46 +0200 Subject: [PATCH 010/183] fix: show edit button only if you have permission --- src/components/structures/ViewSource.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 4ee70ee2a7..ddcffe4f7f 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -23,6 +23,7 @@ import { _t } from "../../languageHandler"; import * as sdk from "../../index"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; +import { canEditContent } from "../../utils/EventUtils"; export default class ViewSource extends React.Component { static propTypes = { @@ -162,6 +163,7 @@ export default class ViewSource extends React.Component { const isEditing = this.state.isEditing; const roomId = mxEvent.getRoomId(); const eventId = mxEvent.getId(); + const canEdit = canEditContent(this.props.mxEvent); return (
@@ -170,7 +172,7 @@ export default class ViewSource extends React.Component {
{isEditing ? this.editSourceContent() : this.viewSourceContent()}
- {!isEditing && ( + {!isEditing && canEdit && (
From 6106e01ab496f16ac4c36911f67f8df817cf2025 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 8 Mar 2021 09:39:07 +0000 Subject: [PATCH 011/183] Collapse redactions into an event list summary --- src/components/structures/MessagePanel.js | 93 ++++++++++++++++++++++- src/i18n/strings/en_EN.json | 2 + 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 161227a139..28214aada9 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -1001,6 +1001,97 @@ class CreationGrouper { } } +class RedactionGrouper { + static canStartGroup = function(panel, ev) { + return panel._shouldShowEvent(ev) && ev.isRedacted(); + } + + constructor(panel, ev, prevEvent, lastShownEvent) { + this.panel = panel; + this.readMarker = panel._readMarkerForEvent( + ev.getId(), + ev === lastShownEvent, + ); + this.events = [ev]; + this.prevEvent = prevEvent; + this.lastShownEvent = lastShownEvent; + } + + shouldGroup(ev) { + if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) { + return false; + } + return ev.isRedacted(); + } + + add(ev) { + this.readMarker = this.readMarker || this.panel._readMarkerForEvent( + ev.getId(), + ev === this.lastShownEvent, + ); + this.events.push(ev); + } + + getTiles() { + if (!this.events || !this.events.length) return []; + + const DateSeparator = sdk.getComponent('messages.DateSeparator'); + const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); + + const panel = this.panel; + const ret = []; + const lastShownEvent = this.lastShownEvent; + + if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { + const ts = this.events[0].getTs(); + ret.push( +
  • , + ); + } + + const key = "redactioneventlistsummary-" + ( + this.prevEvent ? this.events[0].getId() : "initial" + ); + + const senders = new Set(); + let eventTiles = this.events.map((e) => { + senders.add(e.sender); + // In order to prevent DateSeparators from appearing in the expanded form, + // 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); + }).reduce((a, b) => a.concat(b), []); + + if (eventTiles.length === 0) { + eventTiles = null; + } + + ret.push( + + { eventTiles } + , + ); + + if (this.readMarker) { + ret.push(this.readMarker); + } + + return ret; + } + + getNewPrevEvent() { + return this.events[0]; + } +} + // Wrap consecutive member events in a ListSummary, ignore if redacted class MemberGrouper { static canStartGroup = function(panel, ev) { @@ -1111,4 +1202,4 @@ class MemberGrouper { } // all the grouper classes that we use -const groupers = [CreationGrouper, MemberGrouper]; +const groupers = [CreationGrouper, MemberGrouper, RedactionGrouper]; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index daa38b9b3e..e78792ec51 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2541,6 +2541,8 @@ "Logout": "Logout", "%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", + "%(count)s messages deleted.|other": "%(count)s messages deleted.", + "%(count)s messages deleted.|one": "%(count)s message deleted.", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", From 9287e8dfa4f55b368a3c108d3c15442cdfdc4c1c Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Mon, 8 Mar 2021 22:15:34 +0200 Subject: [PATCH 012/183] use isEncrypted, edit state events --- src/components/structures/ViewSource.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index ddcffe4f7f..cfe28e9f73 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -51,7 +51,7 @@ export default class ViewSource extends React.Component { // returns the dialog body for viewing the event source viewSourceContent() { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit - const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(); + const isEncrypted = mxEvent.isEncrypted(); const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private const originalEventSource = mxEvent.event; @@ -85,7 +85,7 @@ export default class ViewSource extends React.Component { // returns the id of the initial message, not the id of the previous edit getBaseEventId() { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit - const isEncrypted = this.props.mxEvent.getType() !== this.props.mxEvent.getWireType(); + const isEncrypted = mxEvent.isEncrypted(); const baseMxEvent = this.props.mxEvent; if (isEncrypted) { @@ -163,7 +163,7 @@ export default class ViewSource extends React.Component { const isEditing = this.state.isEditing; const roomId = mxEvent.getRoomId(); const eventId = mxEvent.getId(); - const canEdit = canEditContent(this.props.mxEvent); + const canEdit = canEditContent(this.props.mxEvent) || mxEvent.isState(); return (
    From 0936ea7e640ac10449150eaed1615bc99c52e70c Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Tue, 9 Mar 2021 14:46:37 +0200 Subject: [PATCH 013/183] feat: show edit button only when user has permissions call appropriate functions for state events and edit message events --- src/components/structures/ViewSource.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index cfe28e9f73..39666edd65 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -24,6 +24,7 @@ import * as sdk from "../../index"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import { SendCustomEvent } from "../views/dialogs/DevtoolsDialog"; import { canEditContent } from "../../utils/EventUtils"; +import { MatrixClientPeg } from '../../MatrixClientPeg'; export default class ViewSource extends React.Component { static propTypes = { @@ -156,6 +157,12 @@ export default class ViewSource extends React.Component { } } + canSendStateEvent(mxEvent) { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(mxEvent.getRoomId()); + return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli); + } + render() { const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit @@ -163,7 +170,7 @@ export default class ViewSource extends React.Component { const isEditing = this.state.isEditing; const roomId = mxEvent.getRoomId(); const eventId = mxEvent.getId(); - const canEdit = canEditContent(this.props.mxEvent) || mxEvent.isState(); + const canEdit = mxEvent.isState() ? this.canSendStateEvent(mxEvent) : canEditContent(this.props.mxEvent); return (
    From 681529aa3cb54b630fd02a25613d4f9e7379c536 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 17:26:35 +0000 Subject: [PATCH 014/183] Upgrade matrix-js-sdk to 9.9.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 7ed1b272da..e7fff438ff 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": "9.9.0-rc.1", "matrix-widget-api": "^0.1.0-beta.13", "minimist": "^1.2.5", "pako": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index f99ea5900d..5c78e70590 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5572,9 +5572,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.8.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/fb73ab687826e4d05fb8b424ab013a771213f84f" +matrix-js-sdk@9.9.0-rc.1: + version "9.9.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.9.0-rc.1.tgz#5ee28aee89a87ccdf742d1512bc44ec54454e94f" + integrity sha512-A3pY5CyCNE5+QdpYL/C7FGU8KjBojXRWbtWsbeTMFwZEuzkIYlCtev1i07NbN3kbF83R3TtOfSCxQITm1C5jwg== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 89f561a1ee645873e0a93fe1a6bc8967560ae8ab Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 17:31:17 +0000 Subject: [PATCH 015/183] Prepare changelog for v3.16.0-rc.1 --- CHANGELOG.md | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c31eedf93b..d4ffeb5fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,110 @@ +Changes in [3.16.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.1) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0...v3.16.0-rc.1) + + * Upgrade to JS SDK 9.9.0-rc.1 + * Translations update from Weblate + [\#5743](https://github.com/matrix-org/matrix-react-sdk/pull/5743) + * Document behaviour of showReadReceipts=false for sent receipts + [\#5739](https://github.com/matrix-org/matrix-react-sdk/pull/5739) + * Tweak sent marker code style + [\#5741](https://github.com/matrix-org/matrix-react-sdk/pull/5741) + * Fix sent markers disappearing for edits/reactions + [\#5737](https://github.com/matrix-org/matrix-react-sdk/pull/5737) + * Ignore to-device decryption in the room list store + [\#5740](https://github.com/matrix-org/matrix-react-sdk/pull/5740) + * Spaces suggested rooms support + [\#5736](https://github.com/matrix-org/matrix-react-sdk/pull/5736) + * Add tooltips to sent/sending receipts + [\#5738](https://github.com/matrix-org/matrix-react-sdk/pull/5738) + * Remove a bunch of useless 'use strict' definitions + [\#5735](https://github.com/matrix-org/matrix-react-sdk/pull/5735) + * [SK-1] Fix types for replaceableComponent + [\#5732](https://github.com/matrix-org/matrix-react-sdk/pull/5732) + * [SK-2] Make debugging skinning problems easier + [\#5733](https://github.com/matrix-org/matrix-react-sdk/pull/5733) + * Support sending invite reasons with /invite command + [\#5695](https://github.com/matrix-org/matrix-react-sdk/pull/5695) + * Fix clicking on the avatar for opening member info requires pixel-perfect + accuracy + [\#5717](https://github.com/matrix-org/matrix-react-sdk/pull/5717) + * Display decrypted and encrypted event source on the same dialog + [\#5713](https://github.com/matrix-org/matrix-react-sdk/pull/5713) + * Fix units of TURN server expiry time + [\#5730](https://github.com/matrix-org/matrix-react-sdk/pull/5730) + * Display room name in pills instead of address + [\#5624](https://github.com/matrix-org/matrix-react-sdk/pull/5624) + * Refresh UI for file uploads + [\#5723](https://github.com/matrix-org/matrix-react-sdk/pull/5723) + * UI refresh for uploaded files + [\#5719](https://github.com/matrix-org/matrix-react-sdk/pull/5719) + * Improve message sending states to match new designs + [\#5699](https://github.com/matrix-org/matrix-react-sdk/pull/5699) + * Add clipboard write permission for widgets + [\#5725](https://github.com/matrix-org/matrix-react-sdk/pull/5725) + * Fix widget resizing + [\#5722](https://github.com/matrix-org/matrix-react-sdk/pull/5722) + * Option for audio streaming + [\#5707](https://github.com/matrix-org/matrix-react-sdk/pull/5707) + * Show a specific error for hs_disabled + [\#5576](https://github.com/matrix-org/matrix-react-sdk/pull/5576) + * Add Edge to the targets list + [\#5721](https://github.com/matrix-org/matrix-react-sdk/pull/5721) + * File drop UI fixes and improvements + [\#5505](https://github.com/matrix-org/matrix-react-sdk/pull/5505) + * Fix Bottom border of state counters is white on the dark theme + [\#5715](https://github.com/matrix-org/matrix-react-sdk/pull/5715) + * Trim spurious whitespace of nicknames + [\#5332](https://github.com/matrix-org/matrix-react-sdk/pull/5332) + * Ensure HostSignupDialog border colour matches light theme + [\#5716](https://github.com/matrix-org/matrix-react-sdk/pull/5716) + * Don't place another call if there's already one ongoing + [\#5712](https://github.com/matrix-org/matrix-react-sdk/pull/5712) + * Space room hierarchies + [\#5706](https://github.com/matrix-org/matrix-react-sdk/pull/5706) + * Iterate Space view and right panel + [\#5705](https://github.com/matrix-org/matrix-react-sdk/pull/5705) + * Add a scroll to bottom on message sent setting + [\#5692](https://github.com/matrix-org/matrix-react-sdk/pull/5692) + * Add .tmp files to gitignore + [\#5708](https://github.com/matrix-org/matrix-react-sdk/pull/5708) + * Initial Space Room View and Creation UX + [\#5704](https://github.com/matrix-org/matrix-react-sdk/pull/5704) + * Add multi language spell check + [\#5452](https://github.com/matrix-org/matrix-react-sdk/pull/5452) + * Fix tetris effect (holes) in read receipts + [\#5697](https://github.com/matrix-org/matrix-react-sdk/pull/5697) + * Fixed edit for markdown images + [\#5703](https://github.com/matrix-org/matrix-react-sdk/pull/5703) + * Iterate Space Panel + [\#5702](https://github.com/matrix-org/matrix-react-sdk/pull/5702) + * Fix read receipts for compact layout + [\#5700](https://github.com/matrix-org/matrix-react-sdk/pull/5700) + * Space Store and Space Panel for Room List filtering + [\#5689](https://github.com/matrix-org/matrix-react-sdk/pull/5689) + * Log when turn creds expire + [\#5691](https://github.com/matrix-org/matrix-react-sdk/pull/5691) + * Null check for maxHeight in call view + [\#5690](https://github.com/matrix-org/matrix-react-sdk/pull/5690) + * Autocomplete invited users + [\#5687](https://github.com/matrix-org/matrix-react-sdk/pull/5687) + * Add send message button + [\#5535](https://github.com/matrix-org/matrix-react-sdk/pull/5535) + * Move call buttons to the room header + [\#5693](https://github.com/matrix-org/matrix-react-sdk/pull/5693) + * Use the default SSSS key if the default is set + [\#5638](https://github.com/matrix-org/matrix-react-sdk/pull/5638) + * Initial Spaces feature flag + [\#5668](https://github.com/matrix-org/matrix-react-sdk/pull/5668) + * Clean up code edge cases and add helpers + [\#5667](https://github.com/matrix-org/matrix-react-sdk/pull/5667) + * Clean up widgets when leaving the room + [\#5684](https://github.com/matrix-org/matrix-react-sdk/pull/5684) + * Fix read receipts? + [\#5567](https://github.com/matrix-org/matrix-react-sdk/pull/5567) + * Fix MAU usage alerts + [\#5678](https://github.com/matrix-org/matrix-react-sdk/pull/5678) + Changes in [3.15.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.15.0) (2021-03-01) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0-rc.1...v3.15.0) From ad1f9edba8297495bf54675bf7c3555fa0b197b6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 17:31:19 +0000 Subject: [PATCH 016/183] v3.16.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e7fff438ff..080b16b4ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.15.0", + "version": "3.16.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", @@ -189,5 +189,6 @@ "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" ] - } + }, + "typings": "./lib/index.d.ts" } From 79ba898b3d6fc1264e785d24fbdfb3a8a9020854 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 18:08:55 +0000 Subject: [PATCH 017/183] Prepare changelog for v3.16.0-rc.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4ffeb5fbf..0b130d1c70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [3.16.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.2) (2021-03-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.1...v3.16.0-rc.2) + + * Fixed incorrect build output in rc.1 + Changes in [3.16.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0-rc.1) (2021-03-10) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.15.0...v3.16.0-rc.1) From 71a3847c35aac3ad58bf49b5e15d986b55d32464 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 10 Mar 2021 18:08:56 +0000 Subject: [PATCH 018/183] v3.16.0-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 080b16b4ea..ddc352104c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.16.0-rc.1", + "version": "3.16.0-rc.2", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 93f7f13c442c0faefd15d04e512bc3575c784585 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 3 Mar 2021 18:16:27 -0700 Subject: [PATCH 019/183] Early proof of concept for media customization support --- src/customisations/Media.ts | 138 ++++++++++++++++++ .../models/IMediaEventContent.ts | 87 +++++++++++ src/utils/DecryptFile.js | 5 +- 3 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/customisations/Media.ts create mode 100644 src/customisations/models/IMediaEventContent.ts diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts new file mode 100644 index 0000000000..27abc6bc50 --- /dev/null +++ b/src/customisations/Media.ts @@ -0,0 +1,138 @@ +/* + * 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 {MatrixClientPeg} from "../MatrixClientPeg"; +import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; + +// Populate this class with the details of your customisations when copying it. + +// Implementation note: The Media class must complete the contract as shown here, though +// the constructor can be whatever is relevant to your implementation. The mediaForX +// functions below create an instance of the Media class and are used throughout the +// project. + +/** + * A media object is a representation of a "source media" and an optional + * "thumbnail media", derived from event contents or external sources. + */ +export class Media { + // Per above, this constructor signature can be whatever is helpful for you. + constructor(private prepared: IPreparedMedia) { + } + + /** + * The MXC URI of the source media. + */ + public get srcMxc(): string { + return this.prepared.mxc; + } + + /** + * The MXC URI of the thumbnail media, if a thumbnail is recorded. Null/undefined + * otherwise. + */ + public get thumbnailMxc(): string | undefined | null { + return this.prepared.thumbnail?.mxc; + } + + /** + * Whether or not a thumbnail is recorded for this media. + */ + public get hasThumbnail(): boolean { + return !!this.thumbnailMxc; + } + + /** + * The HTTP URL for the source media. + */ + public get srcHttp(): string { + return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc); + } + + /** + * Gets the HTTP URL for the thumbnail media with the requested characteristics, if a thumbnail + * is recorded for this media. Returns null/undefined otherwise. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {string} The HTTP URL which points to the thumbnail. + */ + public getThumbnailHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string | null | undefined { + if (!this.hasThumbnail) return null; + return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode); + } + + /** + * Gets the HTTP URL for a thumbnail of the source media with the requested characteristics. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {string} The HTTP URL which points to the thumbnail. + */ + public getThumbnailOfSourceHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string { + return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode); + } + + /** + * Downloads the source media. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadSource(): Promise { + return fetch(this.srcHttp); + } + + /** + * Downloads the thumbnail media with the requested characteristics. If no thumbnail media is present, + * this throws an exception. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadThumbnail(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail"); + return fetch(this.getThumbnailHttp(width, height, mode)); + } + + /** + * Downloads a thumbnail of the source media with the requested characteristics. + * @param {number} width The desired width of the thumbnail. + * @param {number} height The desired height of the thumbnail. + * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. + * @returns {Promise} Resolves to the server's response for chaining. + */ + public downloadThumbnailOfSource(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + return fetch(this.getThumbnailOfSourceHttp(width, height, mode)); + } +} + +/** + * Creates a media object from event content. + * @param {IMediaEventContent} content The event content. + * @returns {Media} The media object. + */ +export function mediaFromContent(content: IMediaEventContent): Media { + return new Media(prepEventContentAsMedia(content)); +} + +/** + * Creates a media object from an MXC URI. + * @param {string} mxc The MXC URI. + * @returns {Media} The media object. + */ +export function mediaFromMxc(mxc: string): Media { + return mediaFromContent({url: mxc}); +} diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts new file mode 100644 index 0000000000..0211a63787 --- /dev/null +++ b/src/customisations/models/IMediaEventContent.ts @@ -0,0 +1,87 @@ +/* + * 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. + */ + +// TODO: These types should be elsewhere. + +export interface IEncryptedFile { + url: string; + key: { + alg: string; + key_ops: string[]; + kty: string; + k: string; + ext: boolean; + }; + iv: string; + hashes: {[alg: string]: string}; + v: string; +} + +export interface IMediaEventContent { + url?: string; // required on unencrypted media + file?: IEncryptedFile; // required for *encrypted* media + info?: { + thumbnail_url?: string; + thumbnail_file?: IEncryptedFile; + }; +} + +export interface IPreparedMedia extends IMediaObject { + thumbnail?: IMediaObject; +} + +export interface IMediaObject { + mxc: string; + file?: IEncryptedFile; +} + +/** + * Parses an event content body into a prepared media object. This prepared media object + * can be used with other functions to manipulate the media. + * @param {IMediaEventContent} content Unredacted media event content. See interface. + * @returns {IPreparedMedia} A prepared media object. + * @throws Throws if the given content cannot be packaged into a prepared media object. + */ +export function prepEventContentAsMedia(content: IMediaEventContent): IPreparedMedia { + let thumbnail: IMediaObject = null; + if (content?.info?.thumbnail_url) { + thumbnail = { + mxc: content.info.thumbnail_url, + file: content.info.thumbnail_file, + }; + } else if (content?.info?.thumbnail_file?.url) { + thumbnail = { + mxc: content.info.thumbnail_file.url, + file: content.info.thumbnail_file, + }; + } + + if (content?.url) { + return { + thumbnail, + mxc: content.url, + file: content.file, + }; + } else if (content?.file?.url) { + return { + thumbnail, + mxc: content.file.url, + file: content.file, + }; + } + + throw new Error("Invalid file provided: cannot determine MXC URI. Has it been redacted?"); +} diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.js index d3625d614a..fb3600cd79 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.js @@ -19,6 +19,7 @@ limitations under the License. import encrypt from 'browser-encrypt-attachment'; // Grab the client so that we can turn mxc:// URLs into https:// URLS. import {MatrixClientPeg} from '../MatrixClientPeg'; +import {mediaFromContent} from "../customisations/Media"; // WARNING: We have to be very careful about what mime-types we allow into blobs, // as for performance reasons these are now rendered via URL.createObjectURL() @@ -87,9 +88,9 @@ const ALLOWED_BLOB_MIMETYPES = { * @returns {Promise} */ export function decryptFile(file) { - const url = MatrixClientPeg.get().mxcUrlToHttp(file.url); + const media = mediaFromContent({file}); // Download the encrypted file as an array buffer. - return Promise.resolve(fetch(url)).then(function(response) { + return media.downloadSource().then(function(response) { return response.arrayBuffer(); }).then(function(responseData) { // Decrypt the array buffer using the information taken from From 53935782bcaa51daab0011ff16236943e994f153 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 3 Mar 2021 18:22:57 -0700 Subject: [PATCH 020/183] Convert DecryptFile to TS and modernize a bit --- .../models/IMediaEventContent.ts | 1 + src/utils/{DecryptFile.js => DecryptFile.ts} | 63 +++++++++---------- 2 files changed, 30 insertions(+), 34 deletions(-) rename src/utils/{DecryptFile.js => DecryptFile.ts} (76%) diff --git a/src/customisations/models/IMediaEventContent.ts b/src/customisations/models/IMediaEventContent.ts index 0211a63787..4cbe07dbd5 100644 --- a/src/customisations/models/IMediaEventContent.ts +++ b/src/customisations/models/IMediaEventContent.ts @@ -18,6 +18,7 @@ export interface IEncryptedFile { url: string; + mimetype?: string; key: { alg: string; key_ops: string[]; diff --git a/src/utils/DecryptFile.js b/src/utils/DecryptFile.ts similarity index 76% rename from src/utils/DecryptFile.js rename to src/utils/DecryptFile.ts index fb3600cd79..93cedbc707 100644 --- a/src/utils/DecryptFile.js +++ b/src/utils/DecryptFile.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd +Copyright 2016, 2018, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,9 +16,8 @@ limitations under the License. // Pull in the encryption lib so that we can decrypt attachments. import encrypt from 'browser-encrypt-attachment'; -// Grab the client so that we can turn mxc:// URLs into https:// URLS. -import {MatrixClientPeg} from '../MatrixClientPeg'; import {mediaFromContent} from "../customisations/Media"; +import {IEncryptedFile} from "../customisations/models/IMediaEventContent"; // WARNING: We have to be very careful about what mime-types we allow into blobs, // as for performance reasons these are now rendered via URL.createObjectURL() @@ -55,48 +53,46 @@ import {mediaFromContent} from "../customisations/Media"; // For the record, mime-types which must NEVER enter this list below include: // text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar. -const ALLOWED_BLOB_MIMETYPES = { - 'image/jpeg': true, - 'image/gif': true, - 'image/png': true, +const ALLOWED_BLOB_MIMETYPES = [ + 'image/jpeg', + 'image/gif', + 'image/png', - 'video/mp4': true, - 'video/webm': true, - 'video/ogg': true, + 'video/mp4', + 'video/webm', + 'video/ogg', - 'audio/mp4': true, - 'audio/webm': true, - 'audio/aac': true, - 'audio/mpeg': true, - 'audio/ogg': true, - 'audio/wave': true, - 'audio/wav': true, - 'audio/x-wav': true, - 'audio/x-pn-wav': true, - 'audio/flac': true, - 'audio/x-flac': true, -}; + 'audio/mp4', + 'audio/webm', + 'audio/aac', + 'audio/mpeg', + 'audio/ogg', + 'audio/wave', + 'audio/wav', + 'audio/x-wav', + 'audio/x-pn-wav', + 'audio/flac', + 'audio/x-flac', +]; /** * Decrypt a file attached to a matrix event. - * @param {Object} file The json taken from the matrix event. + * @param {IEncryptedFile} file The json taken from the matrix event. * This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments} * as the encryption info object, so will also have the those keys in addition to * the keys below. - * @param {string} file.url An mxc:// URL for the encrypted file. - * @param {string} file.mimetype The MIME-type of the plaintext file. - * @returns {Promise} + * @returns {Promise} Resolves to a Blob of the file. */ -export function decryptFile(file) { +export function decryptFile(file: IEncryptedFile): Promise { const media = mediaFromContent({file}); // Download the encrypted file as an array buffer. - return media.downloadSource().then(function(response) { + return media.downloadSource().then((response) => { return response.arrayBuffer(); - }).then(function(responseData) { + }).then((responseData) => { // Decrypt the array buffer using the information taken from // the event content. return encrypt.decryptAttachment(responseData, file); - }).then(function(dataArray) { + }).then((dataArray) => { // Turn the array into a Blob and give it the correct MIME-type. // IMPORTANT: we must not allow scriptable mime-types into Blobs otherwise @@ -104,11 +100,10 @@ export function decryptFile(file) { // browser (e.g. by copying the URI into a new tab or window.) // See warning at top of file. let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : ''; - if (!ALLOWED_BLOB_MIMETYPES[mimetype]) { + if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) { mimetype = 'application/octet-stream'; } - const blob = new Blob([dataArray], {type: mimetype}); - return blob; + return new Blob([dataArray], {type: mimetype}); }); } From 1ac12479ca7b132b9d9d4eddc64ed6aa9b7fbde0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 3 Mar 2021 19:06:46 -0700 Subject: [PATCH 021/183] Convert cases of mxcUrlToHttp to new media customisation --- src/HtmlUtils.tsx | 9 ++--- src/Notifier.ts | 3 +- src/autocomplete/CommunityProvider.tsx | 3 +- src/components/structures/GroupView.js | 11 +++--- src/components/structures/LeftPanel.tsx | 3 +- .../structures/SpaceRoomDirectory.tsx | 15 ++------ src/components/views/avatars/GroupAvatar.tsx | 9 +++-- .../views/dialogs/ConfirmUserActionDialog.js | 6 ++- .../dialogs/EditCommunityPrototypeDialog.tsx | 3 +- .../views/dialogs/IncomingSasDialog.js | 20 +++++----- src/components/views/elements/AddressTile.js | 5 +-- src/components/views/elements/Flair.js | 4 +- src/components/views/elements/Pill.js | 3 +- src/components/views/elements/SSOButtons.tsx | 3 +- src/components/views/elements/TagTile.js | 13 ++++--- .../views/groups/GroupInviteTile.js | 6 ++- .../views/groups/GroupMemberTile.js | 8 ++-- src/components/views/groups/GroupRoomInfo.js | 7 ++-- src/components/views/groups/GroupRoomTile.js | 8 ++-- src/components/views/groups/GroupTile.js | 6 ++- src/components/views/messages/MAudioBody.js | 7 ++-- src/components/views/messages/MFileBody.js | 5 ++- src/components/views/messages/MImageBody.js | 36 +++++++----------- src/components/views/messages/MVideoBody.tsx | 14 ++++--- .../views/messages/RoomAvatarEvent.js | 3 +- src/components/views/right_panel/UserInfo.tsx | 3 +- .../room_settings/RoomProfileSettings.js | 5 ++- .../views/rooms/LinkPreviewWidget.js | 9 +++-- src/components/views/settings/ChangeAvatar.js | 3 +- .../views/settings/ProfileSettings.js | 5 ++- src/customisations/Media.ts | 38 +++++++++++++++++-- src/customisations/models/ResizeMode.ts | 17 +++++++++ src/stores/OwnProfileStore.ts | 9 ++++- 33 files changed, 178 insertions(+), 121 deletions(-) create mode 100644 src/customisations/models/ResizeMode.ts diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 7d6b049914..12752eb20f 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -36,6 +36,7 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; +import {mediaFromMxc} from "./customisations/Media"; linkifyMatrix(linkify); @@ -181,11 +182,9 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) { return { tagName, attribs: {}}; } - attribs.src = MatrixClientPeg.get().mxcUrlToHttp( - attribs.src, - attribs.width || 800, - attribs.height || 600, - ); + const width = Number(attribs.width) || 800; + const height = Number(attribs.height) || 600; + attribs.src = mediaFromMxc(attribs.src).getThumbnailOfSourceHttp(width, height); return { tagName, attribs }; }, 'code': function(tagName: string, attribs: sanitizeHtml.Attributes) { diff --git a/src/Notifier.ts b/src/Notifier.ts index 6460be20ad..f68bfabc18 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -36,6 +36,7 @@ import {SettingLevel} from "./settings/SettingLevel"; import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers"; import RoomViewStore from "./stores/RoomViewStore"; import UserActivity from "./UserActivity"; +import {mediaFromMxc} from "./customisations/Media"; /* * Dispatches: @@ -150,7 +151,7 @@ export const Notifier = { // Ideally in here we could use MSC1310 to detect the type of file, and reject it. return { - url: MatrixClientPeg.get().mxcUrlToHttp(content.url), + url: mediaFromMxc(content.url).srcHttp, name: content.name, type: content.type, size: content.size, diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index ebf5d536ec..b7a4e0960e 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -27,6 +27,7 @@ import {sortBy} from "lodash"; import {makeGroupPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; +import {mediaFromMxc} from "../customisations/Media"; const COMMUNITY_REGEX = /\B\+\S*/g; @@ -95,7 +96,7 @@ export default class CommunityProvider extends AutocompleteProvider { name={name || groupId} width={24} height={24} - url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} /> + url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} /> ), range, diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index b4b871a0b4..f05d8d0758 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -39,6 +39,7 @@ import {Group} from "matrix-js-sdk"; import {allSettled, sleep} from "../../utils/promise"; import RightPanelStore from "../../stores/RightPanelStore"; import AutoHideScrollbar from "./AutoHideScrollbar"; +import {mediaFromMxc} from "../../customisations/Media"; import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( @@ -368,8 +369,7 @@ class FeaturedUser extends React.Component { const permalink = makeUserPermalink(this.props.summaryInfo.user_id); const userNameNode = { name }; - const httpUrl = MatrixClientPeg.get() - .mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64); + const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64); const deleteButton = this.props.editing ?
    ; } - const httpInviterAvatar = this.state.inviterProfile ? - this._matrixClient.mxcUrlToHttp( - this.state.inviterProfile.avatarUrl, 36, 36, - ) : null; + const httpInviterAvatar = this.state.inviterProfile + ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36) + : null; const inviter = group.inviter || {}; let inviterName = inviter.userId; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 88c7a71b35..f7865d094a 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -41,6 +41,7 @@ import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; import SpacePanel from "../views/spaces/SpacePanel"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../customisations/Media"; interface IProps { isMinimized: boolean; @@ -121,7 +122,7 @@ export default class LeftPanel extends React.Component { let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage"); if (settingBgMxc) { - avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize); + avatarUrl = mediaFromMxc(settingBgMxc).getSquareThumbnailHttp(avatarSize); } const avatarUrlProp = `url(${avatarUrl})`; diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 72e52678b6..9ee16558d3 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -34,6 +34,7 @@ import {EnhancedMap} from "../../utils/maps"; import StyledCheckbox from "../views/elements/StyledCheckbox"; import AutoHideScrollbar from "./AutoHideScrollbar"; import BaseAvatar from "../views/avatars/BaseAvatar"; +import {mediaFromMxc} from "../../customisations/Media"; interface IProps { space: Room; @@ -158,12 +159,7 @@ const SubSpace: React.FC = ({ let url: string; if (space.avatar_url) { - url = MatrixClientPeg.get().mxcUrlToHttp( - space.avatar_url, - Math.floor(24 * window.devicePixelRatio), - Math.floor(24 * window.devicePixelRatio), - "crop", - ); + url = mediaFromMxc(space.avatar_url).getSquareThumbnailHttp(Math.floor(24 * window.devicePixelRatio)); } return
    @@ -265,12 +261,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli let url: string; if (room.avatar_url) { - url = cli.mxcUrlToHttp( - room.avatar_url, - Math.floor(32 * window.devicePixelRatio), - Math.floor(32 * window.devicePixelRatio), - "crop", - ); + url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(32 * window.devicePixelRatio)); } const content = diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index a033257871..dc363da304 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd +Copyright 2017, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import React from 'react'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import BaseAvatar from './BaseAvatar'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; +import {ResizeMode} from "../../../customisations/models/ResizeMode"; export interface IProps { groupId?: string; @@ -25,7 +27,7 @@ export interface IProps { groupAvatarUrl?: string; width?: number; height?: number; - resizeMethod?: string; + resizeMethod?: ResizeMode; onClick?: React.MouseEventHandler; } @@ -38,8 +40,7 @@ export default class GroupAvatar extends React.Component { }; getGroupAvatarUrl() { - return MatrixClientPeg.get().mxcUrlToHttp( - this.props.groupAvatarUrl, + return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp( this.props.width, this.props.height, this.props.resizeMethod, diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 8827f161f1..8cfd28986b 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -21,6 +21,7 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; /* * A dialog for confirming an operation on another user. @@ -108,8 +109,9 @@ export default class ConfirmUserActionDialog extends React.Component { name = this.props.member.name; userId = this.props.member.userId; } else { - const httpAvatarUrl = this.props.groupMember.avatarUrl ? - this.props.matrixClient.mxcUrlToHttp(this.props.groupMember.avatarUrl, 48, 48) : null; + const httpAvatarUrl = this.props.groupMember.avatarUrl + ? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48) + : null; name = this.props.groupMember.displayname || this.props.groupMember.userId; userId = this.props.groupMember.userId; avatar = ; diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx index 504d563bd9..ee3696b427 100644 --- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -24,6 +24,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import FlairStore from "../../../stores/FlairStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends IDialogProps { communityId: string; @@ -118,7 +119,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent; if (!this.state.avatarPreview) { if (this.state.currentAvatarUrl) { - const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl); + const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp; preview = ; } else { preview =
    diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index d65ec7563f..f18b7a9d0c 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; const PHASE_START = 0; const PHASE_SHOW_SAS = 1; @@ -123,22 +124,21 @@ export default class IncomingSasDialog extends React.Component { const Spinner = sdk.getComponent("views.elements.Spinner"); const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); - const isSelf = this.props.verifier.userId == MatrixClientPeg.get().getUserId(); + const isSelf = this.props.verifier.userId === MatrixClientPeg.get().getUserId(); let profile; - if (this.state.opponentProfile) { + const oppProfile = this.state.opponentProfile; + if (oppProfile) { + const url = oppProfile.avatar_url + ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio)) + : null; profile =
    - -

    {this.state.opponentProfile.displayname}

    +

    {oppProfile.displayname}

    ; } else if (this.state.opponentProfileError) { profile =
    diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 4a216dbae4..4f5ee45a3c 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -23,6 +23,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import { UserAddressType } from '../../../UserAddress.js'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.elements.AddressTile") export default class AddressTile extends React.Component { @@ -47,9 +48,7 @@ export default class AddressTile extends React.Component { const isMatrixAddress = ['mx-user-id', 'mx-room-id'].includes(address.addressType); if (isMatrixAddress && address.avatarMxc) { - imgUrls.push(MatrixClientPeg.get().mxcUrlToHttp( - address.avatarMxc, 25, 25, 'crop', - )); + imgUrls.push(mediaFromMxc(address.avatarMxc).getSquareThumbnailHttp(25)); } else if (address.addressType === 'email') { imgUrls.push(require("../../../../res/img/icon-email-user.svg")); } diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 75998cb721..73d5b91511 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -20,6 +20,7 @@ import FlairStore from '../../../stores/FlairStore'; import dis from '../../../dispatcher/dispatcher'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; class FlairAvatar extends React.Component { @@ -39,8 +40,7 @@ class FlairAvatar extends React.Component { } render() { - const httpUrl = this.context.mxcUrlToHttp( - this.props.groupProfile.avatarUrl, 16, 16, 'scale', false); + const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16); const tooltip = this.props.groupProfile.name ? `${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`: this.props.groupProfile.groupId; diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index b0d4fc7fa2..bf99ee6078 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -26,6 +26,7 @@ import FlairStore from "../../../stores/FlairStore"; import {getPrimaryPermalinkEntity, parseAppLocalLink} from "../../../utils/permalinks/Permalinks"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {Action} from "../../../dispatcher/actions"; +import {mediaFromMxc} from "../../../customisations/Media"; import Tooltip from './Tooltip'; import {replaceableComponent} from "../../../utils/replaceableComponent"; @@ -259,7 +260,7 @@ class Pill extends React.Component { linkText = groupId; if (this.props.shouldShowPillAvatar) { avatar =
    ); + const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800); + avatarElement =
    ; } const groupRoomName = this.state.groupRoom.displayname; diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index 8b25437f71..7edfc1a376 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -21,6 +21,7 @@ import dis from '../../../dispatcher/dispatcher'; import { GroupRoomType } from '../../../groups'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.groups.GroupRoomTile") class GroupRoomTile extends React.Component { @@ -42,10 +43,9 @@ class GroupRoomTile extends React.Component { render() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const avatarUrl = this.context.mxcUrlToHttp( - this.props.groupRoom.avatarUrl, - 36, 36, 'crop', - ); + const avatarUrl = this.props.groupRoom.avatarUrl + ? mediaFromMxc(this.props.groupRoom.avatarUrl).getSquareThumbnailHttp(36) + : null; const av = ( { profile.shortDescription }
    :
    ; - const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp( - profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null; + const httpUrl = profile.avatarUrl + ? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight) + : null; let avatarElement = (
    diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js index 498e2db12a..78ded9a514 100644 --- a/src/components/views/messages/MAudioBody.js +++ b/src/components/views/messages/MAudioBody.js @@ -22,6 +22,7 @@ import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; @replaceableComponent("views.messages.MAudioBody") export default class MAudioBody extends React.Component { @@ -41,11 +42,11 @@ export default class MAudioBody extends React.Component { } _getContentUrl() { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(this.props.mxEvent.getContent()); + if (media.isEncrypted) { return this.state.decryptedUrl; } else { - return MatrixClientPeg.get().mxcUrlToHttp(content.url); + return media.srcHttp; } } diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index e9893f99b6..07d7beb793 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -27,6 +27,7 @@ import request from 'browser-request'; import Modal from '../../../Modal'; import AccessibleButton from "../elements/AccessibleButton"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; // A cached tinted copy of require("../../../../res/img/download.svg") @@ -178,8 +179,8 @@ export default class MFileBody extends React.Component { } _getContentUrl() { - const content = this.props.mxEvent.getContent(); - return MatrixClientPeg.get().mxcUrlToHttp(content.url); + const media = mediaFromContent(this.props.mxEvent.getContent()); + return media.srcHttp; } componentDidMount() { diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 59c5b4e66b..0a1f875935 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -28,6 +28,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; @replaceableComponent("views.messages.MImageBody") export default class MImageBody extends React.Component { @@ -167,16 +168,16 @@ export default class MImageBody extends React.Component { } _getContentUrl() { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(this.props.mxEvent.getContent()); + if (media.isEncrypted) { return this.state.decryptedUrl; } else { - return this.context.mxcUrlToHttp(content.url); + return media.srcHttp; } } _getThumbUrl() { - // FIXME: the dharma skin lets images grow as wide as you like, rather than capped to 800x600. + // FIXME: we let images grow as wide as you like, rather than capped to 800x600. // So either we need to support custom timeline widths here, or reimpose the cap, otherwise the // thumbnail resolution will be unnecessarily reduced. // custom timeline widths seems preferable. @@ -185,21 +186,19 @@ export default class MImageBody extends React.Component { const thumbHeight = Math.round(600 * pixelRatio); const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(content); + + if (media.isEncrypted) { // Don't use the thumbnail for clients wishing to autoplay gifs. if (this.state.decryptedThumbnailUrl) { return this.state.decryptedThumbnailUrl; } return this.state.decryptedUrl; - } else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) { + } else if (content.info && content.info.mimetype === "image/svg+xml" && media.hasThumbnail) { // special case to return clientside sender-generated thumbnails for SVGs, if any, // given we deliberately don't thumbnail them serverside to prevent // billion lol attacks and similar - return this.context.mxcUrlToHttp( - content.info.thumbnail_url, - thumbWidth, - thumbHeight, - ); + return media.getThumbnailHttp(thumbWidth, thumbHeight, 'scale'); } else { // we try to download the correct resolution // for hi-res images (like retina screenshots). @@ -218,7 +217,7 @@ export default class MImageBody extends React.Component { pixelRatio === 1.0 || (!info || !info.w || !info.h || !info.size) ) { - return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight); + return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight); } else { // we should only request thumbnails if the image is bigger than 800x600 // (or 1600x1200 on retina) otherwise the image in the timeline will just @@ -233,24 +232,17 @@ export default class MImageBody extends React.Component { info.w > thumbWidth || info.h > thumbHeight ); - const isLargeFileSize = info.size > 1*1024*1024; + const isLargeFileSize = info.size > 1*1024*1024; // 1mb if (isLargeFileSize && isLargerThanThumbnail) { // image is too large physically and bytewise to clutter our timeline so // we ask for a thumbnail, despite knowing that it will be max 800x600 // despite us being retina (as synapse doesn't do 1600x1200 thumbs yet). - return this.context.mxcUrlToHttp( - content.url, - thumbWidth, - thumbHeight, - ); + return media.getThumbnailOfSourceHttp(thumbWidth, thumbHeight); } else { // download the original image otherwise, so we can scale it client side // to take pixelRatio into account. - // ( no width/height means we want the original image) - return this.context.mxcUrlToHttp( - content.url, - ); + return media.srcHttp; } } } diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 89985dee7d..32b071ea24 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import InlineSpinner from '../elements/InlineSpinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromContent} from "../../../customisations/Media"; interface IProps { /* the MatrixEvent to show */ @@ -76,11 +77,11 @@ export default class MVideoBody extends React.PureComponent { } private getContentUrl(): string|null { - const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(this.props.mxEvent.getContent()); + if (media.isEncrypted) { return this.state.decryptedUrl; } else { - return MatrixClientPeg.get().mxcUrlToHttp(content.url); + return media.srcHttp; } } @@ -91,10 +92,11 @@ export default class MVideoBody extends React.PureComponent { private getThumbUrl(): string|null { const content = this.props.mxEvent.getContent(); - if (content.file !== undefined) { + const media = mediaFromContent(content); + if (media.isEncrypted) { return this.state.decryptedThumbnailUrl; - } else if (content.info && content.info.thumbnail_url) { - return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url); + } else if (media.hasThumbnail) { + return media.thumbnailHttp; } else { return null; } diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index ba860216f0..00aaf9bfda 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -24,6 +24,7 @@ import * as sdk from '../../../index'; import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.messages.RoomAvatarEvent") export default class RoomAvatarEvent extends React.Component { @@ -35,7 +36,7 @@ export default class RoomAvatarEvent extends React.Component { onAvatarClick = () => { const cli = MatrixClientPeg.get(); const ev = this.props.mxEvent; - const httpUrl = cli.mxcUrlToHttp(ev.getContent().url); + const httpUrl = mediaFromMxc(ev.getContent().url).srcHttp; const room = cli.getRoom(this.props.mxEvent.getRoomId()); const text = _t('%(senderDisplayName)s changed the avatar for %(roomName)s', { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index eb47a56269..d415d19852 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -63,6 +63,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IDevice { deviceId: string; @@ -1408,7 +1409,7 @@ const UserInfoHeader: React.FC<{ const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl; if (!avatarUrl) return; - const httpUrl = cli.mxcUrlToHttp(avatarUrl); + const httpUrl = mediaFromMxc(avatarUrl).srcHttp; const params = { src: httpUrl, name: member.name, diff --git a/src/components/views/room_settings/RoomProfileSettings.js b/src/components/views/room_settings/RoomProfileSettings.js index 563368384b..3dbe2b2b7f 100644 --- a/src/components/views/room_settings/RoomProfileSettings.js +++ b/src/components/views/room_settings/RoomProfileSettings.js @@ -21,6 +21,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import Field from "../elements/Field"; import * as sdk from "../../../index"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; // TODO: Merge with ProfileSettings? @replaceableComponent("views.room_settings.RoomProfileSettings") @@ -38,7 +39,7 @@ export default class RoomProfileSettings extends React.Component { const avatarEvent = room.currentState.getStateEvents("m.room.avatar", ""); let avatarUrl = avatarEvent && avatarEvent.getContent() ? avatarEvent.getContent()["url"] : null; - if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false); + if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); const topicEvent = room.currentState.getStateEvents("m.room.topic", ""); const topic = topicEvent && topicEvent.getContent() ? topicEvent.getContent()['topic'] : ''; @@ -112,7 +113,7 @@ export default class RoomProfileSettings extends React.Component { if (this.state.avatarFile) { const uri = await client.uploadContent(this.state.avatarFile); await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: uri}, ''); - newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false); + newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96); newState.originalAvatarUrl = newState.avatarUrl; newState.avatarFile = null; } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { diff --git a/src/components/views/rooms/LinkPreviewWidget.js b/src/components/views/rooms/LinkPreviewWidget.js index 39c9f0bcf7..536abf57fc 100644 --- a/src/components/views/rooms/LinkPreviewWidget.js +++ b/src/components/views/rooms/LinkPreviewWidget.js @@ -26,6 +26,7 @@ import Modal from "../../../Modal"; import * as ImageUtils from "../../../ImageUtils"; import { _t } from "../../../languageHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.rooms.LinkPreviewWidget") export default class LinkPreviewWidget extends React.Component { @@ -83,7 +84,7 @@ export default class LinkPreviewWidget extends React.Component { let src = p["og:image"]; if (src && src.startsWith("mxc://")) { - src = MatrixClientPeg.get().mxcUrlToHttp(src); + src = mediaFromMxc(src).srcHttp; } const params = { @@ -109,9 +110,11 @@ export default class LinkPreviewWidget extends React.Component { if (!SettingsStore.getValue("showImages")) { image = null; // Don't render a button to show the image, just hide it outright } - const imageMaxWidth = 100; const imageMaxHeight = 100; + const imageMaxWidth = 100; + const imageMaxHeight = 100; if (image && image.startsWith("mxc://")) { - image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight); + // We deliberately don't want a square here, so use the source HTTP thumbnail function + image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, 'scale'); } let thumbHeight = imageMaxHeight; diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 8067046ffd..0b6739df64 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -21,6 +21,7 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Spinner from '../elements/Spinner'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.settings.ChangeAvatar") export default class ChangeAvatar extends React.Component { @@ -117,7 +118,7 @@ export default class ChangeAvatar extends React.Component { httpPromise.then(function() { self.setState({ phase: ChangeAvatar.Phases.Display, - avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl), + avatarUrl: mediaFromMxc(newUrl).srcHttp, }); }, function(error) { self.setState({ diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js index 30dcdc3c47..971b868751 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.js @@ -24,6 +24,7 @@ import {OwnProfileStore} from "../../../stores/OwnProfileStore"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; @replaceableComponent("views.settings.ProfileSettings") export default class ProfileSettings extends React.Component { @@ -32,7 +33,7 @@ export default class ProfileSettings extends React.Component { const client = MatrixClientPeg.get(); let avatarUrl = OwnProfileStore.instance.avatarMxc; - if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false); + if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); this.state = { userId: client.getUserId(), originalDisplayName: OwnProfileStore.instance.displayName, @@ -97,7 +98,7 @@ export default class ProfileSettings extends React.Component { ` (${this.state.avatarFile.size}) bytes`); const uri = await client.uploadContent(this.state.avatarFile); await client.setAvatarUrl(uri); - newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false); + newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96); newState.originalAvatarUrl = newState.avatarUrl; newState.avatarFile = null; } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index 27abc6bc50..f42307c530 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -16,6 +16,7 @@ import {MatrixClientPeg} from "../MatrixClientPeg"; import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; +import {ResizeMode} from "./models/ResizeMode"; // Populate this class with the details of your customisations when copying it. @@ -33,6 +34,13 @@ export class Media { constructor(private prepared: IPreparedMedia) { } + /** + * True if the media appears to be encrypted. Actual file contents may vary. + */ + public get isEncrypted(): boolean { + return !!this.prepared.file; + } + /** * The MXC URI of the source media. */ @@ -62,6 +70,15 @@ export class Media { return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc); } + /** + * The HTTP URL for the thumbnail media (without any specified width, height, etc). Null/undefined + * if no thumbnail media recorded. + */ + public get thumbnailHttp(): string | undefined | null { + if (!this.hasThumbnail) return null; + return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc); + } + /** * Gets the HTTP URL for the thumbnail media with the requested characteristics, if a thumbnail * is recorded for this media. Returns null/undefined otherwise. @@ -70,7 +87,7 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {string} The HTTP URL which points to the thumbnail. */ - public getThumbnailHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string | null | undefined { + public getThumbnailHttp(width: number, height: number, mode: ResizeMode = "scale"): string | null | undefined { if (!this.hasThumbnail) return null; return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode); } @@ -82,10 +99,23 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {string} The HTTP URL which points to the thumbnail. */ - public getThumbnailOfSourceHttp(width: number, height: number, mode: 'scale' | 'crop' = "scale"): string { + public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMode = "scale"): string { return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode); } + /** + * Creates a square thumbnail of the media. If the media has a thumbnail recorded, that MXC will + * be used, otherwise the source media will be used. + * @param {number} dim The desired width and height. + * @returns {string} An HTTP URL for the thumbnail. + */ + public getSquareThumbnailHttp(dim: number): string { + if (this.hasThumbnail) { + return this.getThumbnailHttp(dim, dim, 'crop'); + } + return this.getThumbnailOfSourceHttp(dim, dim, 'crop'); + } + /** * Downloads the source media. * @returns {Promise} Resolves to the server's response for chaining. @@ -102,7 +132,7 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {Promise} Resolves to the server's response for chaining. */ - public downloadThumbnail(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + public downloadThumbnail(width: number, height: number, mode: ResizeMode = "scale"): Promise { if (!this.hasThumbnail) throw new Error("Cannot download non-existent thumbnail"); return fetch(this.getThumbnailHttp(width, height, mode)); } @@ -114,7 +144,7 @@ export class Media { * @param {"scale"|"crop"} mode The desired thumbnailing mode. Defaults to scale. * @returns {Promise} Resolves to the server's response for chaining. */ - public downloadThumbnailOfSource(width: number, height: number, mode: 'scale' | 'crop' = "scale"): Promise { + public downloadThumbnailOfSource(width: number, height: number, mode: ResizeMode = "scale"): Promise { return fetch(this.getThumbnailOfSourceHttp(width, height, mode)); } } diff --git a/src/customisations/models/ResizeMode.ts b/src/customisations/models/ResizeMode.ts new file mode 100644 index 0000000000..401b6723e5 --- /dev/null +++ b/src/customisations/models/ResizeMode.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type ResizeMode = "scale" | "crop"; diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts index 8983380fec..5e722877e2 100644 --- a/src/stores/OwnProfileStore.ts +++ b/src/stores/OwnProfileStore.ts @@ -22,6 +22,7 @@ import { User } from "matrix-js-sdk/src/models/user"; import { throttle } from "lodash"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from "../languageHandler"; +import {mediaFromMxc} from "../customisations/Media"; interface IState { displayName?: string; @@ -72,8 +73,12 @@ export class OwnProfileStore extends AsyncStoreWithClient { */ public getHttpAvatarUrl(size = 0): string { if (!this.avatarMxc) return null; - const adjustedSize = size > 1 ? size : undefined; // don't let negatives or zero through - return this.matrixClient.mxcUrlToHttp(this.avatarMxc, adjustedSize, adjustedSize); + const media = mediaFromMxc(this.avatarMxc); + if (!size || size <= 0) { + return media.srcHttp; + } else { + return media.getSquareThumbnailHttp(size); + } } protected async onNotReady() { From fa5d98c319c5cddea8fe94c0cc70215e7909401f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 5 Mar 2021 18:45:09 -0700 Subject: [PATCH 022/183] Convert cases of getHttpUriForMxc to new media customisation --- src/Avatar.ts | 10 +++------- src/components/structures/RoomDirectory.js | 9 ++++----- src/components/views/avatars/RoomAvatar.tsx | 16 ++++++++-------- src/components/views/avatars/WidgetAvatar.tsx | 4 ++-- .../dialogs/CommunityPrototypeInviteDialog.tsx | 10 ++++++---- src/components/views/dialogs/InviteDialog.tsx | 14 +++++++------- src/components/views/rooms/RoomDetailRow.js | 10 +++++----- src/components/views/settings/BridgeTile.tsx | 8 ++------ 8 files changed, 37 insertions(+), 44 deletions(-) diff --git a/src/Avatar.ts b/src/Avatar.ts index e2557e21a8..eeef3e2c69 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {RoomMember} from "matrix-js-sdk/src/models/room-member"; import {User} from "matrix-js-sdk/src/models/user"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClientPeg} from './MatrixClientPeg'; import DMRoomMap from './utils/DMRoomMap'; +import {mediaFromMxc} from "./customisations/Media"; export type ResizeMethod = "crop" | "scale"; @@ -47,16 +47,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu } export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { - const url = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, + if (!user.avatarUrl) return null; + return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp( Math.floor(width * window.devicePixelRatio), Math.floor(height * window.devicePixelRatio), resizeMethod, ); - if (!url || url.length === 0) { - return null; - } - return url; } function isValidHexColor(color: string): boolean { diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 363c67262b..3613261da6 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -27,7 +27,6 @@ import { _t } from '../../languageHandler'; import SdkConfig from '../../SdkConfig'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {ALL_ROOMS} from "../views/directory/NetworkDropdown"; import SettingsStore from "../../settings/SettingsStore"; import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore"; @@ -35,6 +34,7 @@ import GroupStore from "../../stores/GroupStore"; import FlairStore from "../../stores/FlairStore"; import CountlyAnalytics from "../../CountlyAnalytics"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../customisations/Media"; const MAX_NAME_LENGTH = 80; const MAX_TOPIC_LENGTH = 800; @@ -521,10 +521,9 @@ export default class RoomDirectory extends React.Component { topic = `${topic.substring(0, MAX_TOPIC_LENGTH)}...`; } topic = linkifyAndSanitizeHtml(topic); - const avatarUrl = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - room.avatar_url, 32, 32, "crop", - ); + let avatarUrl = null; + if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32); + return [
    this.onRoomClicked(room, ev)} diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 0a59f6e36a..31245b44b7 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React, {ComponentProps} from 'react'; import Room from 'matrix-js-sdk/src/models/room'; -import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo'; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; @@ -24,6 +23,7 @@ import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; import {ResizeMethod} from "../../../Avatar"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, @@ -90,16 +90,16 @@ export default class RoomAvatar extends React.Component { }; private static getImageUrls(props: IProps): string[] { - return [ - getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - // Default props don't play nicely with getDerivedStateFromProps - //props.oobData !== undefined ? props.oobData.avatarUrl : {}, - props.oobData.avatarUrl, + 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.resizeMethod, - ), // highest priority + ); + } + return [ + oobAvatar, // highest priority RoomAvatar.getRoomAvatarUrl(props), ].filter(function(url) { return (url !== null && url !== ""); diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx index 04cfce7670..6468b0dd49 100644 --- a/src/components/views/avatars/WidgetAvatar.tsx +++ b/src/components/views/avatars/WidgetAvatar.tsx @@ -16,11 +16,11 @@ limitations under the License. import React, {ComponentProps, useContext} from 'react'; import classNames from 'classnames'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {IApp} from "../../../stores/WidgetStore"; import BaseAvatar, {BaseAvatarType} from "./BaseAvatar"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends Omit, "name" | "url" | "urls"> { app: IApp; @@ -47,7 +47,7 @@ const WidgetAvatar: React.FC = ({ app, className, width = 20, height = 2 name={app.id} className={classNames("mx_WidgetAvatar", className)} // MSC2765 - url={app.avatar_url ? getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop") : undefined} + url={app.avatar_url ? mediaFromMxc(app.avatar_url).getSquareThumbnailHttp(20) : undefined} urls={iconUrls} width={width} height={height} diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx index d1080566ac..2635f95bb7 100644 --- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx +++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx @@ -26,12 +26,12 @@ import SdkConfig from "../../../SdkConfig"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import InviteDialog from "./InviteDialog"; import BaseAvatar from "../avatars/BaseAvatar"; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite"; import StyledCheckbox from "../elements/StyledCheckbox"; import Modal from "../../../Modal"; import ErrorDialog from "./ErrorDialog"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps extends IDialogProps { roomId: string; @@ -142,12 +142,14 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< private renderPerson(person: IPerson, key: any) { const avatarSize = 36; + let avatarUrl = null; + if (person.user.getMxcAvatarUrl()) { + avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize); + } return (
    { width={avatarSize} height={avatarSize} /> : { src={require("../../../../res/img/icon-email-pill-avatar.svg")} width={avatarSize} height={avatarSize} /> : ) :
    ; + let avatarUrl = null; + if (room.avatarUrl) avatarUrl = mediaFromMxc(room.avatarUrl).getSquareThumbnailHttp(24); + return + url={avatarUrl} />
    { name }
      diff --git a/src/components/views/settings/BridgeTile.tsx b/src/components/views/settings/BridgeTile.tsx index b33219ad4a..3565d1ba2e 100644 --- a/src/components/views/settings/BridgeTile.tsx +++ b/src/components/views/settings/BridgeTile.tsx @@ -16,9 +16,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {_t} from "../../../languageHandler"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; import Pill from "../elements/Pill"; import {makeUserPermalink} from "../../../utils/permalinks/Permalinks"; import BaseAvatar from "../avatars/BaseAvatar"; @@ -27,6 +25,7 @@ import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { isUrlPermitted } from '../../../HtmlUtils'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import {mediaFromMxc} from "../../../customisations/Media"; interface IProps { ev: MatrixEvent; @@ -114,10 +113,7 @@ export default class BridgeTile extends React.PureComponent { let networkIcon; if (protocol.avatar_url) { - const avatarUrl = getHttpUriForMxc( - MatrixClientPeg.get().getHomeserverUrl(), - protocol.avatar_url, 64, 64, "crop", - ); + const avatarUrl = mediaFromMxc(protocol.avatar_url).getSquareThumbnailHttp(64); networkIcon = Date: Fri, 5 Mar 2021 18:49:32 -0700 Subject: [PATCH 023/183] Appease the linter --- src/HtmlUtils.tsx | 1 - src/components/structures/LeftPanel.tsx | 1 - src/components/views/avatars/GroupAvatar.tsx | 1 - src/components/views/avatars/WidgetAvatar.tsx | 5 +---- src/components/views/elements/AddressTile.js | 1 - src/components/views/elements/Pill.js | 10 ++++------ src/components/views/messages/MAudioBody.js | 1 - src/components/views/messages/MFileBody.js | 1 - src/components/views/messages/MVideoBody.tsx | 1 - src/components/views/right_panel/UserInfo.tsx | 2 +- src/customisations/models/IMediaEventContent.ts | 6 +++--- test/components/structures/GroupView-test.js | 3 ++- 12 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 12752eb20f..59b596a5da 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -32,7 +32,6 @@ import { AllHtmlEntities } from 'html-entities'; import SettingsStore from './settings/SettingsStore'; import cheerio from 'cheerio'; -import {MatrixClientPeg} from './MatrixClientPeg'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index f7865d094a..9a1ce63785 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -36,7 +36,6 @@ import {Key} from "../../Keyboard"; import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; -import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; import LeftPanelWidget from "./LeftPanelWidget"; import SpacePanel from "../views/spaces/SpacePanel"; diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index dc363da304..321ca025a3 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; import BaseAvatar from './BaseAvatar'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {mediaFromMxc} from "../../../customisations/Media"; diff --git a/src/components/views/avatars/WidgetAvatar.tsx b/src/components/views/avatars/WidgetAvatar.tsx index 6468b0dd49..cca158269e 100644 --- a/src/components/views/avatars/WidgetAvatar.tsx +++ b/src/components/views/avatars/WidgetAvatar.tsx @@ -14,10 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ComponentProps, useContext} from 'react'; +import React, {ComponentProps} from 'react'; import classNames from 'classnames'; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {IApp} from "../../../stores/WidgetStore"; import BaseAvatar, {BaseAvatarType} from "./BaseAvatar"; import {mediaFromMxc} from "../../../customisations/Media"; @@ -27,8 +26,6 @@ interface IProps extends Omit, "name" | "url" | " } const WidgetAvatar: React.FC = ({ app, className, width = 20, height = 20, ...props }) => { - const cli = useContext(MatrixClientContext); - let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")]; // heuristics for some better icons until Widgets support their own icons if (app.type.includes("jitsi")) { diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index 4f5ee45a3c..df66d10a71 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -19,7 +19,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import * as sdk from "../../../index"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import { UserAddressType } from '../../../UserAddress.js'; import {replaceableComponent} from "../../../utils/replaceableComponent"; diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index bf99ee6078..e61d312305 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -1,7 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd -Copyright 2019, 2021 The Matrix.org Foundation C.I.C. +Copyright 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. @@ -255,12 +253,12 @@ class Pill extends React.Component { case Pill.TYPE_GROUP_MENTION: { if (this.state.group) { const {avatarUrl, groupId, name} = this.state.group; - const cli = MatrixClientPeg.get(); linkText = groupId; if (this.props.shouldShowPillAvatar) { - avatar =
    } - return - { content } - ; + let childToggle; + let childSection; + if (children) { + // the chevron is purposefully a div rather than a button as it should be ignored for a11y + childToggle =
    { + ev.stopPropagation(); + toggleShowChildren(); + }} + />; + if (showChildren) { + childSection =
    + { children } +
    ; + } + } + + return <> + + { content } + { childToggle } + + { childSection } + ; }; export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => { @@ -325,88 +240,77 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi interface IHierarchyLevelProps { spaceId: string; rooms: Map; - editing?: boolean; - relations: EnhancedMap; + relations: EnhancedMap>; parents: Set; - queueAction?(action: IAction): void; - onPreviewClick(roomId: string): void; - onRemoveFromSpaceClick?(roomId: string): void; - onJoinClick?(roomId: string): void; + selectedMap?: Map>; + onViewRoomClick(roomId: string, autoJoin: boolean): void; + onToggleClick?(parentId: string, childId: string): void; } export const HierarchyLevel = ({ spaceId, rooms, - editing, relations, parents, - onPreviewClick, - onJoinClick, - queueAction, + selectedMap, + onViewRoomClick, + onToggleClick, }: IHierarchyLevelProps) => { const cli = MatrixClientPeg.get(); const space = cli.getRoom(spaceId); - // TODO respect order - const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => { - if (!rooms.has(roomId)) return result; // TODO wat + const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()) + + const sortedChildren = sortBy([...relations.get(spaceId)?.values()], ev => ev.content.order || null); + const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { + const roomId = ev.state_key; + if (!rooms.has(roomId)) return result; result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId); return result; }, [[], []]) || [[], []]; - // Don't render this subspace if it has no rooms we can show - // TODO this is broken - as a space may have subspaces we still need to show - // if (!childRooms.length) return null; - - const userId = cli.getUserId(); - const newParents = new Set(parents).add(spaceId); return { childRooms.map(roomId => ( - { - onPreviewClick(roomId); + suggested={relations.get(spaceId)?.get(roomId)?.content.suggested} + selected={selectedMap?.get(spaceId)?.has(roomId)} + onViewRoomClick={(autoJoin) => { + onViewRoomClick(roomId, autoJoin); }} - onJoinClick={onJoinClick ? () => { - onJoinClick(roomId); - } : undefined} + hasPermissions={hasPermissions} + onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined} /> )) } { subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => ( - { - onPreviewClick(roomId); - }} - onJoinClick={() => { - onJoinClick(roomId); + room={rooms.get(roomId)} + numChildRooms={Array.from(relations.get(roomId)?.values() || []) + .filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length} + suggested={relations.get(spaceId)?.get(roomId)?.content.suggested} + selected={selectedMap?.get(spaceId)?.has(roomId)} + onViewRoomClick={(autoJoin) => { + onViewRoomClick(roomId, autoJoin); }} + hasPermissions={hasPermissions} + onToggleClick={onToggleClick ? () => onToggleClick(spaceId, roomId) : undefined} > - + )) } @@ -415,8 +319,8 @@ export const HierarchyLevel = ({ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinished }) => { // TODO pagination const cli = MatrixClientPeg.get(); + const userId = cli.getUserId(); const [query, setQuery] = useState(initialText); - const [isEditing, setIsEditing] = useState(false); const onCreateRoomClick = () => { dis.dispatch({ @@ -426,51 +330,19 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis onFinished(); }; - // stored within a ref as we don't need to re-render when it changes - const pendingActions = useRef(new Map()); + const [selected, setSelected] = useState(new Map>()); // Map> - let adminButton; - if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test - const onManageButtonClicked = () => { - setIsEditing(true); - }; - - const onSaveButtonClicked = () => { - // TODO setBusy - pendingActions.current.forEach(({event, suggested, removed}) => { - const content = { - ...event.getContent(), - suggested, - }; - - if (removed) { - delete content["via"]; - } - - cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey()); - }); - setIsEditing(false); - }; - - if (isEditing) { - adminButton = - - { _t("Promoted to users") } - ; - } else { - adminButton = ; - } - } - - const [rooms, relations, viaMap] = useAsyncMemo(async () => { + const [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => { try { const data = await cli.getSpaceSummary(space.roomId); - const parentChildRelations = new EnhancedMap(); + const parentChildRelations = new EnhancedMap>(); + const childParentRelations = new EnhancedMap>(); const viaMap = new EnhancedMap>(); data.events.map((ev: ISpaceSummaryEvent) => { if (ev.type === EventType.SpaceChild) { - parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key); + parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); + childParentRelations.getOrCreate(ev.state_key, new Set()).add(ev.room_id); } if (Array.isArray(ev.content["via"])) { const set = viaMap.getOrCreate(ev.state_key, new Set()); @@ -478,7 +350,7 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis } }); - return [data.rooms, parentChildRelations, viaMap]; + return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap]; } catch (e) { console.error(e); // TODO } @@ -488,54 +360,204 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis const roomsMap = useMemo(() => { if (!rooms) return null; - const lcQuery = query.toLowerCase(); + const lcQuery = query.toLowerCase().trim(); - const filteredRooms = rooms.filter(r => { - return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms - || r.name?.toLowerCase().includes(lcQuery) - || r.topic?.toLowerCase().includes(lcQuery); + const roomsMap = new Map(rooms.map(r => [r.room_id, r])); + if (!lcQuery) return roomsMap; + + const directMatches = rooms.filter(r => { + return r.name?.toLowerCase().includes(lcQuery) || r.topic?.toLowerCase().includes(lcQuery); }); - return new Map(filteredRooms.map(r => [r.room_id, r])); - // const root = rooms.get(space.roomId); - }, [rooms, query]); + // Walk back up the tree to find all parents of the direct matches to show their place in the hierarchy + const visited = new Set(); + const queue = [...directMatches.map(r => r.room_id)]; + while (queue.length) { + const roomId = queue.pop(); + visited.add(roomId); + childParentMap.get(roomId)?.forEach(parentId => { + if (!visited.has(parentId)) { + queue.push(parentId); + } + }); + } + + // Remove any mappings for rooms which were not visited in the walk + Array.from(roomsMap.keys()).forEach(roomId => { + if (!visited.has(roomId)) { + roomsMap.delete(roomId); + } + }); + return roomsMap; + }, [rooms, childParentMap, query]); const title = - +

    { _t("Explore rooms") }

    ; + const explanation = - _t("If you can't find the room you're looking for, ask for an invite or Create a new room.", null, + _t("If you can't find the room you're looking for, ask for an invite or create a new room.", null, {a: sub => { return {sub}; }}, ); + const [error, setError] = useState(""); + const [removing, setRemoving] = useState(false); + const [saving, setSaving] = useState(false); + let content; if (roomsMap) { - content = - { - pendingActions.current.set(action.event.room_id, action); - }} - onPreviewClick={roomId => { - showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false); - onFinished(); - }} - onJoinClick={(roomId) => { - showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true); - onFinished(); - }} - /> - ; + const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length; + const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at + + let countsStr; + if (numSpaces > 1) { + countsStr = _t("%(count)s rooms and %(numSpaces)s spaces", { count: numRooms, numSpaces }); + } else if (numSpaces > 0) { + countsStr = _t("%(count)s rooms and 1 space", { count: numRooms, numSpaces }); + } else { + countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces }); + } + + let editSection; + if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { + const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { + return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][]; + }); + + let buttons; + if (selectedRelations.length) { + const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { + return parentChildMap.get(parentId)?.get(childId)?.content.suggested; + }); + + const disabled = removing || saving; + + buttons = <> + { + setRemoving(true); + try { + for (const [parentId, childId] of selectedRelations) { + await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId); + parentChildMap.get(parentId).get(childId).content = {}; + parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); + } + } catch (e) { + setError(_t("Failed to remove some rooms. Try again later")); + } + setRemoving(false); + }} + kind="danger_outline" + disabled={disabled} + > + { removing ? _t("Removing...") : _t("Remove") } + + { + setSaving(true); + try { + for (const [parentId, childId] of selectedRelations) { + const suggested = !selectionAllSuggested; + const existingContent = parentChildMap.get(parentId)?.get(childId)?.content; + if (!existingContent || existingContent.suggested === suggested) continue; + + const content = { + ...existingContent, + suggested: !selectionAllSuggested, + }; + + await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId); + + parentChildMap.get(parentId).get(childId).content = content; + parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); + } + } catch (e) { + setError("Failed to update some suggestions. Try again later"); + } + setSaving(false); + }} + kind="primary_outline" + disabled={disabled} + > + { saving + ? _t("Saving...") + : (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested")) + } + + ; + } + + editSection = + { buttons } + ; + } + + let results; + if (roomsMap.size) { + results = <> + { + setError(""); + if (!selected.has(parentId)) { + setSelected(new Map(selected.set(parentId, new Set([childId])))); + return; + } + + const parentSet = selected.get(parentId); + if (!parentSet.has(childId)) { + setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId])))); + return; + } + + parentSet.delete(childId); + setSelected(new Map(selected.set(parentId, new Set(parentSet)))); + }} + onViewRoomClick={(roomId, autoJoin) => { + showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); + onFinished(); + }} + /> +
    + ; + } else { + results =
    +

    { _t("No results found") }

    +
    { _t("You may want to try a different search or check for typos.") }
    +
    ; + } + + content = <> +
    + { countsStr } + { editSection } +
    + { error &&
    + { error } +
    } + + { results } + + { _t("Create room") } + + + ; + } else { + content = ; } // TODO loading state/error state @@ -546,13 +568,10 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis -
    - { adminButton } -
    { content }
    diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 0b0f2a2ac9..2b4168a983 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -230,10 +230,10 @@ const SpaceLanding = ({ space }) => { try { const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join"); - const parentChildRelations = new EnhancedMap(); + const parentChildRelations = new EnhancedMap>(); data.events.map((ev: ISpaceSummaryEvent) => { if (ev.type === EventType.SpaceChild) { - parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key); + parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); } }); @@ -257,11 +257,10 @@ const SpaceLanding = ({ space }) => { { - showRoom(roomsMap.get(roomId), [], false); // TODO + onViewRoomClick={(roomId, autoJoin) => { + showRoom(roomsMap.get(roomId), [], autoJoin); }} /> ; diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js index e4ad234ae2..0bd491768c 100644 --- a/src/components/views/elements/TextWithTooltip.js +++ b/src/components/views/elements/TextWithTooltip.js @@ -46,12 +46,14 @@ export default class TextWithTooltip extends React.Component { render() { const Tooltip = sdk.getComponent("elements.Tooltip"); + const {class: className, children, tooltip, tooltipClass, ...props} = this.props; + return ( - - {this.props.children} + + {children} {this.state.hover && } ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 07d292a0e7..e4d197470d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2603,20 +2603,30 @@ "Drop file here to upload": "Drop file here to upload", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", - "Undo": "Undo", - "Remove from Space": "Remove from Space", - "No permissions": "No permissions", - "You're in this space": "You're in this space", - "You're in this room": "You're in this room", - "Save changes": "Save changes", - "Promoted to users": "Promoted to users", - "Manage rooms": "Manage rooms", - "Find a room...": "Find a room...", + "Open": "Open", + "You don't have permission": "You don't have permission", + "%(count)s members|other": "%(count)s members", + "%(count)s members|one": "%(count)s member", + "%(count)s rooms|other": "%(count)s rooms", + "%(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", + "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.", + "%(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", + "%(count)s rooms and 1 space|one": "%(count)s room and 1 space", + "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", + "Removing...": "Removing...", + "Mark as not suggested": "Mark as not suggested", + "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.", + "Create room": "Create room", + "Search names and description": "Search names and description", " invites you": " invites you", "Public space": "Public space", "Private space": "Private space", - "%(count)s members|other": "%(count)s members", - "%(count)s members|one": "%(count)s member", "Add existing rooms & spaces": "Add existing rooms & spaces", "Default Rooms": "Default Rooms", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", From 90d87122bc45d3082d4322f8f06085f71377086b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 16 Mar 2021 11:39:06 +0000 Subject: [PATCH 114/183] Tweak copy on space creation flows --- src/components/structures/SpaceRoomView.tsx | 14 ++++++++++---- src/components/views/spaces/SpaceCreateMenu.tsx | 4 ++-- src/i18n/strings/en_EN.json | 10 +++++----- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 0b0f2a2ac9..17ab75e707 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -369,7 +369,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("Next") + buttonLabel = busy ? _t("Creating rooms...") : _t("Continue") } return
    @@ -391,8 +391,14 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { const SpaceSetupPublicShare = ({ space, onFinished }) => { return
    -

    { _t("Share your public space") }

    -
    { _t("At the moment only you can see it.") }
    +

    { _t("Invite people") }

    +
    + { + _t("It's just you at the moment.") + } { + _t("%(spaceName)s will be even better with others", { spaceName: space.name }) + } +
    @@ -610,7 +616,7 @@ export default class SpaceRoomView extends React.PureComponent { return this.setState({ phase: Phase.PublicShare })} />; case Phase.PublicShare: diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 88098d1b66..ca5b25f128 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -140,9 +140,9 @@ const SpaceCreateMenu = ({ onFinished }) => {

    { - _t("Give it a photo, name and description to help you identify it.") + _t("Add some details to help people recognise it.") } { - _t("You can change these at any point.") + _t("You can change these anytime.") }

    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 07d292a0e7..e3ccc1794d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -998,8 +998,8 @@ "Go back": "Go back", "Your public space": "Your public space", "Your private space": "Your private space", - "Give it a photo, name and description to help you identify it.": "Give it a photo, name and description to help you identify it.", - "You can change these at any point.": "You can change these at any point.", + "Add some details to help people recognise it.": "Add some details to help people recognise it.", + "You can change these anytime.": "You can change these anytime.", "Creating...": "Creating...", "Create": "Create", "Expand space panel": "Expand space panel", @@ -2629,8 +2629,8 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", - "At the moment only you can see it.": "At the moment only you can see it.", - "Finish": "Finish", + "It's just you at the moment.": "It's just you at the moment.", + "%(spaceName)s will be even better with others": "%(spaceName)s will be even better with others", "Who are you working with?": "Who are you working with?", "Ensure the right people have access to the space.": "Ensure the right people have access to the space.", "Just Me": "Just Me", @@ -2642,7 +2642,7 @@ "Invite by username": "Invite by username", "Inviting...": "Inviting...", "What are some things you want to discuss?": "What are some things you want to discuss?", - "We'll create rooms for each topic.": "We'll create rooms for each topic.", + "We'll create a room for each of them. You can add more later too.": "We'll create a room for each of them. You can add more later too.", "What projects are you working on?": "What projects are you working on?", "We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", From c6f6d24b32b22fc1a77fc306a117e7209438ce76 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 16 Mar 2021 14:18:45 +0000 Subject: [PATCH 115/183] Iterate space creation and previews --- src/components/structures/SpaceRoomView.tsx | 87 ++++++++++++++++++--- src/i18n/strings/en_EN.json | 1 + src/stores/SpaceStore.tsx | 35 ++++++--- 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 17ab75e707..cfa261bb9b 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, {RefObject, useContext, useRef, useState} from "react"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; +import {EventSubscription} from "fbemitter"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import RoomAvatar from "../views/avatars/RoomAvatar"; @@ -42,7 +43,6 @@ import ErrorBoundary from "../views/elements/ErrorBoundary"; import {ActionPayload} from "../../dispatcher/payloads"; import RightPanel from "./RightPanel"; import RightPanelStore from "../../stores/RightPanelStore"; -import {EventSubscription} from "fbemitter"; import {RightPanelPhases} from "../../stores/RightPanelStorePhases"; import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload"; import {useStateArray} from "../../hooks/useStateArray"; @@ -54,6 +54,7 @@ import {EnhancedMap} from "../../utils/maps"; import AutoHideScrollbar from "./AutoHideScrollbar"; import MemberAvatar from "../views/avatars/MemberAvatar"; import {useStateToggle} from "../../hooks/useStateToggle"; +import SpaceStore from "../../stores/SpaceStore"; interface IProps { space: Room; @@ -66,6 +67,7 @@ interface IProps { interface IState { phase: Phase; showRightPanel: boolean; + myMembership: string; } enum Phase { @@ -98,6 +100,8 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); + const [busy, setBusy] = useState(false); + let inviterSection; let joinButtons; if (myMembership === "invite") { @@ -121,11 +125,35 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => } joinButtons = <> - - + { + setBusy(true); + onRejectButtonClicked(); + }} /> + { + setBusy(true); + onJoinButtonClicked(); + }} + /> ; } else { - joinButtons = + joinButtons = ( + { + setBusy(true); + onJoinButtonClicked(); + }} + /> + ) + } + + if (busy) { + joinButtons = ; } let visibilitySection; @@ -403,7 +431,7 @@ const SpaceSetupPublicShare = ({ space, onFinished }) => {
    - +
    ; }; @@ -553,17 +581,26 @@ export default class SpaceRoomView extends React.PureComponent { this.state = { phase, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + myMembership: this.props.space.getMyMembership(), }; this.dispatcherRef = defaultDispatcher.register(this.onAction); this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate); + this.context.on("Room.myMembership", this.onMyMembership); } componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); this.rightPanelStoreToken.remove(); + this.context.off("Room.myMembership", this.onMyMembership); } + private onMyMembership = (room: Room, myMembership: string) => { + if (room.roomId === this.props.space.roomId) { + this.setState({ myMembership }); + } + }; + private onRightPanelStoreUpdate = () => { this.setState({ showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, @@ -600,10 +637,43 @@ export default class SpaceRoomView extends React.PureComponent { } }; + private goToFirstRoom = async () => { + const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId); + if (childRooms.length) { + const room = childRooms[0]; + defaultDispatcher.dispatch({ + action: "view_room", + room_id: room.roomId, + }); + return; + } + + let suggestedRooms = SpaceStore.instance.suggestedRooms; + if (SpaceStore.instance.activeSpace !== this.props.space) { + // the space store has the suggested rooms loaded for a different space, fetch the right ones + suggestedRooms = (await SpaceStore.instance.fetchSuggestedRooms(this.props.space, 1)).rooms; + } + + if (suggestedRooms.length) { + const room = suggestedRooms[0]; + defaultDispatcher.dispatch({ + action: "view_room", + room_id: room.room_id, + oobData: { + avatarUrl: room.avatar_url, + name: room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"), + }, + }); + return; + } + + this.setState({ phase: Phase.Landing }); + }; + private renderBody() { switch (this.state.phase) { case Phase.Landing: - if (this.props.space.getMyMembership() === "join") { + if (this.state.myMembership === "join") { return ; } else { return { onFinished={() => this.setState({ phase: Phase.PublicShare })} />; case Phase.PublicShare: - return this.setState({ phase: Phase.Landing })} - />; + return ; case Phase.PrivateScope: return { } if (space) { - try { - const data: { - rooms: ISpaceSummaryRoom[]; - events: ISpaceSummaryEvent[]; - } = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, MAX_SUGGESTED_ROOMS); - if (this._activeSpace === space) { - this._suggestedRooms = data.rooms.filter(roomInfo => { - return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id); - }); - this.emit(SUGGESTED_ROOMS, this._suggestedRooms); - } - } catch (e) { - console.error(e); + const data = await this.fetchSuggestedRooms(space); + if (this._activeSpace === space) { + this._suggestedRooms = data.rooms.filter(roomInfo => { + return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id); + }); + this.emit(SUGGESTED_ROOMS, this._suggestedRooms); } } } + public fetchSuggestedRooms = async (space: Room, limit = MAX_SUGGESTED_ROOMS) => { + try { + const data: { + rooms: ISpaceSummaryRoom[]; + events: ISpaceSummaryEvent[]; + } = await this.matrixClient.getSpaceSummary(space.roomId, 0, true, false, limit); + return data; + } catch (e) { + console.error(e); + } + return { + rooms: [], + events: [], + }; + }; + public addRoomToSpace(space: Room, roomId: string, via: string[], suggested = false, autoJoin = false) { return this.matrixClient.sendStateEvent(space.roomId, EventType.SpaceChild, { via, From 31ce19373be030ec2869195545828eadb567cd0a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 16 Mar 2021 16:16:51 +0000 Subject: [PATCH 116/183] Fix space panel spacings --- res/css/structures/_SpacePanel.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index d3e7d7efee..ffe67ce6ab 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -63,7 +63,7 @@ $activeBorderColor: $secondary-fg-color; } .mx_AutoHideScrollbar { - padding: 16px 0; + padding: 8px 0 16px; } .mx_SpaceButton_toggleCollapse { @@ -99,7 +99,6 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton { border-radius: 8px; - margin-bottom: 2px; display: flex; align-items: center; padding: 4px 4px 4px 0; From 88b7c8f53dd8d043abbc4f589cb190e9e7c8b9d9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 18 Mar 2021 13:17:30 +0000 Subject: [PATCH 117/183] Fix add existing to space dialog showing all spaces additionally as rooms --- 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 66efaefd9d..500637244a 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -69,6 +69,7 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, const existingRoomsSet = new Set(existingRooms); const rooms = cli.getVisibleRooms().filter(room => { return !existingRoomsSet.has(room) // not already in space + && !room.isSpaceRoom() // not a space itself && room.name.toLowerCase().includes(lcQuery) // contains query && !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM }); From 3718d550c5b97d722247c03d4a9bfafc03158b1c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Mar 2021 11:42:53 +0000 Subject: [PATCH 118/183] Fix space creation menu shade width --- res/css/views/spaces/_SpaceCreateMenu.scss | 5 +---- src/components/views/spaces/SpacePanel.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index 2a11ec9f23..be387d2c26 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -14,10 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// TODO: the space panel currently does not have a fixed width, -// just the headers at each level have a max-width of 150px -// so this will look slightly off for now. We should probably use css grid for the whole main layout... -$spacePanelWidth: 200px; +$spacePanelWidth: 71px; .mx_SpaceCreateMenu_wrapper { // background blur everything except SpacePanel diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 48e2c86b2c..bacf1bd929 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -220,13 +220,19 @@ const SpacePanel = () => { { + openMenu(); + if (!isPanelCollapsed) setPanelCollapsed(true); + }} isNarrow={isPanelCollapsed} /> setPanelCollapsed(!isPanelCollapsed)} + onClick={() => { + setPanelCollapsed(!isPanelCollapsed); + if (menuDisplayed) closeMenu(); + }} title={expandCollapseButtonTitle} /> { contextMenu } From 2b4c670b8964cf52c4762331f9e566527fdb1ad0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Mar 2021 11:46:44 +0000 Subject: [PATCH 119/183] Fix favourites not showing up in home until a refresh --- 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 a329ee78e1..b82acfd0ed 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -394,7 +394,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent: MatrixEvent) => { if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) { // If the room was in favourites and now isn't or the opposite then update its position in the trees - if (!!ev.getContent()[DefaultTagID.Favourite] !== !!lastEvent.getContent()[DefaultTagID.Favourite]) { + const oldTags = lastEvent.getContent()?.tags; + const newTags = ev.getContent()?.tags; + if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) { this.onRoomUpdate(room); } } From 1fbbb67e7484115139ecb87d7299c7913b8994d4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Mar 2021 13:16:36 +0000 Subject: [PATCH 120/183] Consolidate button styles in Space creation flows --- res/css/structures/_SpaceRoomView.scss | 105 ++++++++---------- res/css/views/spaces/_SpaceCreateMenu.scss | 44 +------- res/css/views/spaces/_SpacePublicShare.scss | 33 +----- src/components/structures/SpaceRoomView.tsx | 41 +++---- .../views/spaces/SpacePublicShare.tsx | 5 +- 5 files changed, 66 insertions(+), 162 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 60abe36c29..a7ce630b96 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -16,6 +16,51 @@ limitations under the License. $SpaceRoomViewInnerWidth: 428px; +@define-mixin SpacePillButton { + position: relative; + padding: 16px 32px 16px 72px; + width: 432px; + box-sizing: border-box; + border-radius: 8px; + border: 1px solid $input-darker-bg-color; + font-size: $font-15px; + margin: 20px 0; + + > h3 { + font-weight: $font-semi-bold; + margin: 0 0 4px; + } + + > span { + color: $secondary-fg-color; + } + + &::before { + position: absolute; + content: ''; + width: 32px; + height: 32px; + top: 24px; + left: 20px; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 24px; + background-color: $tertiary-fg-color; + } + + &:hover { + border-color: $accent-color; + + &::before { + background-color: $accent-color; + } + + > span { + color: $primary-fg-color; + } + } +} + .mx_SpaceRoomView { .mx_MainSplit > div:first-child { padding: 80px 60px; @@ -331,64 +376,8 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView_privateScope { - .mx_RadioButton { - width: $SpaceRoomViewInnerWidth; - border-radius: 8px; - border: 1px solid $space-button-outline-color; - padding: 16px 16px 16px 72px; - margin-top: 36px; - cursor: pointer; - box-sizing: border-box; - position: relative; - - > div:first-of-type { - // hide radio dot - display: none; - } - - .mx_RadioButton_content { - margin: 0; - - > h3 { - margin: 0 0 4px; - font-size: $font-15px; - font-weight: $font-semi-bold; - line-height: $font-18px; - } - - > div { - color: $secondary-fg-color; - font-size: $font-15px; - line-height: $font-24px; - } - } - - &::before { - content: ""; - position: absolute; - height: 32px; - width: 32px; - top: 24px; - left: 20px; - background-color: $secondary-fg-color; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - } - } - - .mx_RadioButton_checked { - border-color: $accent-color; - - .mx_RadioButton_content { - > div { - color: $primary-fg-color; - } - } - - &::before { - background-color: $accent-color; - } + .mx_AccessibleButton { + @mixin SpacePillButton; } .mx_SpaceRoomView_privateScope_justMeButton::before { diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index be387d2c26..bea39e2389 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -45,53 +45,11 @@ $spacePanelWidth: 71px; } .mx_SpaceCreateMenuType { - position: relative; - padding: 16px 32px 16px 72px; - width: 432px; - box-sizing: border-box; - border-radius: 8px; - border: 1px solid $input-darker-bg-color; - font-size: $font-15px; - margin: 20px 0; - - > h3 { - font-weight: $font-semi-bold; - margin: 0 0 4px; - } - - > span { - color: $secondary-fg-color; - } - - &::before { - position: absolute; - content: ''; - width: 32px; - height: 32px; - top: 24px; - left: 20px; - mask-position: center; - mask-repeat: no-repeat; - mask-size: 32px; - background-color: $tertiary-fg-color; - } - - &:hover { - border-color: $accent-color; - - &::before { - background-color: $accent-color; - } - - > span { - color: $primary-fg-color; - } - } + @mixin SpacePillButton; } .mx_SpaceCreateMenuType_public::before { mask-image: url('$(res)/img/globe.svg'); - mask-size: 26px; } .mx_SpaceCreateMenuType_private::before { mask-image: url('$(res)/img/element-icons/lock.svg'); diff --git a/res/css/views/spaces/_SpacePublicShare.scss b/res/css/views/spaces/_SpacePublicShare.scss index 9ba0549ae3..373fa94e00 100644 --- a/res/css/views/spaces/_SpacePublicShare.scss +++ b/res/css/views/spaces/_SpacePublicShare.scss @@ -16,38 +16,7 @@ limitations under the License. .mx_SpacePublicShare { .mx_AccessibleButton { - border: 1px solid $space-button-outline-color; - box-sizing: border-box; - border-radius: 8px; - padding: 12px 24px 12px 52px; - margin-top: 16px; - width: $SpaceRoomViewInnerWidth; - font-size: $font-15px; - line-height: $font-24px; - position: relative; - display: flex; - - > span { - color: #368bd6; - margin-left: auto; - } - - &:hover { - background-color: rgba(141, 151, 165, 0.1); - } - - &::before { - content: ""; - position: absolute; - width: 30px; - height: 30px; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - background: $muted-fg-color; - left: 12px; - top: 9px; - } + @mixin SpacePillButton; &.mx_SpacePublicShare_shareButton::before { mask-image: url('$(res)/img/element-icons/link.svg'); diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index cfa261bb9b..23cd2f898c 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -32,7 +32,6 @@ import {useRoomMembers} from "../../hooks/useRoomMembers"; import createRoom, {IOpts, Preset} from "../../createRoom"; import Field from "../views/elements/Field"; import {useEventEmitter} from "../../hooks/useEventEmitter"; -import StyledRadioGroup from "../views/elements/StyledRadioGroup"; import withValidation from "../views/elements/Validation"; import * as Email from "../../email"; import defaultDispatcher from "../../dispatcher/dispatcher"; @@ -443,32 +442,20 @@ const SpaceSetupPrivateScope = ({ onFinished }) => {

    { _t("Who are you working with?") }

    { _t("Ensure the right people have access to the space.") }
    - -

    { _t("Just Me") }

    -
    { _t("A private space just for you") }
    - , - }, { - value: "meAndMyTeammates", - className: "mx_SpaceRoomView_privateScope_meAndMyTeammatesButton", - label: -

    { _t("Me and my teammates") }

    -
    { _t("A private space for you and your teammates") }
    -
    , - }, - ]} - /> - -
    - onFinished(option !== "justMe")} /> -
    + { onFinished(false) }} + > +

    { _t("Just me") }

    +
    { _t("A private space to organise your rooms") }
    +
    + { onFinished(true) }} + > +

    { _t("Me and my teammates") }

    +
    { _t("A private space for you and your teammates") }
    +
    ; }; diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx index 3930c1db16..07257d295f 100644 --- a/src/components/views/spaces/SpacePublicShare.tsx +++ b/src/components/views/spaces/SpacePublicShare.tsx @@ -47,7 +47,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => { } }} > - { _t("Share invite link") } +

    { _t("Share invite link") }

    { copiedText } { onFinished(); }} > - { _t("Invite by email or username") } +

    { _t("Invite people") }

    + { _t("Invite with email or username") }
    ; }; From 76dffdcb2c0671ea30d0c77a9b1374fe9c455cf4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 19 Mar 2021 13:20:04 +0000 Subject: [PATCH 121/183] Tweak space creation copy and auto focus fields --- src/components/structures/SpaceRoomView.tsx | 41 +++++++++++-------- .../views/spaces/SpaceCreateMenu.tsx | 2 +- .../views/spaces/SpacePublicShare.tsx | 2 +- src/i18n/strings/en_EN.json | 25 +++++------ 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 23cd2f898c..f0789e1e21 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -364,6 +364,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { placeholder={placeholders[i]} value={roomNames[i]} onChange={ev => setRoomName(i, ev.target.value)} + autoFocus={i === 2} />; }); @@ -418,13 +419,9 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { const SpaceSetupPublicShare = ({ space, onFinished }) => { return
    -

    { _t("Invite people") }

    -
    - { - _t("It's just you at the moment.") - } { - _t("%(spaceName)s will be even better with others", { spaceName: space.name }) - } +

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

    +
    + { _t("It's just you at the moment, it will be even better with others.") }
    @@ -435,12 +432,12 @@ const SpaceSetupPublicShare = ({ space, onFinished }) => {
    ; }; -const SpaceSetupPrivateScope = ({ onFinished }) => { - const [option, setOption] = useState(null); - +const SpaceSetupPrivateScope = ({ space, onFinished }) => { return

    { _t("Who are you working with?") }

    -
    { _t("Ensure the right people have access to the space.") }
    +
    + { _t("Make sure the right people have access to %(name)s", { name: space.name }) } +
    { onChange={ev => setEmailAddress(i, ev.target.value)} ref={fieldRefs[i]} onValidate={validateEmailRules} + autoFocus={i === 0} />; }); @@ -522,9 +520,18 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => { setBusy(false); }; + let onClick = onFinished; + let buttonLabel = _t("Skip for now"); + if (emailAddresses.some(name => name.trim())) { + onClick = onNextClick; + buttonLabel = busy ? _t("Inviting...") : _t("Continue") + } + return

    { _t("Invite your teammates") }

    -
    { _t("Ensure the right people have access to the space.") }
    +
    + { _t("Make sure the right people have access. You can invite more later.") } +
    { error &&
    { error }
    } { fields } @@ -539,8 +546,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
    - {_t("Skip for now")} - +
    ; }; @@ -673,7 +679,8 @@ export default class SpaceRoomView extends React.PureComponent { return this.setState({ phase: Phase.PublicShare })} />; case Phase.PublicShare: @@ -681,6 +688,7 @@ export default class SpaceRoomView extends React.PureComponent { case Phase.PrivateScope: return { this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms }); }} @@ -694,7 +702,8 @@ export default class SpaceRoomView extends React.PureComponent { return this.setState({ phase: Phase.Landing })} />; } diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index ca5b25f128..879cf929e0 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -108,7 +108,7 @@ const SpaceCreateMenu = ({ onFinished }) => { body =

    { _t("Create a space") }

    { _t("Spaces are new ways to group rooms and people. " + - "To join an existing space you’ll need an invite") }

    + "To join an existing space you'll need an invite.") }

    { const success = await copyPlaintext(permalinkCreator.forRoom()); const text = success ? _t("Copied!") : _t("Failed to copy"); setCopiedText(text); - await sleep(10); + await sleep(5000); if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time setCopiedText(_t("Click to copy")); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4be6e06459..5ba787422f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -989,7 +989,7 @@ "Name": "Name", "Description": "Description", "Create a space": "Create a space", - "Spaces are new ways to group rooms and people. To join an existing space you’ll need an invite": "Spaces are new ways to group rooms and people. To join an existing space you’ll need an invite", + "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.", "Public": "Public", "Open space for anyone, best for communities": "Open space for anyone, best for communities", "Private": "Private", @@ -1009,10 +1009,10 @@ "Copied!": "Copied!", "Failed to copy": "Failed to copy", "Share invite link": "Share invite link", - "Invite by email or username": "Invite by email or username", + "Invite people": "Invite people", + "Invite with email or username": "Invite with email or username", "Invite members": "Invite members", "Share your public space": "Share your public space", - "Invite people": "Invite people", "Settings": "Settings", "Leave space": "Leave space", "New room": "New room", @@ -2629,23 +2629,24 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", - "It's just you at the moment.": "It's just you at the moment.", - "%(spaceName)s will be even better with others": "%(spaceName)s will be even better with others", + "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", "Who are you working with?": "Who are you working with?", - "Ensure the right people have access to the space.": "Ensure the right people have access to the space.", - "Just Me": "Just Me", - "A private space just for you": "A private space just for you", + "Make sure the right people have access to %(name)s": "Make sure the right people have access to %(name)s", + "Just me": "Just me", + "A private space to organise your rooms": "A private space to organise your rooms", "Me and my teammates": "Me and my teammates", "A private space for you and your teammates": "A private space for you and your teammates", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", - "Invite your teammates": "Invite your teammates", - "Invite by username": "Invite by username", "Inviting...": "Inviting...", + "Invite your teammates": "Invite your teammates", + "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", + "Invite by username": "Invite by username", "What are some things you want to discuss?": "What are some things you want to discuss?", - "We'll create a room for each of them. You can add more later too.": "We'll create a room for each of them. You can add more later too.", + "Let's create a room for each of them. You can add more later too, including already existing ones.": "Let's create a room for each of them. You can add more later too, including already existing ones.", "What projects are you working on?": "What projects are you working on?", - "We'll create rooms for each of them. You can add existing rooms after setup.": "We'll create rooms for each of them. You can add existing rooms after setup.", + "We'll create rooms for each of them. You can add more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", From 70e0b77fc4db3f9ac6a83b903e15fa25fa0be907 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Fri, 19 Mar 2021 21:12:47 +0530 Subject: [PATCH 122/183] Changed invite phrases in context menu and invite dialog --- src/components/views/dialogs/InviteDialog.tsx | 4 +++- src/components/views/rooms/RoomTile.tsx | 2 +- src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 9aef421d5a..de0b5b237b 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -1256,7 +1256,9 @@ export default class InviteDialog extends React.PureComponent { {canInvite ? ( ) : null} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 07d292a0e7..34a346fef0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1601,6 +1601,7 @@ "Favourited": "Favourited", "Favourite": "Favourite", "Low Priority": "Low Priority", + "Invite People": "Invite People", "Leave Room": "Leave Room", "Room options": "Room options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", @@ -2201,6 +2202,7 @@ "Go": "Go", "Invite to %(spaceName)s": "Invite to %(spaceName)s", "Unnamed Space": "Unnamed Space", + "Invite to %(roomName)s": "Invite to %(roomName)s", "Invite someone using their name, email address, username (like ) or share this space.": "Invite someone using their name, email address, username (like ) or share this space.", "Invite someone using their name, username (like ) or share this space.": "Invite someone using their name, username (like ) or share this space.", "Invite someone using their name, email address, username (like ) or share this room.": "Invite someone using their name, email address, username (like ) or share this room.", From 495cbadb08f7ab9cf7f961459d3dc843297e9faf Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Fri, 19 Mar 2021 22:46:19 +0530 Subject: [PATCH 123/183] Update src/components/views/rooms/RoomTile.tsx --- src/components/views/rooms/RoomTile.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 7eaad9e204..79db460275 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -464,16 +464,8 @@ export default class RoomTile extends React.PureComponent { const isLowPriority = roomTags.includes(DefaultTagID.LowPriority); const lowPriorityLabel = _t("Low Priority"); - const inRoom = this.props.room.getMyMembership() === "join"; const userId = MatrixClientPeg.get().getUserId(); - let canInvite = inRoom; - const powerLevels = this.props.room.currentState - .getStateEvents("m.room.power_levels", "") - ?.getContent(); - const me = this.props.room.getMember(userId); - if (powerLevels && me && powerLevels.invite > me.powerLevel) { - canInvite = false; - } + const canInvite = this.props.room.canInvite(userId); contextMenu = Date: Fri, 19 Mar 2021 17:08:01 -0600 Subject: [PATCH 124/183] Tweak a bunch of settings --- src/voice/VoiceRecorder.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/voice/VoiceRecorder.ts b/src/voice/VoiceRecorder.ts index 66eb64b424..3d1008d45e 100644 --- a/src/voice/VoiceRecorder.ts +++ b/src/voice/VoiceRecorder.ts @@ -26,10 +26,15 @@ export class VoiceRecorder { mediaTrackConstraints: { deviceId: CallMediaHandler.getAudioInput(), }, - encoderSampleRate: 16000, // we could go down to 12khz, but we lose quality + encoderSampleRate: 48000, // we could go down to 12khz, but we lose quality. 48khz is a webrtc default encoderApplication: 2048, // voice (default is "audio") streamPages: true, // so we can have a live EQ for the user - encoderFrameSize: 10, // we want updates fairly regularly for the UI + encoderFrameSize: 20, // ms, we want updates fairly regularly for the UI + numberOfChannels: 1, // stereo isn't important for us + //sourceNode: instanceof MediaStreamAudioSourceNode, // TODO: @@ Travis: Use this for EQ stuff. + encoderBitRate: 64000, // 64kbps is average for webrtc + encoderComplexity: 3, // 0-10, 0 is fast and low complexity + resampleQuality: 3, // 0-10, 10 is slow and high quality }); private buffer = new Uint8Array(0); private mxc: string; From 48db1a5967a3565f399de71f9b37aee22b361ac7 Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 19 Mar 2021 19:43:53 +0000 Subject: [PATCH 125/183] Translated using Weblate (German) Currently translated at 96.7% (2794 of 2889 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 | 188 ++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 92 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index a8b2ac7878..22f27823a6 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1,5 +1,5 @@ { - "Filter room members": "Raum-Mitglieder filtern", + "Filter room members": "Raummitglieder filtern", "You have no visible notifications": "Du hast keine sichtbaren Benachrichtigungen", "Invites": "Einladungen", "Favourites": "Favoriten", @@ -97,7 +97,7 @@ "VoIP conference started.": "VoIP-Konferenz gestartet.", "Who can access this room?": "Wer kann diesen Raum betreten?", "Who can read history?": "Wer kann den bisherigen Chatverlauf lesen?", - "You do not have permission to post to this room": "Du hast keine Berechtigung, in diesem Raum etwas zu senden", + "You do not have permission to post to this room": "Du hast keine Berechtigung, etwas in diesen Raum zu senden", "Call Timeout": "Anruf-Timeout", "Existing Call": "Bereits bestehender Anruf", "Failed to verify email address: make sure you clicked the link in the email": "Verifizierung der E-Mail-Adresse fehlgeschlagen: Bitte stelle sicher, dass du den Link in der E-Mail angeklickt hast", @@ -193,7 +193,7 @@ "and %(count)s others...|one": "und ein(e) weitere(r)...", "Are you sure?": "Bist du sicher?", "Attachment": "Anhang", - "Ban": "Verbannen", + "Ban": "Bannen", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Es kann keine Verbindung zum Heimserver via HTTP aufgebaut werden, wenn die Adresszeile des Browsers eine HTTPS-URL enthält. Entweder HTTPS verwenden oder alternativ unsichere Skripte erlauben.", "Click to mute audio": "Klicke um den Ton stumm zu stellen", "Click to mute video": "Klicken, um das Video stummzuschalten", @@ -210,7 +210,7 @@ "Failed to set display name": "Anzeigename konnte nicht gesetzt werden", "Fill screen": "Fülle Bildschirm", "Incorrect verification code": "Falscher Verifizierungscode", - "Join Room": "Dem Raum beitreten", + "Join Room": "Raum beitreten", "Kick": "Kicken", "not specified": "nicht spezifiziert", "No more results": "Keine weiteren Ergebnisse", @@ -218,7 +218,7 @@ "OK": "OK", "Search": "Suchen", "Search failed": "Suche ist fehlgeschlagen", - "Server error": "Server-Fehler", + "Server error": "Serverfehler", "Server may be unavailable, overloaded, or search timed out :(": "Der Server ist entweder nicht verfügbar, überlastet oder die Suche wurde wegen Zeitüberschreitung abgebrochen :(", "Server unavailable, overloaded, or something else went wrong.": "Server ist nicht verfügbar, überlastet oder ein anderer Fehler ist aufgetreten.", "%(count)s of your messages have not been sent.|other": "Einige deiner Nachrichten wurden nicht gesendet.", @@ -255,7 +255,7 @@ "Operation failed": "Aktion fehlgeschlagen", "Unmute": "Stummschalten aufheben", "Invalid file%(extra)s": "Ungültige Datei%(extra)s", - "Please select the destination room for this message": "Bitte den Raum auswählen, an den diese Nachricht gesendet werden soll", + "Please select the destination room for this message": "Wähle den Raum aus, an den du die Nachricht schicken willst", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s hat den Raum-Namen entfernt.", "Passphrases must match": "Passphrases müssen übereinstimmen", "Passphrase must not be empty": "Passphrase darf nicht leer sein", @@ -280,8 +280,8 @@ "Please enter the code it contains:": "Bitte gib den darin enthaltenen Code ein:", "powered by Matrix": "betrieben mit Matrix", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "Wenn du keine E-Mail-Adresse angibst, wirst du nicht in der Lage sein, dein Passwort zurückzusetzen. Bist du sicher?", - "Error decrypting audio": "Audio-Entschlüsselung fehlgeschlagen", - "Error decrypting image": "Bild-Entschlüsselung fehlgeschlagen", + "Error decrypting audio": "Entschlüsseln des Audios fehlgeschlagen", + "Error decrypting image": "Entschlüsselung des Bilds fehlgeschlagen", "Error decrypting video": "Video-Entschlüsselung fehlgeschlagen", "Import room keys": "Raum-Schlüssel importieren", "File to import": "Zu importierende Datei", @@ -299,14 +299,14 @@ "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Dieser Prozess erlaubt es dir, die zuvor von einem anderen Matrix-Client exportierten Verschlüsselungs-Schlüssel zu importieren. Danach kannst du alle Nachrichten entschlüsseln, die auch bereits auf dem anderen Client entschlüsselt werden konnten.", "If you have previously used a more recent version of %(brand)s, your session may be incompatible with this version. Close this window and return to the more recent version.": "Wenn du zuvor eine aktuellere Version von %(brand)s verwendet hast, ist deine Sitzung eventuell inkompatibel mit dieser Version. Bitte schließe dieses Fenster und kehre zur aktuelleren Version zurück.", "Drop file here to upload": "Datei hier loslassen zum hochladen", - "Idle": "Untätig", + "Idle": "Abwesend", "Ongoing conference call%(supportedText)s.": "Laufendes Konferenzgespräch%(supportedText)s.", - "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Du wirst jetzt auf die Website eines Drittanbieters weitergeleitet, damit du dein Benutzerkonto für die Verwendung von %(integrationsUrl)s authentifizieren kannst. Möchtest du fortfahren?", + "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Um dein Konto für die Verwendung von %(integrationsUrl)s zu authentifizieren, wirst du jetzt auf die Website eines Drittanbieters weitergeleitet. Möchtest du fortfahren?", "Start automatically after system login": "Nach System-Login automatisch starten", "Jump to first unread message.": "Zur ersten ungelesenen Nachricht springen.", "Options": "Optionen", "Invited": "Eingeladen", - "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s hat das Raum-Bild entfernt.", + "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s hat das Raumbild entfernt.", "No Webcams detected": "Keine Webcam erkannt", "No Microphones detected": "Keine Mikrofone erkannt", "No media permissions": "Keine Medienberechtigungen", @@ -322,14 +322,14 @@ "Anyone": "Jeder", "Are you sure you want to leave the room '%(roomName)s'?": "Bist du sicher, dass du den Raum '%(roomName)s' verlassen möchtest?", "Custom level": "Benutzerdefiniertes Berechtigungslevel", - "Publish this room to the public in %(domain)s's room directory?": "Diesen Raum im Raum-Verzeichnis von %(domain)s veröffentlichen?", + "Publish this room to the public in %(domain)s's room directory?": "Diesen Raum im Raumverzeichnis von %(domain)s veröffentlichen?", "Register": "Registrieren", "Save": "Speichern", "Verified key": "Verifizierter Schlüssel", "You have disabled URL previews by default.": "Du hast die URL-Vorschau standardmäßig deaktiviert.", "You have enabled URL previews by default.": "Du hast die URL-Vorschau standardmäßig aktiviert.", - "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s hat das Raum-Bild geändert zu ", - "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s hat das Raum-Bild für %(roomName)s geändert", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s hat das Raumbild zu geändert", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s hat das Raumbild von %(roomName)s geändert", "Add": "Hinzufügen", "Error: Problem communicating with the given homeserver.": "Fehler: Problem bei der Kommunikation mit dem angegebenen Home-Server.", "Failed to fetch avatar URL": "Abrufen der Avatar-URL fehlgeschlagen", @@ -344,14 +344,14 @@ "New Password": "Neues Passwort", "Username available": "Benutzername ist verfügbar", "Username not available": "Benutzername ist nicht verfügbar", - "Something went wrong!": "Etwas ging schief!", + "Something went wrong!": "Etwas ist schiefgelaufen!", "This will be your account name on the homeserver, or you can pick a different server.": "Dies wird dein zukünftiger Benutzername auf dem Heimserver. Alternativ kannst du auch einen anderen Server auswählen.", "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", "Active call (%(roomName)s)": "Aktiver Anruf (%(roomName)s)", - "Admin Tools": "Admin-Werkzeuge", + "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.", "Close": "Schließen", "Custom": "Erweitert", @@ -366,9 +366,9 @@ "No display name": "Kein Anzeigename", "Private Chat": "Privater Chat", "Public Chat": "Öffentlicher Chat", - "%(roomName)s does not exist.": "%(roomName)s existert nicht.", - "%(roomName)s is not accessible at this time.": "%(roomName)s ist aktuell nicht zugreifbar.", - "Seen by %(userName)s at %(dateTime)s": "Gesehen von %(userName)s um %(dateTime)s", + "%(roomName)s does not exist.": "%(roomName)s existiert nicht.", + "%(roomName)s is not accessible at this time.": "Auf %(roomName)s kann momentan nicht zugegriffen werden.", + "Seen by %(userName)s at %(dateTime)s": "Von %(userName)s um %(dateTime)s gesehen", "Start authentication": "Authentifizierung beginnen", "This room": "diesen Raum", "unknown caller": "Unbekannter Anrufer", @@ -376,7 +376,7 @@ "Upload new:": "Neue(s) hochladen:", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (Berechtigungslevel %(powerLevelNumber)s)", "(~%(count)s results)|one": "(~%(count)s Ergebnis)", - "(~%(count)s results)|other": "(~%(count)s Ergebnis)", + "(~%(count)s results)|other": "(~%(count)s Ergebnisse)", "(could not connect media)": "(Medienverbindung konnte nicht hergestellt werden)", "(no answer)": "(keine Antwort)", "(unknown failure: %(reason)s)": "(Unbekannter Fehler: %(reason)s)", @@ -474,7 +474,7 @@ "Invalid community ID": "Ungültige Community-ID", "'%(groupId)s' is not a valid community ID": "'%(groupId)s' ist keine gültige Community-ID", "New community ID (e.g. +foo:%(localDomain)s)": "Neue Community-ID (z. B. +foo:%(localDomain)s)", - "Remove from community": "Aus der Community entfernen", + "Remove from community": "Aus Community entfernen", "Failed to remove user from community": "Entfernen des Benutzers aus der Community fehlgeschlagen", "Filter community members": "Community-Mitglieder filtern", "Filter community rooms": "Community-Räume filtern", @@ -598,12 +598,12 @@ "%(duration)sh": "%(duration)sh", "%(duration)sd": "%(duration)sT", "Online for %(duration)s": "Online seit %(duration)s", - "Idle for %(duration)s": "Untätig seit %(duration)s", + "Idle for %(duration)s": "Abwesend seit %(duration)s", "Offline for %(duration)s": "Offline seit %(duration)s", "Unknown for %(duration)s": "Unbekannt seit %(duration)s", "Flair": "Abzeichen", "Showing flair for these communities:": "Abzeichen für diese Communities zeigen:", - "This room is not showing flair for any communities": "Dieser Raum zeigt für keine Communities die Abzeichen an", + "This room is not showing flair for any communities": "Dieser Raum zeigt Abzeichen keiner Communities an", "Something went wrong when trying to get your communities.": "Beim Laden deiner Communities ist etwas schief gelaufen.", "Display your community flair in rooms configured to show it.": "Zeige deinen Community-Flair in den Räumen, die es erlauben.", "This homeserver doesn't offer any login flows which are supported by this client.": "Dieser Heimserver verfügt über kein von diesem Client unterstütztes Anmeldeverfahren.", @@ -643,7 +643,7 @@ "Did you know: you can use communities to filter your %(brand)s experience!": "Wusstest du: Du kannst Communities nutzen um deine %(brand)s-Erfahrung zu filtern!", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Um einen Filter zu setzen, ziehe ein Community-Bild auf das Filter-Panel ganz links. Du kannst jederzeit auf einen Avatar im Filter-Panel klicken, um nur die Räume und Personen aus der Community zu sehen.", "Clear filter": "Filter zurücksetzen", - "Key request sent.": "Schlüssel-Anfragen gesendet.", + "Key request sent.": "Schlüsselanfrage gesendet.", "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-ID's und Aliase die du besucht hast und Nutzernamen anderer Nutzer. Sie enthalten keine Nachrichten.", "Submit debug logs": "Fehlerberichte einreichen", "Code": "Code", @@ -655,8 +655,8 @@ "Join this community": "Community beitreten", "Leave this community": "Community verlassen", "You don't currently have any stickerpacks enabled": "Du hast aktuell keine Stickerpacks aktiviert", - "Hide Stickers": "Sticker verbergen", - "Show Stickers": "Sticker zeigen", + "Hide Stickers": "Sticker ausblenden", + "Show Stickers": "Sticker anzeigen", "Who can join this community?": "Wer kann dieser Community beitreten?", "Everyone": "Jeder", "Stickerpack": "Stickerpack", @@ -809,7 +809,7 @@ "Terms and Conditions": "Geschäftsbedingungen", "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Um den %(homeserverDomain)s -Heimserver weiter zu verwenden, musst du die Geschäftsbedingungen sichten und ihnen zustimmen.", "Review terms and conditions": "Geschäftsbedingungen anzeigen", - "Share Link to User": "Link zum Benutzer teilen", + "Share Link to User": "Link zu Benutzer teilen", "Share room": "Raum teilen", "Share Room": "Raum teilen", "Link to most recent message": "Link zur aktuellsten Nachricht", @@ -821,15 +821,15 @@ "Share Message": "Nachricht teilen", "No Audio Outputs detected": "Keine Audioausgabe erkannt", "Audio Output": "Audioausgabe", - "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In verschlüsselten Räumen, wie diesem, ist die Link-Vorschau standardmäßig deaktiviert damit dein Heimserver (auf dem die Vorschau erzeugt wird) keine Informationen über Links in diesem Raum bekommt.", - "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Wenn jemand eine URL in seine Nachricht einfügt, kann eine URL-Vorschau angezeigt werden, um mehr Informationen über diesen Link zu erhalten, wie z.B. den Titel, die Beschreibung und ein Bild von der Website.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In verschlüsselten Räumen wie diesem ist die Link-Vorschau standardmäßig deaktiviert, damit dein Heimserver (der die Vorschau erzeugt) keine Informationen über Links in diesem Raum bekommt.", + "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Wenn jemand eine URL sendet, kann die Vorschau Informationen wie Titel, Beschreibung oder ein Vorschaubild enthalten.", "The email field must not be blank.": "Das E-Mail-Feld darf nicht leer sein.", "The phone number field must not be blank.": "Das Telefonnummern-Feld darf nicht leer sein.", "The password field must not be blank.": "Das Passwort-Feld darf nicht leer sein.", "Call in Progress": "Gespräch läuft", "A call is already in progress!": "Ein Gespräch läuft bereits!", "You can't send any messages until you review and agree to our terms and conditions.": "Du kannst keine Nachrichten senden bis du unsere Geschäftsbedingungen gelesen und akzeptiert hast.", - "Demote yourself?": "Selbst zurückstufen?", + "Demote yourself?": "Dein eigenes Berechtigungslevel herabsetzen?", "Demote": "Zurückstufen", "This event could not be displayed": "Dieses Ereignis konnte nicht angezeigt werden", "A call is currently being placed!": "Ein Anruf wurde schon gestartet!", @@ -837,7 +837,7 @@ "You do not have permission to start a conference call in this room": "Du hast keine Berechtigung, ein Konferenzgespräch in diesem Raum zu starten", "Failed to remove widget": "Widget konnte nicht entfernt werden", "An error ocurred whilst trying to remove the widget from the room": "Ein Fehler trat auf während versucht wurde, das Widget aus diesem Raum zu entfernen", - "System Alerts": "System-Benachrichtigung", + "System Alerts": "Systembenachrichtigung", "Only room administrators will see this warning": "Nur Raum-Administratoren werden diese Nachricht sehen", "Please contact your service administrator to continue using the service.": "Bitte kontaktiere deinen Systemadministrator um diesen Dienst weiter zu nutzen.", "This homeserver has hit its Monthly Active User limit.": "Dieser Heimserver hat sein Limit an monatlich aktiven Nutzern erreicht.", @@ -856,7 +856,7 @@ "This room has been replaced and is no longer active.": "Dieser Raum wurde ersetzt und ist nicht länger aktiv.", "The conversation continues here.": "Die Konversation wird hier fortgesetzt.", "This room is a continuation of another conversation.": "Dieser Raum ist eine Fortsetzung einer anderen Konversation.", - "Click here to see older messages.": "Klicke hier um ältere Nachrichten zu sehen.", + "Click here to see older messages.": "Klicke hier, um ältere Nachrichten zu sehen.", "Failed to upgrade room": "Konnte Raum nicht aufrüsten", "The room upgrade could not be completed": "Die Raum-Aufrüstung konnte nicht fertiggestellt werden", "Upgrade this room to version %(version)s": "Diesen Raum zur Version %(version)s aufrüsten", @@ -938,7 +938,7 @@ "Don't ask again": "Nicht erneut fragen", "Set up": "Einrichten", "Please review and accept all of the homeserver's policies": "Bitte prüfe und akzeptiere alle Richtlinien des Heimservers", - "Failed to load group members": "Konnte Gruppenmitglieder nicht laden", + "Failed to load group members": "Gruppenmitglieder konnten nicht geladen werden", "That doesn't look like a valid email address": "Sieht nicht nach einer gültigen E-Mail-Adresse aus", "Unable to load commit detail: %(msg)s": "Konnte Commit-Details nicht laden: %(msg)s", "Checking...": "Überprüfe...", @@ -1115,8 +1115,8 @@ "Missing media permissions, click the button below to request.": "Fehlende Medienberechtigungen. Drücke auf den Knopf unten, um sie anzufordern.", "Request media permissions": "Medienberechtigungen anfordern", "Main address": "Primäre Adresse", - "Room avatar": "Raum-Bild", - "Room Name": "Raum-Name", + "Room avatar": "Raumbild", + "Room Name": "Raumname", "Room Topic": "Raum-Thema", "Join": "Beitreten", "Waiting for partner to confirm...": "Warte auf Bestätigung des Gesprächspartners...", @@ -1264,13 +1264,13 @@ "Show hidden events in timeline": "Zeige versteckte Ereignisse in der Chronik", "Low bandwidth mode": "Modus für niedrige Bandbreite", "Reset": "Zurücksetzen", - "Joining room …": "Trete Raum bei …", + "Joining room …": "Raum beitreten …", "Rejecting invite …": "Einladung ablehnen…", "Sign Up": "Registrieren", "Sign In": "Anmelden", "Reason: %(reason)s": "Grund: %(reason)s", "Forget this room": "Diesen Raum entfernen", - "Do you want to join %(roomName)s?": "Möchtest du %(roomName)s betreten?", + "Do you want to join %(roomName)s?": "Möchtest du %(roomName)s beitreten?", " invited you": " hat dich eingeladen", "edited": "bearbeitet", "Edit message": "Nachricht bearbeiten", @@ -1288,9 +1288,9 @@ "Add room": "Raum hinzufügen", "Your profile": "Dein Profil", "Registration Successful": "Registrierung erfolgreich", - "Failed to revoke invite": "Einladung zurückziehen fehlgeschlagen", + "Failed to revoke invite": "Einladung konnte nicht zurückgezogen werden", "Revoke invite": "Einladung zurückziehen", - "Invited by %(sender)s": "Eingeladen von %(sender)s", + "Invited by %(sender)s": "%(sender)s eingeladen", "Changes your avatar in all rooms": "Verändert dein Profilbild in allen Räumen", "Messages": "Nachrichten", "Actions": "Aktionen", @@ -1439,7 +1439,7 @@ "Set a new custom sound": "Setze einen neuen benutzerdefinierten Ton", "Browse": "Durchsuchen", "Direct Messages": "Direktnachrichten", - "You can use /help to list available commands. Did you mean to send this as a message?": "Du kannst /help benutzen, um verfügbare Befehle aufzulisten. Willst du dies als Nachricht senden?", + "You can use /help to list available commands. Did you mean to send this as a message?": "Du kannst /help benutzen, um alle verfügbaren Befehle aufzulisten. Willst du es stattdessen als Nachricht senden?", "Direct message": "Direktnachricht", "Suggestions": "Vorschläge", "Recently Direct Messaged": "Kürzlich direkt verschickt", @@ -1515,8 +1515,8 @@ "Messages in this room are not end-to-end encrypted.": "Nachrichten in diesem Raum sind nicht Ende-zu-Ende verschlüsselt.", "Security": "Sicherheit", "Ask %(displayName)s to scan your code:": "Bitte %(displayName)s, deinen Code zu scannen:", - "Verify by emoji": "Verifizierung durch Emojis", - "Verify by comparing unique emoji.": "Verifizierung durch den Vergleich einzigartiger Emojis.", + "Verify by emoji": "Mit Emojis verifizieren", + "Verify by comparing unique emoji.": "Durch den Vergleich einzigartiger Emojis verifizieren.", "You've successfully verified %(displayName)s!": "Du hast %(displayName)s erfolgreich verifiziert!", "Got it": "Verstanden", "Widget added by": "Widget hinzugefügt von", @@ -1544,8 +1544,8 @@ "Discovery options will appear once you have added an email above.": "Entdeckungsoptionen werden angezeigt, sobald oben eine E-Mail hinzugefügt wurde.", "Discovery options will appear once you have added a phone number above.": "Entdeckungsoptionen werden angezeigt, sobald eine Telefonnummer hinzugefügt wurde.", "Close preview": "Vorschau schließen", - "Loading room preview": "Lade Raumvorschau", - "Join the discussion": "Tritt der Diskussion bei", + "Loading room preview": "Raumvorschau wird geladen", + "Join the discussion": "Der Diskussion beitreten", "Remove for everyone": "Für alle entfernen", "Remove for me": "Für mich entfernen", "Create your Matrix account on ": "Erstelle dein Matrix-Konto auf ", @@ -1553,12 +1553,12 @@ "Your Matrix account on ": "Dein Matrix-Konto auf ", "Remove %(email)s?": "%(email)s entfernen?", "Remove %(phone)s?": "%(phone)s entfernen?", - "Remove recent messages by %(user)s": "Letzte Nachrichten von %(user)s entfernen", - "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Du bist dabei %(count)s Nachrichten von %(user)s zu löschen, was nicht rückgängig gemacht werden kann. Fortfahren?", + "Remove recent messages by %(user)s": "Kürzlich gesendete Nachrichten von %(user)s entfernen", + "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|other": "Du bist dabei, %(count)s Nachrichten von %(user)s zu löschen, was nicht rückgängig gemacht werden kann. Fortfahren?", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "Du bist dabei, eine Nachricht von %(user)s zu löschen. Das kann nicht rückgängig gemacht werden. Fortfahren?", "Remove %(count)s messages|other": "%(count)s Nachrichten entfernen", "Remove %(count)s messages|one": "Eine Nachricht entfernen", - "Remove recent messages": "Letzte Nachrichten entfernen", + "Remove recent messages": "Kürzlich gesendete Nachrichten entfernen", "You're previewing %(roomName)s. Want to join it?": "Du betrachtest %(roomName)s. Willst du beitreten?", "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s hat die alternative Adresse 2%(addresses)s für diesen Raum hinzugefügt.", "%(senderName)s changed the addresses for this room.": "%(senderName)s hat die Adresse für diesen Raum geändert.", @@ -1603,9 +1603,9 @@ "Upgrade the room": "Raum hochstufen", "Enable room encryption": "Raumverschlüsselung aktivieren", "This message cannot be decrypted": "Diese Nachricht kann nicht entschlüsselt werden", - "Encrypted by an unverified session": "Verschlüsselt von einer nicht verifizierten Sitzung", + "Encrypted by an unverified session": "Von einer nicht verifizierten Sitzung verschlüsselt", "Unencrypted": "Unverschlüsselt", - "Encrypted by a deleted session": "Verschlüsselt von einer gelöschten Sitzung", + "Encrypted by a deleted session": "Von einer gelöschten Sitzung verschlüsselt", "The encryption used by this room isn't supported.": "Die von diesem Raum verwendete Verschlüsselung wird nicht unterstützt.", "React": "Reagieren", "e.g. my-room": "z.B. mein-raum", @@ -1670,7 +1670,7 @@ "This bridge is managed by .": "Diese Brücke wird von verwaltet.", "Workspace: %(networkName)s": "Arbeitsbereich: %(networkName)s", "Channel: %(channelName)s": "Kanal: %(channelName)s", - "Show less": "Weniger zeigen", + "Show less": "Weniger anzeigen", "Warning: You should only set up key backup from a trusted computer.": "Achtung: Du solltest die Schlüsselsicherung nur von einem vertrauenswürdigen Computer aus einrichten.", "Regain access to your account and recover encryption keys stored in this session. Without them, you won’t be able to read all of your secure messages in any session.": "Melde dich an, um die ausschließlich in dieser Sitzung gespeicherten Verschlüsselungsschlüssel wiederherzustellen. Du benötigst sie, um deine verschlüsselten Nachrichten in jeder Sitzung zu lesen.", "Forgotten your password?": "Passwort vergessen?", @@ -1792,16 +1792,16 @@ "Deactivate user?": "Benutzer deaktivieren?", "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Beim Deaktivieren wird der Benutzer abgemeldet und ein erneutes Anmelden verhindert. Zusätzlich wird er aus allen Räumen entfernt. Diese Aktion kann nicht rückgängig gemacht werden. Bist du sicher dass du diesen Benutzer deaktivieren willst?", "Deactivate user": "Benutzer deaktivieren", - "Failed to deactivate user": "Deaktivieren des Benutzers fehlgeschlagen", - "Send a reply…": "Sende eine Antwort…", - "Send a message…": "Sende eine Nachricht…", + "Failed to deactivate user": "Benutzer konnte nicht deaktiviert werden", + "Send a reply…": "Antwort senden…", + "Send a message…": "Nachricht senden…", "Bold": "Fett", "Italics": "Kursiv", "Strikethrough": "Durchgestrichen", "Code block": "Code-Block", "Recent rooms": "Letzte Räume", "Loading …": "Lade …", - "Join the conversation with an account": "Tritt der Unterhaltung mit einem Konto bei", + "Join the conversation with an account": "Unterhaltung mit einem Konto beitreten", "You were kicked from %(roomName)s by %(memberName)s": "Du wurdest von %(memberName)s aus %(roomName)s entfernt", "Re-join": "Wieder beitreten", "You were banned from %(roomName)s by %(memberName)s": "Du wurdest von %(memberName)s aus %(roomName)s verbannt", @@ -1809,16 +1809,16 @@ "An error (%(errcode)s) was returned while trying to validate your invite. You could try to pass this information on to a room admin.": "Während der Verifizierung deiner Einladung ist ein Fehler (%(errcode)s) aufgetreten. Du kannst diese Information einem Raum-Administrator weitergeben.", "You can only join it with a working invite.": "Du kannst nur mit einer gültigen Einladung beitreten.", "Try to join anyway": "Dennoch versuchen beizutreten", - "You can still join it because this is a public room.": "Du kannst dennoch beitreten, da es ein öffentlicher Raum ist.", + "You can still join it because this is a public room.": "Du kannst trotzdem beitreten, weil es ein öffentlicher Raum ist.", "This invite to %(roomName)s was sent to %(email)s which is not associated with your account": "Diese Einladung zu %(roomName)s wurde an die Adresse %(email)s gesendet, die nicht zu deinem Konto gehört", - "Link this email with your account in Settings to receive invites directly in %(brand)s.": "Verbinde diese E-Mail-Adresse in den Einstellungen mit deinem Konto um die Einladungen direkt in %(brand)s zu erhalten.", + "Link this email with your account in Settings to receive invites directly in %(brand)s.": "Verbinde diese E-Mail-Adresse in den Einstellungen mit deinem Konto, um die Einladungen direkt in %(brand)s zu erhalten.", "This invite to %(roomName)s was sent to %(email)s": "Diese Einladung zu %(roomName)s wurde an %(email)s gesendet", "Use an identity server in Settings to receive invites directly in %(brand)s.": "Verknüpfe einen Identitätsserver in den Einstellungen um die Einladungen direkt in %(brand)s zu erhalten.", - "Share this email in Settings to receive invites directly in %(brand)s.": "Teile diese E-Mail-Adresse in den Einstellungen um Einladungen direkt in %(brand)s zu erhalten.", + "Share this email in Settings to receive invites directly in %(brand)s.": "Teile diese E-Mail-Adresse in den Einstellungen, um Einladungen direkt in %(brand)s zu erhalten.", "%(roomName)s can't be previewed. Do you want to join it?": "Vorschau von %(roomName)s kann nicht angezeigt werden. Möchtest du den Raum betreten?", "This room doesn't exist. Are you sure you're at the right place?": "Dieser Raum existiert nicht. Bist du sicher, dass du hier richtig bist?", "Try again later, or ask a room admin to check if you have access.": "Versuche es später erneut oder bitte einen Raum-Administrator zu prüfen, ob du berechtigt bist.", - "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s wurde, beim Versuch den Raum zu betreten, zurückgegeben. Wenn du denkst dass diese Meldung nicht korrekt ist, erstelle bitte einen Fehlerbericht.", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s wurde beim Versuch den Raum zu betreten, zurückgegeben. Wenn du denkst dass diese Meldung nicht korrekt ist, erstelle bitte einen Fehlerbericht.", "%(count)s unread messages including mentions.|other": "%(count)s ungelesene Nachrichten einschließlich Erwähnungen.", "%(count)s unread messages including mentions.|one": "1 ungelesene Erwähnung.", "%(count)s unread messages.|other": "%(count)s ungelesene Nachrichten.", @@ -1838,16 +1838,16 @@ "Published Addresses": "Öffentliche Adresse", "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Öffentliche Adressen können von jedem verwendet werden, um den Raum zu betreten. Um eine Adresse zu veröffentlichen musst du zunächst eine lokale Adresse anlegen.", "Other published addresses:": "Andere öffentliche Adressen:", - "No other published addresses yet, add one below": "Keine anderen öffentlichen Adressen vorhanden, füge unten eine hinzu", + "No other published addresses yet, add one below": "Keine anderen öffentlichen Adressen vorhanden. Du kannst weiter unten eine hinzufügen", "New published address (e.g. #alias:server)": "Neue öffentliche Adresse (z.B. #alias:server)", "Local Addresses": "Lokale Adressen", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Erstelle Adressen für diesen Raum, damit andere Benutzer den Raum auf deinem Heimserver (%(localDomain)s) finden können", "Waiting for you to accept on your other session…": "Warte auf die Bestätigung in deiner anderen Sitzung…", "Waiting for %(displayName)s to accept…": "Warte auf die Annahme von %(displayName)s …", "Accepting…": "Annehmen…", - "Start Verification": "Starte Verifikation", + "Start Verification": "Verifizierung starten", "Messages in this room are end-to-end encrypted.": "Nachrichten in diesem Raum sind Ende-zu-Ende verschlüsselt.", - "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Diese Nachrichten sind verschlüsselt und nur du und der Empfänger habt die Schlüssel, um sie zu entschlüsseln.", + "Your messages are secured and only you and the recipient have the unique keys to unlock them.": "Diese Nachrichten sind verschlüsselt und nur du und der Empfänger könnt sie lesen.", "In encrypted rooms, your messages are secured and only you and the recipient have the unique keys to unlock them.": "In verschlüsselten Räumen sind deine Nachrichten verschlüsselt und nur du und der Empfänger habt die Schlüssel um sie zu entschlüsseln.", "Verify User": "Nutzer verifizieren", "For extra security, verify this user by checking a one-time code on both of your devices.": "Für zusätzliche Sicherheit, verifiziere diesen Nutzer, durch Vergleichen eines Einmal-Codes auf euren beiden Geräten.", @@ -1859,10 +1859,10 @@ "Yours, or the other users’ session": "Deine Sitzung oder die des Gegenüber", "%(role)s in %(roomName)s": "%(role)s in %(roomName)s", "This client does not support end-to-end encryption.": "Diese Anwendung unterstützt keine Ende-zu-Ende-Verschlüsselung.", - "Verify by scanning": "Verifizierung durch QR-Code-Scannen", + "Verify by scanning": "Verifizierung durch Scannen eines QR-Codes", "If you can't scan the code above, verify by comparing unique emoji.": "Wenn du den obigen Code nicht scannen kannst, verifiziere stattdessen durch den Emoji-Vergleich.", "Verify all users in a room to ensure it's secure.": "Verifiziere alle Benutzer in einem Raum um die vollständige Sicherheit zu gewährleisten.", - "In encrypted rooms, verify all users to ensure it’s secure.": "Verifiziere alle Benutzer in verschlüsselten Räumen um die vollständige Sicherheit zu gewährleisten.", + "In encrypted rooms, verify all users to ensure it’s secure.": "Verifiziere alle Benutzer in verschlüsselten Räumen, um die vollständige Sicherheit zu gewährleisten.", "You've successfully verified %(deviceName)s (%(deviceId)s)!": "Du hast %(deviceName)s (%(deviceId)s) erfolgreich verifiziert!", "Verified": "Verifiziert", "Start verification again from the notification.": "Starte die Verifikation aus der Benachrichtigung erneut.", @@ -1872,7 +1872,7 @@ "%(displayName)s cancelled verification.": "%(displayName)s hat die Verifikationsanfrage abgelehnt.", "You cancelled verification.": "Du hast die Verifikation abgebrochen.", "Verification cancelled": "Verifikation abgebrochen", - "Compare emoji": "Vergleiche Emojis", + "Compare emoji": "Emojis vergleichen", "Message Actions": "Nachrichtenaktionen", "Show image": "Bild anzeigen", "You have ignored this user, so their message is hidden. Show anyways.": "Du ignorierst diesen Benutzer, deshalb werden seine Nachrichten nicht angezeigt. Trotzdem anzeigen.", @@ -1882,16 +1882,16 @@ "Accepting …": "Annehmen …", "Declining …": "Ablehnen …", "You sent a verification request": "Du hast eine Verifizierungsanfrage gesendet", - "Show all": "Alle zeigen", + "Show all": "Alles zeigen", "Reactions": "Reaktionen", " reacted with %(content)s": " hat mit %(content)s reagiert", "reacted with %(shortName)s": "hat mit %(shortName)s reagiert", "Message deleted": "Nachricht gelöscht", "Message deleted by %(name)s": "Nachricht von %(name)s gelöscht", - "Edited at %(date)s. Click to view edits.": "Am %(date)s geändert. Klicke um Änderungen anzuzeigen.", - "Can't load this message": "Kann diese Nachricht nicht laden", - "Submit logs": "Logs übermitteln", - "Frequently Used": "Häufig verwendet", + "Edited at %(date)s. Click to view edits.": "Am %(date)s geändert. Klicke, um Änderungen anzuzeigen.", + "Can't load this message": "Diese Nachricht kann nicht geladen werden", + "Submit logs": "Log-Dateien senden", + "Frequently Used": "Oft verwendet", "Smileys & People": "Smileys & Leute", "Animals & Nature": "Tiere & Natur", "Food & Drink": "Essen & Trinken", @@ -1900,9 +1900,9 @@ "Objects": "Objekte", "Symbols": "Symbole", "Flags": "Flaggen", - "Quick Reactions": "Praktische Reaktionen", + "Quick Reactions": "Schnelle Reaktionen", "Cancel search": "Suche abbrechen", - "Any of the following data may be shared:": "Die folgenden Daten können geteilt werden:", + "Any of the following data may be shared:": "Die folgenden Informationen können geteilt werden:", "Your avatar URL": "Deine Avatar-URL", "Your user ID": "Deine Nutzer-ID", "Your theme": "Dein Design", @@ -2212,10 +2212,10 @@ "New version available. Update now.": "Neue Version verfügbar. Jetzt aktualisieren.", "Please verify the room ID or address and try again.": "Bitte überprüfe die Raum-ID oder -adresse und versuche es erneut.", "To link to this room, please add an address.": "Um den Raum zu verlinken, füge bitte eine Adresse hinzu.", - "Emoji picker": "Emoji Auswahl", + "Emoji picker": "Emojiauswahl", "Error creating address": "Fehler beim Anlegen der Adresse", "There was an error creating that address. It may not be allowed by the server or a temporary failure occurred.": "Es gab einen Fehler beim Anlegen der Adresse. Entweder erlaubt es der Server nicht oder es gab ein temporäres Problem.", - "You don't have permission to delete the address.": "Du hast nicht die Berechtigung die Adresse zu löschen.", + "You don't have permission to delete the address.": "Du hast nicht die Berechtigung, die Adresse zu löschen.", "Error removing address": "Fehler beim Löschen der Adresse", "Categories": "Kategorien", "Room address": "Raumadresse", @@ -2227,7 +2227,7 @@ "Use a different passphrase?": "Eine andere Passphrase verwenden?", "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Deine Server-Administration hat die Ende-zu-Ende-Verschlüsselung für private Räume und Direktnachrichten standardmäßig deaktiviert.", "People": "Personen", - "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Beim Entfernen dieser Adresse ist ein Fehler aufgetreten. Vielleicht existiert diese nicht mehr oder es kam zu einem temporären Fehler.", + "There was an error removing that address. It may no longer exist or a temporary error occurred.": "Beim Entfernen dieser Adresse ist ein Fehler aufgetreten. Vielleicht existiert sie nicht mehr oder es kam zu einem temporären Fehler.", "Set a room address to easily share your room with other people.": "Vergebe eine Raum-Adresse, um diesen Raum auf einfache Weise mit anderen Personen teilen zu können.", "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "Du hast für diese Sitzung zuvor eine neuere Version von %(brand)s verwendet. Um diese Version mit Ende-zu-Ende-Verschlüsselung wieder zu benutzen, musst du dich erst ab- und dann wieder anmelden.", "Delete the room address %(alias)s and remove %(name)s from the directory?": "Soll die Raum-Adresse %(alias)s gelöscht und %(name)s aus dem Raum-Verzeichnis entfernt werden?", @@ -2245,9 +2245,9 @@ "Show": "Zeige", "Message preview": "Nachrichtenvorschau", "List options": "Optionen anzeigen", - "Show %(count)s more|other": "Zeige %(count)s weitere", - "Show %(count)s more|one": "Zeige %(count)s weitere", - "Leave Room": "Verlasse Raum", + "Show %(count)s more|other": "%(count)s weitere anzeigen", + "Show %(count)s more|one": "%(count)s weitere anzeigen", + "Leave Room": "Raum verlassen", "Room options": "Raumoptionen", "Activity": "Aktivität", "A-Z": "A-Z", @@ -2349,15 +2349,15 @@ "Show rooms with unread messages first": "Räume mit ungelesenen Nachrichten zuerst zeigen", "Show previews of messages": "Nachrichtenvorschau anzeigen", "Use default": "Standardeinstellungen benutzen", - "Mentions & Keywords": "Erwähnungen & Schlüsselwörter", + "Mentions & Keywords": "Erwähnungen und Schlüsselwörter", "Notification options": "Benachrichtigungsoptionen", "Forget Room": "Raum vergessen", "Favourited": "Favorisiert", "This room is public": "Dieser Raum ist öffentlich", "Away": "Abwesend", - "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.": "Die Sitzung, die du verifizieren möchtest, unterstützt weder das scannen eines QR-Codes noch eine Emoji-Verifikation, welche von %(brand)s unterstützt werden. Versuche es mit einer anderen Anwendung.", + "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.": "Die Sitzung, die du verifizieren möchtest, unterstützt weder das scannen eines QR-Codes noch eine Emoji-Verifikation, welche von %(brand)s unterstützt werden. Versuche es mit einem anderen Client.", "Edited at %(date)s": "Geändert am %(date)s", - "Click to view edits": "Klicke um Änderungen anzuzeigen", + "Click to view edits": "Klicke, um Änderungen anzuzeigen", "%(brand)s encountered an error during upload of:": "%(brand)s hat einen Fehler festgestellt beim hochladen von:", "Use your account to sign in to the latest version of the app at ": "Verwende dein Konto um dich an der neusten Version der App anzumelden", "We’re excited to announce Riot is now Element!": "Wir freuen uns bekanntzugeben: Riot ist jetzt Element!", @@ -2460,7 +2460,7 @@ "Prepends ( ͡° ͜ʖ ͡°) to a plain-text message": "Stellt ( ͡° ͜ʖ ͡°) einer Klartextnachricht voran", "Unknown App": "Unbekannte App", "%(count)s results|one": "%(count)s Ergebnis", - "Room Info": "Raum-Info", + "Room Info": "Rauminfo", "Apps": "Apps", "Unpin app": "App nicht mehr anheften", "Edit apps, bridges & bots": "Apps, Bridges & Bots bearbeiten", @@ -2507,8 +2507,8 @@ "Unable to set up keys": "Schlüssel können nicht eingerichtet werden", "Use the Desktop app to see all encrypted files": "Nutze die Desktop-App um alle verschlüsselten Dateien zu sehen", "Use the Desktop app to search encrypted messages": "Nutze die Desktop-App um verschlüsselte Nachrichten zu suchen", - "This version of %(brand)s does not support viewing some encrypted files": "Diese Version von %(brand)s unterstützt nicht alle verschlüsselten Dateien anzuzeigen", - "This version of %(brand)s does not support searching encrypted messages": "Diese Version von %(brand)s unterstützt nicht verschlüsselte Nachrichten zu durchsuchen", + "This version of %(brand)s does not support viewing some encrypted files": "Diese Version von %(brand)s kann nicht alle verschlüsselten Dateien anzuzeigen", + "This version of %(brand)s does not support searching encrypted messages": "Diese Version von %(brand)s kann verschlüsselte Nachrichten nicht durchsuchen", "Cannot create rooms in this community": "Räume können in dieser Community nicht erstellt werden", "You do not have permission to create rooms in this community.": "Du bist nicht berechtigt Räume in dieser Community zu erstellen.", "End conference": "Konferenzgespräch beenden", @@ -2530,7 +2530,7 @@ "Move left": "Nach links schieben", "Revoke permissions": "Berechtigungen widerrufen", "Unpin a widget to view it in this panel": "Widget nicht mehr anheften, um es in diesem Bereich anzuzeigen", - "You can only pin up to %(count)s widgets|other": "Du kannst nur bis zu %(count)s Widgets anheften", + "You can only pin up to %(count)s widgets|other": "Du kannst nur %(count)s Widgets anheften", "Show Widgets": "Widgets anzeigen", "Hide Widgets": "Widgets verstecken", "%(senderName)s declined the call.": "%(senderName)s hat den Anruf abgelehnt.", @@ -2621,19 +2621,19 @@ "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 einer von euch lädt jemanden neues ein.", - "This is the beginning of your direct message history with .": "Dies ist der Beginn deiner Direktnachrichtenhistorie mit .", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Nur ihr zwei seid in dieser Konversation, außer ihr lädt jemanden neues ein.", + "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 ", - "Add a topic to help people know what it is about.": "Füge ein Thema hinzu um Personen zu verdeutlichen um was es in ihm geht.", + "Add a topic to help people know what it is about.": "Füge ein Thema hinzu, um Personen zu verdeutlichen um was es in ihm geht.", "You created this room.": "Du hast diesen Raum erstellt.", - "%(displayName)s created this room.": "%(displayName)s erstellte diesen Raum.", + "%(displayName)s created this room.": "%(displayName)s hat diesen Raum erstellt.", "Add a photo, so people can easily spot your room.": "Füge ein Foto hinzu, sodass Personen deinen Raum einfach finden können.", "This is the start of .": "Dies ist der Beginn von .", "Start a new chat": "Starte einen neuen Chat", "Role": "Rolle", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Nachrichten hier sind Ende-zu-Ende-verschlüsselt. Verifiziere %(displayName)s im deren Profil - klicke auf deren Avatar.", - "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. Wenn Personen beitreten, kannst du sie in ihrem Profil verifizieren, klicke hierfür auf deren Avatar.", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Nachrichten in diesem Raum sind Ende-zu-Ende-verschlüsselt. Wenn Personen beitreten, kannst du sie in ihrem Profil verifizieren, indem du auf deren Avatar klickst.", "Tell us below how you feel about %(brand)s so far.": "Erzähle uns wie %(brand)s dir soweit gefällt.", "Please go into as much detail as you like, so we can track down the problem.": "Bitte nenne so viele Details wie du möchtest, sodass wir das Problem finden können.", "Comment": "Kommentar", @@ -3023,7 +3023,7 @@ "Access your secure message history and set up secure messaging by entering your Security Key.": "Greife auf deinen sicheren Chatverlauf zu und richte die sichere Nachrichtenübermittlung ein, indem du deinen Sicherheitsschlüssel eingibst.", "If you've forgotten your Security Phrase you can use your Security Key or set up new recovery options": "Wenn du deine Sicherheitsphrase vergessen hast, kannst du deinen Sicherheitsschlüssel nutzen oder neue Wiederherstellungsoptionen einrichten", "Security Key mismatch": "Nicht übereinstimmende Sicherheitsschlüssel", - "Set my room layout for everyone": "Setze mein Raum-Layout für alle", + "Set my room layout for everyone": "Dieses Raum-Layout 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)", "Remember this": "Dies merken", @@ -3045,7 +3045,7 @@ "We couldn't log you in": "Wir konnten dich nicht anmelden", "Windows": "Fenster", "Screens": "Bildschirme", - "Share your screen": "Deinen Bildschirm teilen", + "Share your screen": "Bildschirm teilen", "Recently visited rooms": "Kürzlich besuchte Räume", "Show line numbers in code blocks": "Zeilennummern in Code-Blöcken anzeigen", "Expand code blocks by default": "Code-Blöcke standardmäßig erweitern", @@ -3112,5 +3112,9 @@ "You're already in a call with this person.": "Du bist schon in einem Anruf mit dieser Person.", "Already in call": "Schon im Anruf", "Invite people": "Personen einladen", - "Jump to the bottom of the timeline when you send a message": "Nach dem Senden einer Nachricht im Chatverlauf nach unten scrollen" + "Jump to the bottom of the timeline when you send a message": "Nach dem Senden einer Nachricht im Chatverlauf nach unten scrollen", + "Empty room": "Leerer Raum", + "Your message was sent": "Die Nachricht wurde gesendet", + "Encrypting your message...": "Nachricht wird verschlüsselt...", + "Sending your message...": "Nachricht wird gesendet..." } From 60efd3c4c09269426772d04df127ed2de18020d0 Mon Sep 17 00:00:00 2001 From: "@a2sc:matrix.org" Date: Fri, 19 Mar 2021 16:51:01 +0000 Subject: [PATCH 126/183] Translated using Weblate (German) Currently translated at 96.7% (2794 of 2889 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 22f27823a6..174c60170f 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1800,7 +1800,7 @@ "Strikethrough": "Durchgestrichen", "Code block": "Code-Block", "Recent rooms": "Letzte Räume", - "Loading …": "Lade …", + "Loading …": "Laden …", "Join the conversation with an account": "Unterhaltung mit einem Konto beitreten", "You were kicked from %(roomName)s by %(memberName)s": "Du wurdest von %(memberName)s aus %(roomName)s entfernt", "Re-join": "Wieder beitreten", From 90c3ca3a2e6cfb93bc4eee0b3ea7e0bc3f08967c Mon Sep 17 00:00:00 2001 From: rkfg Date: Fri, 19 Mar 2021 11:36:03 +0000 Subject: [PATCH 127/183] Translated using Weblate (Russian) Currently translated at 95.6% (2763 of 2889 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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 7cbcb08167..e7c8c2b4d7 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -96,7 +96,7 @@ "Failure to create room": "Не удалось создать комнату", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "для %(userId)s с %(fromPowerLevel)s на %(toPowerLevel)s", "click to reveal": "нажмите для открытия", - "%(senderName)s invited %(targetName)s.": "%(senderName)s пригласил %(targetName)s.", + "%(senderName)s invited %(targetName)s.": "%(senderName)s пригласил(а) %(targetName)s.", "%(targetName)s joined the room.": "%(targetName)s вошёл в комнату.", "%(senderName)s kicked %(targetName)s.": "%(senderName)s исключил(а) %(targetName)s.", "%(targetName)s left the room.": "%(targetName)s покинул(а) комнату.", @@ -2613,7 +2613,7 @@ "Unable to validate homeserver": "Невозможно проверить домашний сервер", "Sign into your homeserver": "Войдите на свой домашний сервер", "with state key %(stateKey)s": "с ключом состояния %(stateKey)s", - "%(creator)s created this DM.": "%(creator)s начал этот чат.", + "%(creator)s created this DM.": "%(creator)s начал(а) этот чат.", "Show chat effects": "Показать эффекты чата", "Host account on": "Ваша учётная запись обслуживается", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s или %(usernamePassword)s", From 90b8a6647689e52cd183db91866cfc02ce810a8e Mon Sep 17 00:00:00 2001 From: Tawfiek Date: Sun, 21 Mar 2021 14:11:45 +0200 Subject: [PATCH 128/183] fix make btns in verify dailog respect system font --- res/css/_common.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/_common.scss b/res/css/_common.scss index 36a81e6651..0093bde0ab 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -395,6 +395,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { border: 1px solid $accent-color; color: $accent-color; background-color: $button-secondary-bg-color; + font-family: inherit; } .mx_Dialog button:last-child { From f5911e6446a430003243ed359664b6f5a7d8e6ec Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Mon, 22 Mar 2021 00:33:09 +0530 Subject: [PATCH 129/183] Fixing the minor UI issues in the email discovery --- src/components/views/settings/discovery/EmailAddresses.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/settings/discovery/EmailAddresses.js b/src/components/views/settings/discovery/EmailAddresses.js index 0493597537..b8d91aae2a 100644 --- a/src/components/views/settings/discovery/EmailAddresses.js +++ b/src/components/views/settings/discovery/EmailAddresses.js @@ -203,6 +203,7 @@ export class EmailAddress extends React.Component { className="mx_ExistingEmailAddress_confirmBtn" kind="primary_sm" onClick={this.onContinueClick} + disabled={this.state.continueDisabled} > {_t("Complete")} From 497caf5645874d5614eb43d220dc8a18eda6149f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Mar 2021 13:22:16 +0000 Subject: [PATCH 130/183] Fix redaction event list summaries breaking sender profiles --- src/components/structures/MessagePanel.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 63f23f22f3..0a9af413d8 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -1044,6 +1044,10 @@ class RedactionGrouper { } shouldGroup(ev) { + // absorb hidden events so that they do not break up streams of messages & redaction events being grouped + if (!this.panel._shouldShowEvent(ev)) { + return true; + } if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) { return false; } @@ -1055,6 +1059,9 @@ class RedactionGrouper { ev.getId(), ev === this.lastShownEvent, ); + if (!this.panel._shouldShowEvent(ev)) { + return; + } this.events.push(ev); } @@ -1080,13 +1087,9 @@ class RedactionGrouper { ); const senders = new Set(); - let eventTiles = this.events.map((e) => { + let eventTiles = this.events.map((e, i) => { senders.add(e.sender); - // In order to prevent DateSeparators from appearing in the expanded form, - // 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(i === 0 ? this.prevEvent : this.events[i - 1], e, e === lastShownEvent); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { From cfbcf12e1d6ffe5fa078976182cd847faf4c2530 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 22 Mar 2021 15:02:28 +0000 Subject: [PATCH 131/183] docs: update file extensions in CIDER editor documentation --- docs/ciderEditor.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ciderEditor.md b/docs/ciderEditor.md index f522dc2fc4..379b6f5b51 100644 --- a/docs/ciderEditor.md +++ b/docs/ciderEditor.md @@ -21,14 +21,14 @@ caret nodes (more on that later). For these reasons it doesn't use `innerText`, `textContent` or anything similar. The model addresses any content in the editor within as an offset within this string. The caret position is thus also converted from a position in the DOM tree -to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.js`. +to an offset in the content string. This happens in `getCaretOffsetAndText` in `dom.ts`. Once the content string and caret offset is calculated, it is passed to the `update()` method of the model. The model first calculates the same content string of its current parts, basically just concatenating their text. It then looks for differences between the current and the new content string. The diffing algorithm is very basic, and assumes there is only one change around the caret offset, -so this should be very inexpensive. See `diff.js` for details. +so this should be very inexpensive. See `diff.ts` for details. The result of the diffing is the strings that were added and/or removed from the current content. These differences are then applied to the parts, @@ -51,7 +51,7 @@ which relate poorly to text input or changes, and don't need the `beforeinput` e which isn't broadly supported yet. Once the parts of the model are updated, the DOM of the editor is then reconciled -with the new model state, see `renderModel` in `render.js` for this. +with the new model state, see `renderModel` in `render.ts` for this. If the model didn't reject the input and didn't make any additional changes, this won't make any changes to the DOM at all, and should thus be fairly efficient. From 0db31dfeaeb656418c5f2690eb73a3b13ded285c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 22 Mar 2021 15:05:22 +0000 Subject: [PATCH 132/183] fix: CIDER formatting buttons on Safari --- res/css/views/rooms/_MessageComposerFormatBar.scss | 2 ++ src/components/views/rooms/MessageComposerFormatBar.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_MessageComposerFormatBar.scss b/res/css/views/rooms/_MessageComposerFormatBar.scss index d97c49630a..b305e91db0 100644 --- a/res/css/views/rooms/_MessageComposerFormatBar.scss +++ b/res/css/views/rooms/_MessageComposerFormatBar.scss @@ -60,6 +60,8 @@ limitations under the License. width: 27px; height: 24px; box-sizing: border-box; + background: none; + vertical-align: middle; } .mx_MessageComposerFormatBar_button::after { diff --git a/src/components/views/rooms/MessageComposerFormatBar.js b/src/components/views/rooms/MessageComposerFormatBar.js index d2539b1ef4..fc0f785b08 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.js +++ b/src/components/views/rooms/MessageComposerFormatBar.js @@ -85,8 +85,8 @@ class FormatButton extends React.PureComponent { return ( Date: Mon, 22 Mar 2021 17:46:50 +0000 Subject: [PATCH 133/183] Fix space hierarchy exploding when encountering an empty subspace --- src/components/structures/SpaceRoomDirectory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 0a53f38238..ab273887c2 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -260,7 +260,7 @@ 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 sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null); const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const roomId = ev.state_key; if (!rooms.has(roomId)) return result; From 796bfd851de39878ae8d082a9a3eb72f3d53cd1a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 22 Mar 2021 17:47:48 +0000 Subject: [PATCH 134/183] Fix left spaces not disappearing from the space panel --- src/stores/SpaceStore.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index b82acfd0ed..dba3f1d8a9 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -196,13 +196,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }; public rebuild = throttle(() => { // exported for tests - const visibleRooms = this.matrixClient.getVisibleRooms(); + // get all most-upgraded rooms & spaces except spaces which have been left (historical) + const visibleRooms = this.matrixClient.getVisibleRooms().filter(r => { + return !r.isSpaceRoom() || r.getMyMembership() === "join"; + }); + + const unseenChildren = new Set(visibleRooms); + const backrefs = new EnhancedMap>(); // Sort spaces by room ID to force the loop breaking to be deterministic - const spaces = sortBy(this.getSpaces(), space => space.roomId); - const unseenChildren = new Set([...visibleRooms, ...spaces]); - - const backrefs = new EnhancedMap>(); + const spaces = sortBy(visibleRooms.filter(r => r.isSpaceRoom()), space => space.roomId); // TODO handle cleaning up links when a Space is removed spaces.forEach(space => { From d5b115dd084c94fcfe301ee15ca8cdc9269eb84f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 22 Mar 2021 19:52:09 -0400 Subject: [PATCH 135/183] don't overwrite callback with undefined if no customization provided --- src/MatrixClientPeg.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index de1d573d40..7db5ed1a4e 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -296,10 +296,11 @@ class _MatrixClientPeg implements IMatrixClientPeg { // These are always installed regardless of the labs flag so that // cross-signing features can toggle on without reloading and also be // accessed immediately after login. - const customisedCallbacks = { - getDehydrationKey: SecurityCustomisations.getDehydrationKey, - }; - Object.assign(opts.cryptoCallbacks, crossSigningCallbacks, customisedCallbacks); + Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); + if (SecurityCustomisations.getDehydrationKey) { + opts.cryptoCallbacks.getDehydrationKey = + SecurityCustomisations.getDehydrationKey; + } this.matrixClient = createMatrixClient(opts); From e352ed19085c76522a5963e89594cd09bfb18c9a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Mar 2021 19:32:24 -0600 Subject: [PATCH 136/183] Run audio through the Web Audio API instead This leads to more reliable frequency/timing information, and involves a whole lot less decoding. We still maintain ongoing encoded frames to avoid having to do one giant encode at the end, as that could take long enough to be disruptive. --- .../views/rooms/VoiceRecordComposerTile.tsx | 4 +- src/voice/VoiceRecorder.ts | 101 +++++++++++++----- 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index a1d0e8c12f..7327bf2380 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -58,8 +58,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent { - // console.log('@@ FRAME', frame); + // recorder.frequencyData.onUpdate((freq) => { + // console.log('@@ UPDATE', freq); // }); this.setState({recorder}); }; diff --git a/src/voice/VoiceRecorder.ts b/src/voice/VoiceRecorder.ts index 3d1008d45e..5646b53cbe 100644 --- a/src/voice/VoiceRecorder.ts +++ b/src/voice/VoiceRecorder.ts @@ -20,40 +20,74 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; import CallMediaHandler from "../CallMediaHandler"; import {SimpleObservable} from "matrix-widget-api"; +const CHANNELS = 1; // stereo isn't important +const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. +const BITRATE = 64000; // 64kbps is average for WebRTC, so we might as well use it too. +const FREQ_SAMPLE_RATE = 4; // Target rate of frequency data. We don't need this super often. + +export interface IFrequencyPackage { + dbBars: Float32Array; + dbMin: number; + dbMax: number; + + // TODO: @@ TravisR: Generalize this for a timing package? +} + export class VoiceRecorder { - private recorder = new Recorder({ - encoderPath, // magic from webpack - mediaTrackConstraints: { - deviceId: CallMediaHandler.getAudioInput(), - }, - encoderSampleRate: 48000, // we could go down to 12khz, but we lose quality. 48khz is a webrtc default - encoderApplication: 2048, // voice (default is "audio") - streamPages: true, // so we can have a live EQ for the user - encoderFrameSize: 20, // ms, we want updates fairly regularly for the UI - numberOfChannels: 1, // stereo isn't important for us - //sourceNode: instanceof MediaStreamAudioSourceNode, // TODO: @@ Travis: Use this for EQ stuff. - encoderBitRate: 64000, // 64kbps is average for webrtc - encoderComplexity: 3, // 0-10, 0 is fast and low complexity - resampleQuality: 3, // 0-10, 10 is slow and high quality - }); + private recorder: Recorder; + private recorderContext: AudioContext; + private recorderSource: MediaStreamAudioSourceNode; + private recorderStream: MediaStream; + private recorderFreqNode: AnalyserNode; private buffer = new Uint8Array(0); private mxc: string; private recording = false; - private observable: SimpleObservable; + private observable: SimpleObservable; + private freqTimerId: number; public constructor(private client: MatrixClient) { + } + + private async makeRecorder() { + this.recorderStream = await navigator.mediaDevices.getUserMedia({ + audio: { + // specify some audio settings so we're feeding the recorder with the + // best possible values. The browser will handle resampling for us. + sampleRate: SAMPLE_RATE, + channelCount: CHANNELS, + noiseSuppression: true, // browsers ignore constraints they can't honour + deviceId: CallMediaHandler.getAudioInput(), + }, + }); + this.recorderContext = new AudioContext({ + latencyHint: "interactive", + sampleRate: SAMPLE_RATE, // once again, the browser will resample for us + }); + this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream); + this.recorderFreqNode = this.recorderContext.createAnalyser(); + this.recorderSource.connect(this.recorderFreqNode); + this.recorder = new Recorder({ + encoderPath, // magic from webpack + encoderSampleRate: SAMPLE_RATE, + encoderApplication: 2048, // voice (default is "audio") + streamPages: true, // this speeds up the encoding process by using CPU over time + encoderFrameSize: 20, // ms, arbitrary frame size we send to the encoder + numberOfChannels: CHANNELS, + sourceNode: this.recorderSource, + encoderBitRate: BITRATE, + encoderComplexity: 3, // 0-10, 0 is fast and low complexity + resampleQuality: 3, // 0-10, 10 is slow and high quality + }); this.recorder.ondataavailable = (a: ArrayBuffer) => { - // TODO: @@ TravisR: We'll have to decode each frame and convert it to an EQ to observe const buf = new Uint8Array(a); const newBuf = new Uint8Array(this.buffer.length + buf.length); newBuf.set(this.buffer, 0); newBuf.set(buf, this.buffer.length); this.buffer = newBuf; - this.observable.update(buf); // send the frame over the observable }; } - public get rawData(): SimpleObservable { + public get frequencyData(): SimpleObservable { if (!this.recording) throw new Error("No observable when not recording"); return this.observable; } @@ -83,7 +117,18 @@ export class VoiceRecorder { if (this.observable) { this.observable.close(); } - this.observable = new SimpleObservable(); + this.observable = new SimpleObservable(); + await this.makeRecorder(); + this.freqTimerId = setInterval(() => { + if (!this.recording) return; + const data = new Float32Array(this.recorderFreqNode.frequencyBinCount); + this.recorderFreqNode.getFloatFrequencyData(data); + this.observable.update({ + dbBars: data, + dbMin: this.recorderFreqNode.minDecibels, + dbMax: this.recorderFreqNode.maxDecibels, + }); + }, 1000 / FREQ_SAMPLE_RATE) as any as number; // XXX: Linter doesn't understand timer environment return this.recorder.start().then(() => this.recording = true); } @@ -91,12 +136,20 @@ export class VoiceRecorder { if (!this.recording) { throw new Error("No recording to stop"); } - return new Promise(resolve => { - this.recorder.stop().then(() => { + // Disconnect the source early to start shutting down resources + this.recorderSource.disconnect(); + return 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) + .then(() => this.recorderContext.close()) + // Now stop all the media tracks so we can release them back to the user/OS + .then(() => this.recorderStream.getTracks().forEach(t => t.stop())) + // Finally do our post-processing and clean up + .then(() => { + clearInterval(this.freqTimerId); this.recording = false; return this.recorder.close(); - }).then(() => resolve(this.buffer)); - }); + }).then(() => this.buffer); } public async upload(): Promise { From 090cf28af497678ce7ebaa132b01112228faa5a2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Mar 2021 19:36:58 -0600 Subject: [PATCH 137/183] Appease the linter --- src/voice/VoiceRecorder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/voice/VoiceRecorder.ts b/src/voice/VoiceRecorder.ts index 5646b53cbe..ec1a745272 100644 --- a/src/voice/VoiceRecorder.ts +++ b/src/voice/VoiceRecorder.ts @@ -146,7 +146,7 @@ export class VoiceRecorder { .then(() => this.recorderStream.getTracks().forEach(t => t.stop())) // Finally do our post-processing and clean up .then(() => { - clearInterval(this.freqTimerId); + clearInterval(this.freqTimerId); this.recording = false; return this.recorder.close(); }).then(() => this.buffer); From 026aa6f88d76bc404b0b2789b9599bbf0af59f59 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Mar 2021 21:39:07 -0600 Subject: [PATCH 138/183] Track next event [tile] over group boundaries Fixes https://github.com/vector-im/element-web/issues/16745 --- src/components/structures/MessagePanel.js | 38 +++++++++++++++-------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0a9af413d8..6951aff357 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -452,6 +452,20 @@ export default class MessagePanel extends React.Component { }); }; + _getNextEventInfo(arr, i) { + const nextEvent = i < arr.length - 1 + ? arr[i + 1] + : null; + + // The next event with tile is used to to determine the 'last successful' flag + // when rendering the tile. The shouldShowEvent function is pretty quick at what + // it does, so this should have no significant cost even when a room is used for + // not-chat purposes. + const nextTile = arr.slice(i + 1).find(e => this._shouldShowEvent(e)); + + return {nextEvent, nextTile}; + } + _getEventTiles() { this.eventNodes = {}; @@ -503,6 +517,7 @@ export default class MessagePanel extends React.Component { const mxEv = this.props.events[i]; const eventId = mxEv.getId(); const last = (mxEv === lastShownEvent); + const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i); if (grouper) { if (grouper.shouldGroup(mxEv)) { @@ -519,21 +534,13 @@ export default class MessagePanel extends React.Component { for (const Grouper of groupers) { if (Grouper.canStartGroup(this, mxEv)) { - grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent); + grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile); } } if (!grouper) { const wantTile = this._shouldShowEvent(mxEv); if (wantTile) { - const nextEvent = i < this.props.events.length - 1 - ? this.props.events[i + 1] - : null; - - // The next event with tile is used to to determine the 'last successful' flag - // when rendering the tile. The shouldShowEvent function is pretty quick at what - // it does, so this should have no significant cost even when a room is used for - // not-chat purposes. - const nextTile = this.props.events.slice(i + 1).find(e => this._shouldShowEvent(e)); + const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i); // make sure we unpack the array returned by _getTilesForEvent, // otherwise react will auto-generate keys and we will end up @@ -982,7 +989,7 @@ class CreationGrouper { )); } - const eventTiles = this.events.map((e) => { + const eventTiles = this.events.map((e, i) => { // In order to prevent DateSeparators from appearing in the expanded form // of EventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the @@ -1032,7 +1039,7 @@ class RedactionGrouper { return panel._shouldShowEvent(ev) && ev.isRedacted(); } - constructor(panel, ev, prevEvent, lastShownEvent) { + constructor(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile) { this.panel = panel; this.readMarker = panel._readMarkerForEvent( ev.getId(), @@ -1041,6 +1048,8 @@ class RedactionGrouper { this.events = [ev]; this.prevEvent = prevEvent; this.lastShownEvent = lastShownEvent; + this.nextEvent = nextEvent; + this.nextEventTile = nextEventTile; } shouldGroup(ev) { @@ -1089,7 +1098,10 @@ class RedactionGrouper { const senders = new Set(); let eventTiles = this.events.map((e, i) => { senders.add(e.sender); - return panel._getTilesForEvent(i === 0 ? this.prevEvent : this.events[i - 1], e, e === lastShownEvent); + return panel._getTilesForEvent( + i === 0 ? this.prevEvent : this.events[i - 1], + e, e === lastShownEvent, + this.nextEvent, this.nextEventTile); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { From fa54ca615a48cda4ff64357bb12b87b6b6871cce Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 22 Mar 2021 21:41:13 -0600 Subject: [PATCH 139/183] Appease the linter --- 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 6951aff357..671d895a21 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -989,7 +989,7 @@ class CreationGrouper { )); } - const eventTiles = this.events.map((e, i) => { + const eventTiles = this.events.map((e) => { // In order to prevent DateSeparators from appearing in the expanded form // of EventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the @@ -1098,10 +1098,8 @@ class RedactionGrouper { const senders = new Set(); let eventTiles = this.events.map((e, i) => { senders.add(e.sender); - return panel._getTilesForEvent( - i === 0 ? this.prevEvent : this.events[i - 1], - e, e === lastShownEvent, - this.nextEvent, this.nextEventTile); + const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1]; + return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { From 1f6f9ca9838b830f33a8e364c21e405f97eda434 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 23 Mar 2021 18:24:05 +0000 Subject: [PATCH 140/183] Only show the ask anyway modal for explicit user lookup failures --- src/utils/MultiInviter.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index 63d3942b37..05fa06ee14 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -111,17 +111,10 @@ export default class MultiInviter { } if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) { - try { - const profile = await MatrixClientPeg.get().getProfileInfo(addr); - if (!profile) { - // noinspection ExceptionCaughtLocallyJS - throw new Error("User has no profile"); - } - } catch (e) { - throw { - errcode: "RIOT.USER_NOT_FOUND", - error: "User does not have a profile or does not exist." - }; + const profile = await MatrixClientPeg.get().getProfileInfo(addr); + if (!profile) { + // noinspection ExceptionCaughtLocallyJS + throw new Error("User has no profile"); } } From b8692bdf175ce27e78bfb27102ef61445b947503 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 23 Mar 2021 18:25:03 +0000 Subject: [PATCH 141/183] Prevent state to be toggled whilst a request is pending --- src/components/views/dialogs/InviteDialog.tsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index de0b5b237b..4ea53349bd 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -886,19 +886,21 @@ export default class InviteDialog extends React.PureComponent { - let filterText = this.state.filterText; - const targets = this.state.targets.map(t => t); // cheap clone for mutation - const idx = targets.indexOf(member); - if (idx >= 0) { - targets.splice(idx, 1); - } else { - targets.push(member); - filterText = ""; // clear the filter when the user accepts a suggestion - } - this.setState({targets, filterText}); + if (!this.state.busy) { + let filterText = this.state.filterText; + const targets = this.state.targets.map(t => t); // cheap clone for mutation + const idx = targets.indexOf(member); + if (idx >= 0) { + targets.splice(idx, 1); + } else { + targets.push(member); + filterText = ""; // clear the filter when the user accepts a suggestion + } + this.setState({targets, filterText}); - if (this._editorRef && this._editorRef.current) { - this._editorRef.current.focus(); + if (this._editorRef && this._editorRef.current) { + this._editorRef.current.focus(); + } } }; From 2f2bb9456ff2757e752fa3f8910706b8ebfac14c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Mar 2021 18:17:41 -0600 Subject: [PATCH 142/183] Reduce code duplication --- src/components/structures/MessagePanel.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 671d895a21..6d03c849c4 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -540,8 +540,6 @@ export default class MessagePanel extends React.Component { if (!grouper) { const wantTile = this._shouldShowEvent(mxEv); if (wantTile) { - const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i); - // make sure we unpack the array returned by _getTilesForEvent, // otherwise react will auto-generate keys and we will end up // replacing all of the DOM elements every time we paginate. From b7e653268b6c8e41fd22db365813b586beba1fbb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Mar 2021 18:19:14 -0600 Subject: [PATCH 143/183] Rename function --- src/components/views/rooms/VoiceRecordComposerTile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 7327bf2380..0d381001a1 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -40,7 +40,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent { + private onStartStopVoiceMessage = async () => { // TODO: @@ TravisR: We do not want to auto-send on stop. if (this.state.recorder) { await this.state.recorder.stop(); @@ -80,7 +80,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent ); From c9938ff704765414ad119e0ea42822d96eefc23f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Mar 2021 18:24:40 -0600 Subject: [PATCH 144/183] Adjust settings/docs for encoder --- src/voice/VoiceRecorder.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/voice/VoiceRecorder.ts b/src/voice/VoiceRecorder.ts index ec1a745272..9fa2faad1e 100644 --- a/src/voice/VoiceRecorder.ts +++ b/src/voice/VoiceRecorder.ts @@ -22,8 +22,8 @@ import {SimpleObservable} from "matrix-widget-api"; const CHANNELS = 1; // stereo isn't important const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality. -const BITRATE = 64000; // 64kbps is average for WebRTC, so we might as well use it too. -const FREQ_SAMPLE_RATE = 4; // Target rate of frequency data. We don't need this super often. +const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus. +const FREQ_SAMPLE_RATE = 4; // Target rate of frequency data (samples / sec). We don't need this super often. export interface IFrequencyPackage { dbBars: Float32Array; @@ -75,7 +75,11 @@ export class VoiceRecorder { numberOfChannels: CHANNELS, sourceNode: this.recorderSource, encoderBitRate: BITRATE, - encoderComplexity: 3, // 0-10, 0 is fast and low complexity + + // We use low values for the following to ease CPU usage - the resulting waveform + // is indistinguishable for a voice message. Note that the underlying library will + // pick defaults which prefer the highest possible quality, CPU be damned. + encoderComplexity: 3, // 0-10, 10 is slow and high quality. resampleQuality: 3, // 0-10, 10 is slow and high quality }); this.recorder.ondataavailable = (a: ArrayBuffer) => { From d929d4839154927c415110ea89db90160fe934dc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 23 Mar 2021 18:26:43 -0600 Subject: [PATCH 145/183] Clean up promises --- src/voice/VoiceRecorder.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/voice/VoiceRecorder.ts b/src/voice/VoiceRecorder.ts index 9fa2faad1e..06c0d939fc 100644 --- a/src/voice/VoiceRecorder.ts +++ b/src/voice/VoiceRecorder.ts @@ -133,27 +133,32 @@ export class VoiceRecorder { dbMax: this.recorderFreqNode.maxDecibels, }); }, 1000 / FREQ_SAMPLE_RATE) as any as number; // XXX: Linter doesn't understand timer environment - return this.recorder.start().then(() => this.recording = true); + await this.recorder.start(); + this.recording = true; } public async stop(): Promise { if (!this.recording) { throw new Error("No recording to stop"); } + // Disconnect the source early to start shutting down resources this.recorderSource.disconnect(); - return 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) - .then(() => this.recorderContext.close()) - // Now stop all the media tracks so we can release them back to the user/OS - .then(() => this.recorderStream.getTracks().forEach(t => t.stop())) - // Finally do our post-processing and clean up - .then(() => { - clearInterval(this.freqTimerId); - this.recording = false; - return this.recorder.close(); - }).then(() => this.buffer); + await this.recorder.stop(); + + // close the context after the recorder so the recorder doesn't try to + // connect anything to the context (this would generate a warning) + await this.recorderContext.close(); + + // Now stop all the media tracks so we can release them back to the user/OS + this.recorderStream.getTracks().forEach(t => t.stop()); + + // Finally do our post-processing and clean up + clearInterval(this.freqTimerId); + this.recording = false; + await this.recorder.close(); + + return this.buffer; } public async upload(): Promise { From d836ca19d86fc65c4bea99a50de37f0958b036af Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 24 Mar 2021 08:58:08 +0000 Subject: [PATCH 146/183] remove references to disused RIOT.USER_NOT_FOUND error code --- src/utils/MultiInviter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.js index 05fa06ee14..3d3a5f4137 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.js @@ -164,7 +164,7 @@ export default class MultiInviter { this._doInvite(address, ignoreProfile).then(resolve, reject); }, 5000); return; - } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'RIOT.USER_NOT_FOUND'].includes(err.errcode)) { + } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) { errorText = _t("User %(user_id)s does not exist", {user_id: address}); } else if (err.errcode === 'M_PROFILE_UNDISCLOSED') { errorText = _t("User %(user_id)s may or may not exist", {user_id: address}); @@ -205,7 +205,7 @@ export default class MultiInviter { if (Object.keys(this.errors).length > 0 && !this.groupId) { // There were problems inviting some people - see if we can invite them // without caring if they exist or not. - const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND', 'RIOT.USER_NOT_FOUND']; + const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND']; const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode)); if (unknownProfileUsers.length > 0) { From 5104d7bed85f744f3cd6d72635712eed0e23b983 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 24 Mar 2021 11:51:39 +0000 Subject: [PATCH 147/183] Improve error reporting when EventIndex fails on a supported environment --- .../views/settings/EventIndexPanel.js | 19 ++++++++++++++++++- src/i18n/strings/en_EN.json | 1 + src/indexing/EventIndexPeg.js | 2 ++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.js index d78b99fc5d..a48583b61d 100644 --- a/src/components/views/settings/EventIndexPanel.js +++ b/src/components/views/settings/EventIndexPanel.js @@ -190,7 +190,7 @@ export default class EventIndexPanel extends React.Component { }
    ); - } else { + } else if (!EventIndexPeg.platformHasSupport()) { eventIndexingSettings = (
    { @@ -208,6 +208,23 @@ export default class EventIndexPanel extends React.Component { }
    ); + } else { + eventIndexingSettings = ( +
    +

    + {_t("Message search initilisation failed")} +

    + {EventIndexPeg.error && ( +
    + {_t("Advanced")} + + {EventIndexPeg.error.message} + +
    + )} + +
    + ); } return eventIndexingSettings; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5f1003bf29..f0d7922836 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1081,6 +1081,7 @@ "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", "%(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)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.", + "Message search initilisation failed": "Message search initilisation failed", "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", diff --git a/src/indexing/EventIndexPeg.js b/src/indexing/EventIndexPeg.js index 443daa8f43..7004efc554 100644 --- a/src/indexing/EventIndexPeg.js +++ b/src/indexing/EventIndexPeg.js @@ -31,6 +31,7 @@ class EventIndexPeg { constructor() { this.index = null; this._supportIsInstalled = false; + this.error = null; } /** @@ -96,6 +97,7 @@ class EventIndexPeg { await index.init(); } catch (e) { console.log("EventIndex: Error initializing the event index", e); + this.error = e; return false; } From fb46815b6a5bbc2b17286f67b3fef70af53111bc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 13:15:31 +0000 Subject: [PATCH 148/183] Spaces move away from Form Buttons --- res/css/structures/_SpaceRoomView.scss | 2 +- .../views/dialogs/_SpaceSettingsDialog.scss | 2 +- res/css/views/spaces/_SpaceCreateMenu.scss | 2 +- src/components/structures/SpaceRoomView.tsx | 41 ++++++++++++------- .../dialogs/AddExistingToSpaceDialog.tsx | 9 ++-- .../views/dialogs/SpaceSettingsDialog.tsx | 12 +++--- .../views/spaces/SpaceCreateMenu.tsx | 9 ++-- 7 files changed, 44 insertions(+), 33 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index a7ce630b96..080773b49b 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -89,7 +89,7 @@ $SpaceRoomViewInnerWidth: 428px; width: $SpaceRoomViewInnerWidth; text-align: right; // button alignment right - .mx_FormButton { + .mx_AccessibleButton_hasKind { padding: 8px 22px; margin-left: 16px; } diff --git a/res/css/views/dialogs/_SpaceSettingsDialog.scss b/res/css/views/dialogs/_SpaceSettingsDialog.scss index c1fa539e9b..6e5fd9c8c8 100644 --- a/res/css/views/dialogs/_SpaceSettingsDialog.scss +++ b/res/css/views/dialogs/_SpaceSettingsDialog.scss @@ -49,7 +49,7 @@ limitations under the License. } } - .mx_FormButton { + .mx_AccessibleButton_hasKind { padding: 8px 22px; } } diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss index bea39e2389..ef3fea351b 100644 --- a/res/css/views/spaces/_SpaceCreateMenu.scss +++ b/res/css/views/spaces/_SpaceCreateMenu.scss @@ -79,7 +79,7 @@ $spacePanelWidth: 71px; } } - .mx_FormButton { + .mx_AccessibleButton_kind_primary { padding: 8px 22px; margin-left: auto; display: block; diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 95846d8e21..46ff37dc14 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -26,7 +26,6 @@ import AccessibleButton from "../views/elements/AccessibleButton"; import RoomName from "../views/elements/RoomName"; import RoomTopic from "../views/elements/RoomTopic"; import InlineSpinner from "../views/elements/InlineSpinner"; -import FormButton from "../views/elements/FormButton"; import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite"; import {useRoomMembers} from "../../hooks/useRoomMembers"; import createRoom, {IOpts, Preset} from "../../createRoom"; @@ -124,30 +123,36 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => } joinButtons = <> - { setBusy(true); onRejectButtonClicked(); - }} /> - + { _t("Reject") } + + { setBusy(true); onJoinButtonClicked(); }} - /> + > + { _t("Accept") } + ; } else { joinButtons = ( - { setBusy(true); onJoinButtonClicked(); }} - /> + > + { _t("Join") } + ) } @@ -407,11 +412,13 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { { fields }
    - + > + { buttonLabel } +
    ; }; @@ -426,7 +433,9 @@ const SpaceSetupPublicShare = ({ space, onFinished }) => {
    - + + { _t("Go to my first room") } +
    ; }; @@ -545,7 +554,9 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
    - + + { buttonLabel } +
    ; }; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 500637244a..e3e28e4fbe 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -22,7 +22,6 @@ import {MatrixClient} from "matrix-js-sdk/src/client"; import {_t} from '../../../languageHandler'; import {IDialogProps} from "./IDialogProps"; import BaseDialog from "./BaseDialog"; -import FormButton from "../elements/FormButton"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; import SpaceStore from "../../../stores/SpaceStore"; @@ -185,8 +184,8 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, - { setBusy(true); @@ -200,7 +199,9 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, } setBusy(false); }} - /> + > + { busy ? _t("Adding...") : _t("Add") } +
    ; }; diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx index f6bf5b87e6..b016e320eb 100644 --- a/src/components/views/dialogs/SpaceSettingsDialog.tsx +++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx @@ -28,7 +28,6 @@ import {getTopic} from "../elements/RoomTopic"; import {avatarUrlForRoom} from "../../../Avatar"; import ToggleSwitch from "../elements/ToggleSwitch"; import AccessibleButton from "../elements/AccessibleButton"; -import FormButton from "../elements/FormButton"; import Modal from "../../../Modal"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {allSettled} from "../../../utils/promise"; @@ -134,16 +133,17 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin />
    - { defaultDispatcher.dispatch({ action: "leave_room", room_id: space.roomId, }); }} - /> + > + { _t("Leave Space") } +
    Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}> @@ -152,7 +152,9 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin { _t("Cancel") } - + + { busy ? _t("Saving...") : _t("Save Changes") } +
    ; diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 879cf929e0..6269de1c50 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -21,7 +21,6 @@ import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types import {_t} from "../../../languageHandler"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {ChevronFace, ContextMenu} from "../../structures/ContextMenu"; -import FormButton from "../elements/FormButton"; import createRoom, {IStateEvent, Preset} from "../../../createRoom"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import SpaceBasicSettings from "./SpaceBasicSettings"; @@ -148,11 +147,9 @@ const SpaceCreateMenu = ({ onFinished }) => { - + + { busy ? _t("Creating...") : _t("Create") } + ; } From 4e9a2df3b0689e11dfb18241a5c8a86a6d6b4a2f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 14:00:52 +0000 Subject: [PATCH 149/183] Spaces autofocus and prefill the search box --- src/components/structures/SearchBox.js | 5 ++++- src/components/structures/SpaceRoomDirectory.tsx | 2 ++ src/components/views/dialogs/AddExistingToSpaceDialog.tsx | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 6daa8526bc..abeb858274 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -32,6 +32,8 @@ export default class SearchBox extends React.Component { onKeyDown: PropTypes.func, className: PropTypes.string, placeholder: PropTypes.string.isRequired, + autoFocus: PropTypes.bool, + initialValue: PropTypes.string, // If true, the search box will focus and clear itself // on room search focus action (it would be nicer to take @@ -49,7 +51,7 @@ export default class SearchBox extends React.Component { this._search = createRef(); this.state = { - searchTerm: "", + searchTerm: this.props.initialValue || "", blurred: true, }; } @@ -158,6 +160,7 @@ export default class SearchBox extends React.Component { onBlur={this._onBlur} placeholder={ placeholder } autoComplete="off" + autoFocus={this.props.autoFocus} /> { clearButton }
    diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index ab273887c2..2fb0101f88 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -570,6 +570,8 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis className="mx_textinput_icon mx_textinput_search" placeholder={ _t("Search names and description") } onSearch={setQuery} + autoFocus={true} + initialValue={initialText} /> { content } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index e3e28e4fbe..fec1a178ca 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -127,6 +127,7 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, className="mx_textinput_icon mx_textinput_search" placeholder={ _t("Filter your rooms and spaces") } onSearch={setQuery} + autoComplete={true} /> { spaces.length > 0 ? ( From d8737913693551141841100554527e7515423e8f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 14:01:24 +0000 Subject: [PATCH 150/183] update comments --- src/stores/SpaceStore.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index dba3f1d8a9..e4b537169e 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -195,7 +195,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); }; - public rebuild = throttle(() => { // exported for tests + private rebuild = throttle(() => { // get all most-upgraded rooms & spaces except spaces which have been left (historical) const visibleRooms = this.matrixClient.getVisibleRooms().filter(r => { return !r.isSpaceRoom() || r.getMyMembership() === "join"; @@ -204,7 +204,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const unseenChildren = new Set(visibleRooms); const backrefs = new EnhancedMap>(); - // Sort spaces by room ID to force the loop breaking to be deterministic + // Sort spaces by room ID to force the cycle breaking to be deterministic const spaces = sortBy(visibleRooms.filter(r => r.isSpaceRoom()), space => space.roomId); // TODO handle cleaning up links when a Space is removed @@ -219,7 +219,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren)); - // untested algorithm to handle full-cycles + // somewhat algorithm to handle full-cycles const detachedNodes = new Set(spaces); const markTreeChildren = (rootSpace: Room, unseen: Set) => { From f7a3805eed1b987d644f81cbdcd3d3c14fc32709 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 14:02:25 +0000 Subject: [PATCH 151/183] Fix styling inconsistency in space room view --- res/css/structures/_SpaceRoomView.scss | 2 +- src/components/structures/SpaceRoomView.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 080773b49b..3d3b5d1bb8 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -22,7 +22,7 @@ $SpaceRoomViewInnerWidth: 428px; width: 432px; box-sizing: border-box; border-radius: 8px; - border: 1px solid $input-darker-bg-color; + border: 1px solid $space-button-outline-color; font-size: $font-15px; margin: 20px 0; diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 46ff37dc14..0c2f1638d1 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -426,7 +426,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { const SpaceSetupPublicShare = ({ space, onFinished }) => { return

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

    -
    +
    { _t("It's just you at the moment, it will be even better with others.") }
    From 6e0ab8616866e0d37a9998837e847e131cab3cd3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 14:10:26 +0000 Subject: [PATCH 152/183] Small usability tweaks to the add existing to space dialog --- .../dialogs/_AddExistingToSpaceDialog.scss | 25 ++++++++--- .../dialogs/AddExistingToSpaceDialog.tsx | 44 +++++++++---------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 0c9d8e3840..a7cfd7bde6 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -28,22 +28,23 @@ limitations under the License. flex-direction: column; flex-wrap: nowrap; min-height: 0; + height: 80vh; .mx_Dialog_title { display: flex; - .mx_BaseAvatar { - display: inline-flex; - margin: 5px 16px 5px 5px; - vertical-align: middle; - } - .mx_BaseAvatar_image { border-radius: 8px; margin: 0; vertical-align: unset; } + .mx_BaseAvatar { + display: inline-flex; + margin: 5px 16px 5px 5px; + vertical-align: middle; + } + > div { > h1 { font-weight: $font-semi-bold; @@ -101,6 +102,7 @@ limitations under the License. .mx_SearchBox { margin: 0; + flex-grow: 0; } .mx_AddExistingToSpaceDialog_errorText { @@ -112,7 +114,10 @@ limitations under the License. } .mx_AddExistingToSpaceDialog_content { + flex-grow: 1; + .mx_AddExistingToSpaceDialog_noResults { + display: block; margin-top: 24px; } } @@ -162,8 +167,14 @@ limitations under the License. > span { flex-grow: 1; - font-size: $font-12px; + font-size: $font-14px; line-height: $font-15px; + font-weight: $font-semi-bold; + + .mx_AccessibleButton { + font-size: inherit; + display: inline-block; + } > * { vertical-align: middle; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index fec1a178ca..04bec39238 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -109,7 +109,7 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, const title =
    -

    { _t("Add existing spaces/rooms") }

    +

    { _t("Add existing rooms") }

    { spaceOptionSection }
    ; @@ -130,27 +130,6 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, autoComplete={true} /> - { spaces.length > 0 ? ( -
    -

    { _t("Spaces") }

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

    { _t("Rooms") }

    @@ -172,6 +151,27 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space,
    ) : undefined } + { spaces.length > 0 ? ( +
    +

    { _t("Spaces") }

    + { spaces.map(space => { + return { + if (checked) { + selectedToAdd.add(space); + } else { + selectedToAdd.delete(space); + } + setSelectedToAdd(new Set(selectedToAdd)); + }} + />; + }) } +
    + ) : null } + { spaces.length + rooms.length < 1 ? { _t("No results") } : undefined } From a2a1e37fa3cb3011508e93043ce507b6a0b64249 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 14:18:06 +0000 Subject: [PATCH 153/183] Add prompt to bottom of room list to invite to space --- res/css/views/rooms/_RoomList.scss | 24 +++++++--- res/img/element-icons/roomlist/browse.svg | 4 ++ src/components/views/rooms/RoomList.tsx | 55 +++++++++++++++++++++-- 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 res/img/element-icons/roomlist/browse.svg diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index d49ed4b736..641b434af4 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -35,28 +35,32 @@ limitations under the License. margin: 4px 12px 4px; padding-top: 12px; border-top: 1px solid $tertiary-fg-color; - font-size: $font-13px; + font-size: $font-14px; div:first-child { font-weight: $font-semi-bold; + line-height: $font-18px; + color: $primary-fg-color; } .mx_AccessibleButton { - color: $secondary-fg-color; + color: $primary-fg-color; position: relative; - padding: 0 0 0 24px; + padding: 8px 8px 8px 32px; font-size: inherit; - margin-top: 8px; + margin-top: 12px; display: block; text-align: start; + background-color: $roomlist-button-bg-color; + border-radius: 4px; &::before { content: ''; width: 16px; height: 16px; position: absolute; - top: 0; - left: 0; + top: 8px; + left: 8px; background: $secondary-fg-color; mask-position: center; mask-size: contain; @@ -70,5 +74,13 @@ limitations under the License. &.mx_RoomList_explorePrompt_explore::before { mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); } + + &.mx_RoomList_explorePrompt_spaceInvite::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + + &.mx_RoomList_explorePrompt_spaceExplore::before { + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); + } } } diff --git a/res/img/element-icons/roomlist/browse.svg b/res/img/element-icons/roomlist/browse.svg new file mode 100644 index 0000000000..04714e2881 --- /dev/null +++ b/res/img/element-icons/roomlist/browse.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 4378154d8f..01affc8b2f 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -20,6 +20,7 @@ import React, { ReactComponentElement } from "react"; import { Dispatcher } from "flux"; import { Room } from "matrix-js-sdk/src/models/room"; import * as fbEmitter from "fbemitter"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t, _td } from "../../../languageHandler"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; @@ -48,12 +49,15 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con import AccessibleButton from "../elements/AccessibleButton"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import CallHandler from "../../../CallHandler"; -import SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore"; +import SpaceStore, {SUGGESTED_ROOMS, UPDATE_SELECTED_SPACE} from "../../../stores/SpaceStore"; import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space"; -import { EventType } from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import RoomAvatar from "../avatars/RoomAvatar"; import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory"; +import { showRoomInviteDialog } from "../../../RoomInvite"; +import Modal from "../../../Modal"; +import SpacePublicShare from "../spaces/SpacePublicShare"; +import InfoDialog from "../dialogs/InfoDialog"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -68,6 +72,7 @@ interface IState { sublists: ITagMap; isNameFiltering: boolean; currentRoomId?: string; + activeSpace: Room; suggestedRooms: ISpaceSummaryRoom[]; } @@ -194,7 +199,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = { : _t("You do not have permissions to add rooms to this space")} /> { e.preventDefault(); @@ -282,6 +287,7 @@ export default class RoomList extends React.PureComponent { this.state = { sublists: {}, isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), + activeSpace: SpaceStore.instance.activeSpace, suggestedRooms: SpaceStore.instance.suggestedRooms, }; @@ -294,6 +300,7 @@ export default class RoomList extends React.PureComponent { } public componentDidMount(): void { + SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); SpaceStore.instance.on(SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists); @@ -301,6 +308,7 @@ export default class RoomList extends React.PureComponent { } public componentWillUnmount() { + SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); SpaceStore.instance.off(SUGGESTED_ROOMS, this.updateSuggestedRooms); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); defaultDispatcher.unregister(this.dispatcherRef); @@ -366,6 +374,10 @@ export default class RoomList extends React.PureComponent { return room; }; + private updateActiveSpace = (activeSpace: Room) => { + this.setState({ activeSpace }); + }; + private updateSuggestedRooms = (suggestedRooms: ISpaceSummaryRoom[]) => { this.setState({ suggestedRooms }); }; @@ -424,6 +436,25 @@ export default class RoomList extends React.PureComponent { dis.dispatch({ action: Action.ViewRoomDirectory, initialText }); }; + private onSpaceInviteClick = () => { + const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search; + if (this.state.activeSpace.getJoinRule() === "public") { + const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { + title: _t("Invite to %(spaceName)s", { spaceName: this.state.activeSpace.name }), + description: + { _t("Share your public space") } + modal.close()} /> + , + fixedWidth: false, + button: false, + className: "mx_SpacePanel_sharePublicSpace", + hasCloseButton: true, + }); + } else { + showRoomInviteDialog(this.state.activeSpace.roomId, initialText); + } + }; + private renderSuggestedRooms(): ReactComponentElement[] { return this.state.suggestedRooms.map(room => { const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"); @@ -569,7 +600,23 @@ export default class RoomList extends React.PureComponent { kind="link" onClick={this.onExplore} > - {_t("Explore all public rooms")} + { this.state.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") } + +
    ; + } else if (this.state.activeSpace) { + explorePrompt =
    +
    { _t("Quick actions") }
    + { this.state.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && + {_t("Invite people")} + } + + {_t("Explore rooms")}
    ; } else if (Object.values(this.state.sublists).some(list => list.length > 0)) { From 3df3baea14f931251b1a76cc995a52f0fb2ea7bf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 14:19:10 +0000 Subject: [PATCH 154/183] Tweak behaviour during space creation --- src/components/structures/SpaceRoomView.tsx | 2 +- src/components/views/spaces/SpacePublicShare.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 0c2f1638d1..16028f0975 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -430,7 +430,7 @@ const SpaceSetupPublicShare = ({ space, onFinished }) => { { _t("It's just you at the moment, it will be even better with others.") }
    - +
    diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx index b2d3b7ce29..fa81b75525 100644 --- a/src/components/views/spaces/SpacePublicShare.tsx +++ b/src/components/views/spaces/SpacePublicShare.tsx @@ -26,7 +26,7 @@ import {showRoomInviteDialog} from "../../../RoomInvite"; interface IProps { space: Room; - onFinished(): void; + onFinished?(): void; } const SpacePublicShare = ({ space, onFinished }: IProps) => { @@ -54,7 +54,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => { className="mx_SpacePublicShare_inviteButton" onClick={() => { showRoomInviteDialog(space.roomId); - onFinished(); + if (onFinished) onFinished(); }} >

    { _t("Invite people") }

    From ea760e8f296cfbcf0b8acc172c7a3b16e8a54adf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 15:26:56 +0000 Subject: [PATCH 155/183] Fix space room directory behaviour --- src/components/structures/SpaceRoomDirectory.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 2fb0101f88..877a4283f1 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -77,7 +77,6 @@ export interface ISpaceSummaryEvent { interface ITileProps { room: ISpaceSummaryRoom; - editing?: boolean; suggested?: boolean; selected?: boolean; numChildRooms?: number; @@ -88,7 +87,6 @@ interface ITileProps { const Tile: React.FC = ({ room, - editing, suggested, selected, hasPermissions, @@ -170,12 +168,6 @@ const Tile: React.FC = ({
    ; - if (editing) { - return
    - { content } -
    - } - let childToggle; let childSection; if (children) { @@ -201,7 +193,7 @@ const Tile: React.FC = ({ className={classNames("mx_SpaceRoomDirectory_roomTile", { mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space, })} - onClick={hasPermissions ? onToggleClick : onPreviewClick} + onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick} > { content } { childToggle } From 65a7d0621d5b14b340a9647049cc85b350ba7381 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 15:30:03 +0000 Subject: [PATCH 156/183] Add invite to space button to room intro --- res/css/views/rooms/_NewRoomIntro.scss | 7 +++- src/components/views/rooms/NewRoomIntro.tsx | 46 +++++++++++++++++---- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss index 4322ba341c..9c2a428cb3 100644 --- a/res/css/views/rooms/_NewRoomIntro.scss +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -33,8 +33,13 @@ limitations under the License. .mx_AccessibleButton { line-height: $font-24px; + display: inline-block; - &::before { + & + .mx_AccessibleButton { + margin-left: 12px; + } + + &:not(.mx_AccessibleButton_kind_primary_outline)::before { content: ''; display: inline-block; background-color: $button-fg-color; diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index ce426a64ed..c85b9d7868 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -28,6 +28,7 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; import {Action} from "../../../dispatcher/actions"; import dis from "../../../dispatcher/dispatcher"; +import SpaceStore from "../../../stores/SpaceStore"; const NewRoomIntro = () => { const cli = useContext(MatrixClientContext); @@ -100,17 +101,48 @@ const NewRoomIntro = () => { }); } - let buttons; - if (room.canInvite(cli.getUserId())) { - const onInviteClick = () => { - dis.dispatch({ action: "view_invite", roomId }); - }; + let parentSpace; + if ( + SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) && + SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId) + ) { + parentSpace = SpaceStore.instance.activeSpace; + } + let buttons; + if (parentSpace) { buttons =
    - + { + dis.dispatch({ action: "view_invite", roomId }); + }} + > + {_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })} + + { room.canInvite(cli.getUserId()) && { + dis.dispatch({ action: "view_invite", roomId }); + }} + > + {_t("Invite to just this room")} + } +
    ; + } else if (room.canInvite(cli.getUserId())) { + buttons =
    + { + dis.dispatch({ action: "view_invite", roomId }); + }} + > {_t("Invite to this room")} -
    +
    ; } const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url; From 11fbd081f146bce1ce53c07a064300afacf8036f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 15:30:36 +0000 Subject: [PATCH 157/183] Iterate space panel context menu --- .../views/spaces/SpaceTreeLevel.tsx | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 83bc2296e7..1b86bb7898 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -30,7 +30,12 @@ import IconizedContextMenu, { import {_t} from "../../../languageHandler"; import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import {toRightOf} from "../../structures/ContextMenu"; -import {shouldShowSpaceSettings, showCreateNewRoom, showSpaceSettings} from "../../../utils/space"; +import { + shouldShowSpaceSettings, + showAddExistingRooms, + showCreateNewRoom, + showSpaceSettings, +} from "../../../utils/space"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {ButtonEvent} from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; @@ -127,7 +132,7 @@ export class SpaceItem extends React.PureComponent { if (this.props.space.getJoinRule() === "public") { const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, { - title: _t("Invite members"), + title: _t("Invite to %(spaceName)s", { spaceName: this.props.space.name }), description: { _t("Share your public space") } modal.close()} /> @@ -170,6 +175,14 @@ export class SpaceItem extends React.PureComponent { this.setState({contextMenuPosition: null}); // also close the menu }; + private onAddExistingRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + showAddExistingRooms(this.context, this.props.space); + this.setState({contextMenuPosition: null}); // also close the menu + }; + private onMembersClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -236,15 +249,20 @@ export class SpaceItem extends React.PureComponent { ; } - let newRoomOption; + let newRoomSection; if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { - newRoomOption = ( + newRoomSection = - ); + + ; } contextMenu = { label={_t("Explore rooms")} onClick={this.onExploreRoomsClick} /> - { newRoomOption } + { newRoomSection } { leaveSection } ; } From 31dd224cc98c4fc7d44ac1296abe338593ca0751 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 15:36:20 +0000 Subject: [PATCH 158/183] Wire up passing through initialText for room invite dialog helper method --- src/RoomInvite.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 9ae41b851a..aa758ecbdc 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -49,11 +49,12 @@ export function showStartChatInviteDialog(initialText) { ); } -export function showRoomInviteDialog(roomId) { +export function showRoomInviteDialog(roomId, initialText = "") { // This dialog handles the room creation internally - we don't need to worry about it. Modal.createTrackedDialog( "Invite Users", "", InviteDialog, { kind: KIND_INVITE, + initialText, roomId, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, From d7484607165c32103f5f344d1fea05116c874a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Mesk=C3=B3?= Date: Tue, 23 Mar 2021 17:02:21 +0000 Subject: [PATCH 159/183] Translated using Weblate (Hungarian) Currently translated at 100.0% (2889 of 2889 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 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index e5901ef9a3..ccc871d097 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -1953,7 +1953,7 @@ "Accepting…": "Elfogadás…", "Accepting …": "Elfogadás …", "Declining …": "Elutasítás …", - "Verification Requests": "Hitelesítés Kérések", + "Verification Requests": "Hitelesítéskérések", "Your account is not secure": "A fiókod nem biztonságos", "Your password": "A jelszavad", "This session, or the other session": "Ez vagy másik munkamenet", @@ -3079,9 +3079,9 @@ "Setting:": "Beállítás:", "Value in this room": "Érték ebben a szobában", "Value": "Érték", - "Setting ID": "Beállítás azon.", - "Failed to save settings": "A beállítások elmentése nem sikerült", - "Settings Explorer": "Beállítás Böngésző", + "Setting ID": "Beállításazonosító", + "Failed to save settings": "A beállítások mentése sikertelen", + "Settings Explorer": "Beállításböngésző", "Show chat effects (animations when receiving e.g. confetti)": "Csevegés effektek megjelenítése (mint a konfetti animáció)", "Original event source": "Eredeti esemény forráskód", "Decrypted event source": "Visszafejtett esemény forráskód", From efa57e5121e87c26e757eaf15cdf2b8139c791d7 Mon Sep 17 00:00:00 2001 From: Marcelo Filho Date: Tue, 23 Mar 2021 19:37:38 +0000 Subject: [PATCH 160/183] Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.1% (2865 of 2889 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pt_BR/ --- src/i18n/strings/pt_BR.json | 75 ++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index f054fa31e6..0ec835362a 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -3037,5 +3037,78 @@ "Delete": "Excluir", "This homeserver has been blocked by it's administrator.": "Este servidor local foi bloqueado pelo seu administrador.", "This homeserver has been blocked by its administrator.": "Este servidor local foi bloqueado pelo seu administrador.", - "You're already in a call with this person.": "Você já está em uma chamada com essa pessoa." + "You're already in a call with this person.": "Você já está em uma chamada com essa pessoa.", + "At the moment only you can see it.": "No momento, só você pode ver.", + "Failed to create initial space rooms": "Falha ao criar salas de espaço iniciais", + "Your private space ": "Seu espaço privado ", + "Your public space ": "Seu espaço público ", + "You have been invited to ": "Você foi convidado para ", + " invited you to ": " convidou você para ", + "%(count)s members|one": "%(count)s integrante", + "%(count)s members|other": "%(count)s integrantes", + "Add existing rooms & spaces": "Adicionar salas & espaços já existentes", + "Accept Invite": "Aceitar o convite", + "Save changes": "Salvar alterações", + "You're in this room": "Você está nesta sala", + "You're in this space": "Você está neste espaço", + "No permissions": "Sem permissões", + "Undo": "Desfazer", + "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.": "A sua mensagem não foi enviada porque este servidor local foi bloqueado pelo seu administrador. Entre em contato com o administrador do serviço para continuar usando o serviço.", + "Are you sure you want to leave the space '%(spaceName)s'?": "Tem certeza de que deseja sair desse espaço '%(spaceName)s'?", + "This space is not public. You will not be able to rejoin without an invite.": "Este espaço não é público. Você não poderá entrar novamente sem um convite.", + "Save Changes": "Salvar alterações", + "Saving...": "Salvando...", + "View dev tools": "Ver ferramentas de desenvolvimento", + "Leave space": "Sair desse espaço", + "Leave Space": "Sair desse espaço", + "Make this space private": "Tornar este espaço privado", + "Edit settings relating to your space.": "Editar configurações relacionadas ao seu espaço.", + "Space settings": "Configurações desse espaço", + "Failed to save space settings.": "Falha ao salvar as configurações desse espaço.", + "Invite someone using their name, username (like ) or share this space.": "Convide alguém a partir do nome, nome de usuário (como ) ou compartilhe este espaço.", + "Invite someone using their name, email address, username (like ) or share this space.": "Convide alguém a partir do nome, endereço de e-mail, nome de usuário (como ) ou compartilhe este espaço.", + "Unnamed Space": "Espaço sem nome", + "Invite to %(spaceName)s": "Convidar para %(spaceName)s", + "Failed to add rooms to space": "Falha ao adicionar salas ao espaço", + "Create a new room": "Criar uma nova sala", + "Don't want to add an existing room?": "Não deseja adicionar uma sala já existente?", + "Spaces": "Espaços", + "Filter your rooms and spaces": "Pesquisar suas salas e espaços", + "Add existing spaces/rooms": "Adicionar espaços/salas já existentes", + "Explore space rooms": "Explorar as salas deste espaço", + "You do not have permissions to add rooms to this space": "Você não tem permissão para adicionar salas neste espaço", + "You do not have permissions to create new rooms in this space": "Você não tem permissão para criar novas salas neste espaço", + "Invite to this space": "Convidar para este espaço", + "Your message was sent": "A sua mensagem foi enviada", + "Encrypting your message...": "Criptografando a sua mensagem...", + "Sending your message...": "Enviando a sua mensagem...", + "Spell check dictionaries": "Dicionários de verificação ortográfica", + "Space options": "Opções do espaço", + "New room": "Nova sala", + "Invite people": "Convidar pessoas", + "Share your public space": "Compartilhar o seu espaço público", + "Invite members": "Convidar integrantes", + "Invite by email or username": "Convidar por e-mail ou nome de usuário", + "Share invite link": "Compartilhar link de convite", + "Click to copy": "Clique para copiar", + "Collapse space panel": "Fechar o painel do espaço", + "Expand space panel": "Expandir o painel do espaço", + "You can change these at any point.": "Você pode alterar esses dados a qualquer momento.", + "Give it a photo, name and description to help you identify it.": "Insira uma foto, nome e descrição para ajudar a identificar o espaço.", + "Your private space": "O seu espaço privado", + "Your public space": "O seu espaço público", + "You can change this later": "Você pode mudar isso depois", + "Open space for anyone, best for communities": "Abra espaços para todos, especialmente para comunidades", + "Spaces are new ways to group rooms and people. To join an existing space you’ll need an invite": "Espaços são novas formas de agrupar salas e pessoas. Para entrar em um espaço existente, você precisará de um convite", + "Create a space": "Criar um espaço", + "Jump to the bottom of the timeline when you send a message": "Vá para o final da linha do tempo ao enviar uma mensagem", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Protótipo de Espaços. Incompatível com Comunidades, Comunidades v2 e tags personalizadas. Requer um servidor compatível com os recursos necessários.", + "Decrypted event source": "Fonte de evento descriptografada", + "We'll create rooms for each of them. You can add existing rooms after setup.": "Criaremos salas para cada um deles. Você pode adicionar salas já existentes após a configuração.", + "What projects are you working on?": "Em quais projetos você trabalha no momento?", + "We'll create rooms for each topic.": "Nós criaremos salas para cada tópico.", + "Inviting...": "Convidando...", + "Invite by username": "Convidar por nome de usuário", + "Support": "Suporte", + "Original event source": "Fonte do evento original" } From 3a401a31518473f8585edcecb26498d2442a0d7f Mon Sep 17 00:00:00 2001 From: Nikita Epifanov Date: Wed, 24 Mar 2021 05:23:15 +0000 Subject: [PATCH 161/183] Translated using Weblate (Russian) Currently translated at 95.6% (2763 of 2889 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index e7c8c2b4d7..4cf444ac0e 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -2064,7 +2064,7 @@ "You changed your avatar": "Вы поменяли свой аватар", "%(targetName)s changed their avatar": "%(targetName)s поменял(а) свой аватар", "You changed the room name": "Вы поменяли имя комнаты", - "Enable experimental, compact IRC style layout": "Включите экспериментальный, компактный стиль IRC", + "Enable experimental, compact IRC style layout": "Включить экспериментальный, компактный стиль IRC", "Unknown caller": "Неизвестный абонент", "Incoming call": "Входящий звонок", "Waiting for your other session to verify…": "Ожидание вашей другой сессии для начала подтверждения…", From 3ec45eda6a783000a5e927412e8c90a5c7fdebfd Mon Sep 17 00:00:00 2001 From: RainSlide Date: Wed, 24 Mar 2021 14:08:40 +0000 Subject: [PATCH 162/183] Translated using Weblate (Chinese (Simplified)) Currently translated at 86.4% (2497 of 2889 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ --- src/i18n/strings/zh_Hans.json | 243 ++++++++++++++++++++++++++++++---- 1 file changed, 214 insertions(+), 29 deletions(-) diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index a22e600865..d07d6c83b6 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -166,7 +166,7 @@ "New passwords don't match": "两次输入的新密码不符", "not specified": "未指定", "Notifications": "通知", - "(not supported by this browser)": "(未被此浏览器支持)", + "(not supported by this browser)": "(此浏览器不支持)", "": "<不支持>", "No display name": "无昵称", "No results": "没有更多结果", @@ -910,10 +910,10 @@ "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s 将此聊天室对知道此聊天室链接的人公开。", "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s 将此聊天室改为仅限邀请。", "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s 将加入规则改为 %(rule)s", - "%(displayName)s is typing …": "%(displayName)s 正在打字…", - "%(names)s and %(count)s others are typing …|other": "%(names)s 与其他 %(count)s 位正在打字…", - "%(names)s and %(count)s others are typing …|one": "%(names)s 与另一位正在打字…", - "%(names)s and %(lastPerson)s are typing …": "%(names)s 和 %(lastPerson)s正在打字…", + "%(displayName)s is typing …": "%(displayName)s 正在输入…", + "%(names)s and %(count)s others are typing …|other": "%(names)s 与其他 %(count)s 位正在输入…", + "%(names)s and %(count)s others are typing …|one": "%(names)s 与另一位正在输入…", + "%(names)s and %(lastPerson)s are typing …": "%(names)s 和 %(lastPerson)s 正在输入…", "Unrecognised address": "无法识别地址", "User %(user_id)s may or may not exist": "用户 %(user_id)s 不一定存在", "Predictable substitutions like '@' instead of 'a' don't help very much": "可预见的替换如将 '@' 替换为 'a' 并不会有太大效果", @@ -942,7 +942,7 @@ "Short keyboard patterns are easy to guess": "键位短序列很容易被猜到", "Group & filter rooms by custom tags (refresh to apply changes)": "按自定义标签分组和过滤聊天室(刷新以应用更改)", "Render simple counters in room header": "在聊天室标题中显示简单计数", - "Enable Emoji suggestions while typing": "键入时启用表情符号建议", + "Enable Emoji suggestions while typing": "启用实时表情符号建议", "Show a placeholder for removed messages": "已移除的消息显示为一个占位符", "Show join/leave messages (invites/kicks/bans unaffected)": "显示 加入/离开 信息(邀请/踢出/禁止 不受影响)", "Show avatar changes": "显示头像更改", @@ -951,7 +951,7 @@ "Show a reminder to enable Secure Message Recovery in encrypted rooms": "在加密聊天室中显示一条允许恢复安全消息的提醒", "Show avatars in user and room mentions": "在用户和聊天室提及中显示头像", "Enable big emoji in chat": "在聊天中启用大型表情符号", - "Send typing notifications": "发送键入状态通知", + "Send typing notifications": "发送正在输入通知", "Enable Community Filter Panel": "启用社区筛选器面板", "Allow Peer-to-Peer for 1:1 calls": "允许一对一通话使用 P2P", "Prompt before sending invites to potentially invalid matrix IDs": "在发送邀请之前提示可能无效的 Matrix ID", @@ -1259,14 +1259,14 @@ "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.": "您也可以尝试使用turn.matrix.org公共服务器,但通话质量稍差,并且其将会得知您的 IP。您可以在设置中更改此选项。", "Try using turn.matrix.org": "尝试使用 turn.matrix.org", "Your %(brand)s is misconfigured": "您的 %(brand)s 配置有错误", - "Use Single Sign On to continue": "使用单点登陆继续", - "Confirm adding this email address by using Single Sign On to prove your identity.": "通过使用单点登陆来证明您的身份,并确认添加此邮件地址。", - "Single Sign On": "单点登陆", + "Use Single Sign On to continue": "使用单点登录继续", + "Confirm adding this email address by using Single Sign On to prove your identity.": "通过使用单点登录来证明您的身份,并确认添加此邮件地址。", + "Single Sign On": "单点登录", "Confirm adding email": "确认使用邮件", - "Click the button below to confirm adding this email address.": "点击下面的按钮,添加此邮箱地址。", + "Click the button below to confirm adding this email address.": "点击下面的按钮以确认添加此邮箱地址。", "Confirm adding this phone number by using Single Sign On to prove your identity.": "通过单点登录以证明您的身份,并确认添加此电话号码。", "Confirm adding phone number": "确认添加电话号码", - "Click the button below to confirm adding this phone number.": "点击下面的按钮,确认添加此电话号码。", + "Click the button below to confirm adding this phone number.": "点击下面的按钮以确认添加此电话号码。", "Whether you're using %(brand)s on a device where touch is the primary input mechanism": "是否在触屏设备上使用 %(brand)s", "Whether you're using %(brand)s as an installed Progressive Web App": "您是否已经安装 %(brand)s 作为一种渐进式的 Web 应用", "Your user agent": "您的代理用户", @@ -1282,14 +1282,14 @@ "Verify this session": "验证此会话", "Encryption upgrade available": "提供加密升级", "Set up encryption": "设置加密", - "Review where you’re logged in": "查看您的登陆位置", - "New login. Was this you?": "现在登陆。请问是您本人吗?", - "Name or Matrix ID": "姓名或Matrix账号", + "Review where you’re logged in": "查看您的登录位置", + "New login. Was this you?": "现在登录。请问是您本人吗?", + "Name or Matrix ID": "姓名或 Matrix ID", "Identity server has no terms of service": "身份服务器无服务条款", - "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.": "此操作需要访问默认的身份服务器以验证电子邮件地址或电话号码,但是此服务器无任何服务条款。", + "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.": "此操作需要访问默认的身份服务器 以验证邮箱地址或电话号码,但是此服务器无任何服务条款。", "Only continue if you trust the owner of the server.": "只有您信任服务器所有者才能继续。", "Trust": "信任", - "%(name)s is requesting verification": "%(name)s请求验证", + "%(name)s is requesting verification": "%(name)s 正在请求验证", "Sign In or Create Account": "登录或创建账户", "Use your account or create a new one to continue.": "使用已有账户或创建一个新账户。", "Create Account": "创建账户", @@ -1301,7 +1301,7 @@ "Sends a message as html, without interpreting it as markdown": "以html格式发送消息,而不是markdown", "You do not have the required permissions to use this command.": "您没有权限使用此命令。", "Error upgrading room": "升级聊天室出错", - "Double check that your server supports the room version chosen and try again.": "再次检查您的服务器是否支持所选聊天室版本,然后重试。", + "Double check that your server supports the room version chosen and try again.": "请再次检查您的服务器是否支持所选聊天室版本,然后再试一次。", "Changes the avatar of the current room": "更改当前聊天室头像", "Changes your avatar in this current room only": "仅改变您在当前聊天室的头像", "Changes your avatar in all rooms": "改变您在所有聊天室的头像", @@ -1351,14 +1351,14 @@ "%(senderName)s created a rule banning users matching %(glob)s for %(reason)s": "%(senderName)s 创建了因为%(reason)s而禁止用户匹配%(glob)s的规则", "%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s": "%(senderName)s 创建了由于%(reason)s而禁止聊天室匹配%(glob)s的规则", "%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s": "%(senderName)s 创建了由于%(reason)s而禁止服务器匹配%(glob)s的规则", - "%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s 创建了由于%(reason)s而禁止匹配%(glob)s的股则", + "%(senderName)s created a ban rule matching %(glob)s for %(reason)s": "%(senderName)s 创建了由于%(reason)s而禁止匹配%(glob)s的规则", "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更改了一个由于%(reason)s而禁止用户%(oldGlob)s跟%(newGlob)s匹配的规则", "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s更改了一个由于%(reason)s而禁止聊天室%(oldGlob)s跟%(newGlob)s匹配的规则", "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更新了一个由于%(reason)s而禁止服务器%(oldGlob)s跟%(newGlob)s匹配的规则", "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s": "%(senderName)s 更新了一个由于%(reason)s而禁止%(oldGlob)s跟%(newGlob)s匹配的规则", - "You signed in to a new session without verifying it:": "您登陆了未经过验证的新会话:", + "You signed in to a new session without verifying it:": "您登录了未经过验证的新会话:", "Verify your other session using one of the options below.": "使用以下选项之一验证您的其他会话。", - "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s(%(userId)s)登陆到未验证的新会话:", + "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s(%(userId)s)登录到未验证的新会话:", "Ask this user to verify their session, or manually verify it below.": "要求该用户验证其会话,或在下面手动进行验证。", "Not Trusted": "不可信任", "Manually Verify by Text": "手动验证文字", @@ -1404,7 +1404,7 @@ "about a day from now": "从现在开始约一天", "%(num)s days from now": "从现在开始%(num)s天", "%(name)s (%(userId)s)": "%(name)s%(userId)s", - "Your browser does not support the required cryptography extensions": "您的浏览器不支持必需的加密插件", + "Your browser does not support the required cryptography extensions": "您的浏览器不支持所需的密码学扩展", "The user's homeserver does not support the version of the room.": "用户的主服务器不支持该聊天室版本。", "Help us improve %(brand)s": "请协助我们改进%(brand)s", "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "发送匿名使用情况数据,以协助我们改进%(brand)s。这将使用cookie。", @@ -1666,7 +1666,7 @@ "Enable advanced debugging for the room list": "为此聊天室列表启用高级调试", "Show info about bridges in room settings": "在聊天室设置中显示桥接信息", "Use a more compact ‘Modern’ layout": "使用更紧凑的「现代」布局", - "Show typing notifications": "显示输入通知", + "Show typing notifications": "显示正在输入通知", "Show shortcuts to recently viewed rooms above the room list": "在聊天室列表上方显示最近浏览过的聊天室的快捷方式", "Show hidden events in timeline": "显示时间线中的隐藏事件", "Low bandwidth mode": "低带宽模式", @@ -2416,17 +2416,17 @@ "%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s 为此聊天室设置了服务器 ACL。", "Hong Kong": "香港", "Cook Islands": "库克群岛", - "Congo - Kinshasa": "刚果金", - "Congo - Brazzaville": "刚果布拉柴维尔", + "Congo - Kinshasa": "刚果 - 金沙萨", + "Congo - Brazzaville": "刚果 - 布拉柴维尔", "Comoros": "科摩罗", "Colombia": "哥伦比亚", - "Cocos (Keeling) Islands": "科科斯基林群岛", + "Cocos (Keeling) Islands": "科科斯(基林)群岛", "Christmas Island": "圣诞岛", "China": "中国", "Chile": "智利", "Chad": "乍得", "Central African Republic": "中非共和国", - "Cayman Islands": "开曼群岛(英)", + "Cayman Islands": "开曼群岛", "Caribbean Netherlands": "荷兰加勒比区", "Cape Verde": "佛得角", "Canada": "加拿大", @@ -2472,7 +2472,7 @@ "United States": "美国", "United Kingdom": "英国", "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.": "您的主服务器不可达,无法使您登入。请重试。如果此情况持续发生,请联系您的主服务器管理员。", + "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": "重试", "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.": "我们已要求浏览器记住您使用的主服务器,但不幸的是您的浏览器已忘记。请前往登录页面重试。", "We couldn't log you in": "我们无法使您登入", @@ -2555,5 +2555,190 @@ "Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message": "在纯文字信息前添加 ┬──┬ ノ( ゜-゜ノ)", "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "在纯文字信息前添加 (╯°□°)╯︵ ┻━┻", "You're already in a call with this person.": "您与此人已处在通话中。", - "Already in call": "已在通话中" + "Already in call": "已在通话中", + "Navigate composer history": "浏览编辑区历史", + "Go to Home View": "转到主视图", + "Search (must be enabled)": "搜索(必须启用)", + "Your Security Key": "您的安全密钥", + "Use Security Key": "使用安全密钥", + "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s 或 %(usernamePassword)s", + "User settings": "用户设置", + "Creating rooms...": "正在创建聊天室…", + "Room name": "聊天室名称", + "Random": "随机", + "%(count)s members|one": "%(count)s 位成员", + "%(count)s members|other": "%(count)s 位成员", + "Default Rooms": "默认聊天室", + "Accept Invite": "接受邀请", + "Manage rooms": "管理聊天室", + "Save changes": "保存修改", + "Remove from Space": "从空间中移除", + "Undo": "撤销", + "Welcome %(name)s": "欢迎 %(name)s", + "Create community": "创建社区", + "Forgot password?": "忘记密码?", + "Enter Security Key": "输入安全密钥", + "Invalid Security Key": "安全密钥无效", + "Wrong Security Key": "安全密钥错误", + "Save Changes": "保存修改", + "Saving...": "正在保存…", + "View dev tools": "查看开发者工具", + "Leave Space": "离开空间", + "Space settings": "空间设置", + "Learn more": "了解更多", + "Other homeserver": "其他主服务器", + "Specify a homeserver": "指定主服务器", + "Transfer": "传输", + "Unnamed Space": "未命名空间", + "Cookie Policy": "Cookie 政策", + "Privacy Policy": "隐私政策", + "Abort": "放弃", + "Send feedback": "发送反馈", + "Report a bug": "反馈问题", + "Edit Values": "编辑值", + "Value:": "值:", + "Setting definition:": "设置定义:", + "Caution:": "警告:", + "Setting:": "设置:", + "Value": "值", + "Setting ID": "设置 ID", + "Enter name": "输入名称", + "Community ID: +:%(domain)s": "社区 ID:+:%(domain)s", + "Reason (optional)": "理由(可选)", + "Show": "显示", + "Apply": "应用", + "Applying...": "正在应用…", + "Create a new room": "创建新聊天室", + "Spaces": "空间", + "Continue with %(provider)s": "使用 %(provider)s 继续", + "Homeserver": "主服务器", + "Server Options": "服务器选项", + "Information": "信息", + "Windows": "窗口", + "Screens": "屏幕", + "Share your screen": "共享屏幕", + "Role": "角色", + "Not encrypted": "未加密", + "Unpin": "取消置顶", + "Empty room": "空聊天室", + "Add existing room": "添加现有的聊天室", + "Open dial pad": "打开拨号键盘", + "Start a Conversation": "开始对话", + "Show Widgets": "显示小挂件", + "Hide Widgets": "隐藏小挂件", + "%(displayName)s created this room.": "%(displayName)s 创建了此聊天室。", + "You created this room.": "你创建了此聊天室。", + "Remove messages sent by others": "移除其他人的消息", + "Sending your message...": "正在发送消息…", + "Encrypting your message...": "正在加密消息…", + "Send message": "发送消息", + "Invite to this space": "邀请至此空间", + "Your message was sent": "消息已发送", + "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "请使用您的帐户数据备份加密密钥,以免您无法访问您的会话。密钥将通过一个唯一的安全密钥进行保护。", + "Spell check dictionaries": "拼写检查字典", + "Failed to save your profile": "个人资料保存失败", + "The operation could not be completed": "操作无法完成", + "Space options": "空间选项", + "Space Home": "空间首页", + "New room": "新建聊天室", + "Leave space": "离开空间", + "Share your public space": "分享你的公共空间", + "Collapse space panel": "收起空间面板", + "Expand space panel": "展开空间面板", + "Create a space": "创建空间", + "Fill Screen": "填充屏幕", + "sends snowfall": "发送雪球", + "Sends the given message with snowfall": "附加雪球发送", + "sends confetti": "发送五彩纸屑", + "Sends the given message with confetti": "附加五彩纸屑发送", + "Sends the given message with fireworks": "附加烟火发送", + "sends fireworks": "发送烟火", + "Offline encrypted messaging using dehydrated devices": "需要离线设备(dehydrated devices)的加密消息离线传递", + "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "正在开发的空间功能的原型。与社区、社区 V2 和自定义标签功能不兼容。需要主服务器兼容才能使用某些功能。", + "The %(capability)s capability": "%(capability)s 容量", + "%(senderName)s has updated the widget layout": "%(senderName)s 已更新小挂件布局", + "Support": "支持", + "Your server does not support showing space hierarchies.": "您的服务器不支持显示空间层次结构。", + "This version of %(brand)s does not support searching encrypted messages": "当前版本的 %(brand)s 不支持搜索加密消息", + "This version of %(brand)s does not support viewing some encrypted files": "当前版本的 %(brand)s 不支持查看某些加密文件", + "Effects": "效果", + "Pakistan": "巴基斯坦", + "United Arab Emirates": "阿拉伯联合酋长国", + "Yemen": "也门", + "Ukraine": "乌克兰", + "Turkmenistan": "土库曼斯坦", + "Uganda": "乌干达", + "Turkey": "土耳其", + "U.S. Virgin Islands": "美属维尔京群岛", + "Taiwan": "台湾", + "Sweden": "瑞典", + "Spain": "西班牙", + "South Korea": "韩国", + "South Africa": "南非", + "Russia": "俄罗斯", + "Romania": "罗马尼亚", + "Philippines": "菲律宾", + "North Korea": "朝鲜", + "Norway": "挪威", + "New Zealand": "新西兰", + "Netherlands": "荷兰", + "Mexico": "墨西哥", + "Malaysia": "马来西亚", + "Macau": "澳门", + "Luxembourg": "卢森堡", + "Lebanon": "黎巴嫩", + "Lithuania": "立陶宛", + "Latvia": "拉脱维亚", + "Liechtenstein": "列支敦士登", + "Laos": "老挝", + "Libya": "利比亚", + "Liberia": "利比里亚", + "Japan": "日本", + "Jamaica": "牙买加", + "Italy": "意大利", + "Israel": "以色列", + "Ireland": "爱尔兰", + "Iraq": "伊拉克", + "Indonesia": "印度尼西亚", + "India": "印度", + "Iceland": "冰岛", + "Iran": "伊朗", + "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": "哥斯达黎加" } From 563403cee81f14506d8e101238f3bd128468099f Mon Sep 17 00:00:00 2001 From: RainSlide Date: Wed, 24 Mar 2021 13:18:58 +0000 Subject: [PATCH 163/183] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2889 of 2889 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index e7d94be8e2..3a4e346761 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -2742,7 +2742,7 @@ "Gibraltar": "直布羅陀", "Ghana": "迦納", "Germany": "德國", - "Georgia": "喬治亞", + "Georgia": "格魯吉亞", "Gambia": "甘比亞", "Gabon": "加彭", "French Southern Territories": "法屬南部領地", @@ -2760,11 +2760,11 @@ "El Salvador": "薩爾瓦多", "Egypt": "埃及", "Ecuador": "厄瓜多", - "Dominican Republic": "多明尼加", + "Dominican Republic": "多明尼加共和國", "Dominica": "多米尼克", "Djibouti": "吉布地", "Denmark": "丹麥", - "Côte d’Ivoire": "象牙海岸", + "Côte d’Ivoire": "科特迪瓦", "Czech Republic": "捷克", "Cyprus": "賽普勒斯", "Curaçao": "古拉索", From af0b4ef41083a71b7872cf5b2ae83c87ac303be6 Mon Sep 17 00:00:00 2001 From: BinotaLIU Date: Sun, 21 Mar 2021 20:27:53 +0000 Subject: [PATCH 164/183] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2889 of 2889 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 | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 3a4e346761..6932b1ba9a 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -25,7 +25,7 @@ "Click here to fix": "點擊這里修復", "Confirm password": "確認密碼", "Continue": "繼續", - "Create Room": "創建聊天室", + "Create Room": "建立聊天室", "Cryptography": "加密", "Current password": "當前密碼", "/ddg is not a command": "/ddg 不是一個命令", @@ -109,7 +109,7 @@ "Someone": "某人", "Submit": "提交", "Success": "成功", - "This email address is already in use": "這個電子郵件地址已被使用", + "This email address is already in use": "該電子郵件位址已被使用", "This email address was not found": "未找到此電子郵件地址", "The email address linked to your account must be entered.": "必須輸入和你帳號關聯的電子郵件地址。", "Unable to add email address": "無法新增電郵地址", @@ -259,7 +259,7 @@ "This room has no local addresses": "此房間沒有本機地址", "This room is not recognised.": "此聊天室不被認可。", "This doesn't appear to be a valid email address": "這似乎不是有效的電子郵件地址", - "This phone number is already in use": "這個電話號碼已在使用中", + "This phone number is already in use": "該電話號碼已被使用", "This room": "此房間", "This room is not accessible by remote Matrix servers": "此房間無法被遠端的 Matrix 伺服器存取", "To use it, just wait for autocomplete results to load and tab through them.": "要使用它,只要等待自動完成的結果載入並在它們上面按 Tab。", @@ -625,7 +625,7 @@ "Room Notification": "聊天室通知", "The information being sent to us to help make %(brand)s better includes:": "傳送給我們以協助改進 %(brand)s 的資訊包含了:", "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "這個頁面包含了可識別的資訊,如聊天室、使用者或群組 ID,這些資料會在傳到伺服器前被刪除。", - "The platform you're on": "您使用的平臺是", + "The platform you're on": "您使用的平台是", "The version of %(brand)s": "%(brand)s 版本", "Your language of choice": "您選擇的語言", "Which officially provided instance you are using, if any": "您正在使用的任何官方實體,如果有的話", @@ -1520,8 +1520,8 @@ "Create a private room": "建立私人聊天室", "Topic (optional)": "主題(選擇性)", "Make this room public": "讓聊天室公開", - "Hide advanced": "隱藏進階的", - "Show advanced": "顯示進階的", + "Hide advanced": "隱藏進階", + "Show advanced": "顯示進階", "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "阻擋其他 matrix 伺服器上的使用加入此聊天室(此設定無法在之後變更!)", "Close dialog": "關閉對話框", "To continue you need to accept the terms of this service.": "要繼續,您必須同意本服務的條款。", @@ -1543,7 +1543,7 @@ "Click the link in the email you received to verify and then click continue again.": "點擊您收到的電子郵件中的連結以驗證然後再次點擊繼續。", "You are about to remove %(count)s messages by %(user)s. This cannot be undone. Do you wish to continue?|one": "您將要移除 %(user)s 的 1 則訊息。這無法復原。您想要繼續嗎?", "Remove %(count)s messages|one": "移除 1 則訊息", - "Add Email Address": "新增電子郵件地址", + "Add Email Address": "新增電子郵件位址", "Add Phone Number": "新增電話號碼", "%(creator)s created and configured the room.": "%(creator)s 建立並設定了聊天室。", "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "您應該在斷線前從身份識別伺服器 移除您的個人資料。不幸的是,身份識別伺服器 目前離線中或無法連線。", @@ -1847,7 +1847,7 @@ "Bridges": "橋接", "This user has not verified all of their sessions.": "此使用者尚未驗證他們的所有工作階段。", "You have verified this user. This user has verified all of their sessions.": "您已驗證此使用者。此使用者已驗證他們所有的工作階段。", - "Someone is using an unknown session": "某人正仔使用未知的工作階段", + "Someone is using an unknown session": "某人正在使用未知的工作階段", "Your key share request has been sent - please check your other sessions for key share requests.": "您的金鑰分享請求已傳送,請檢查您其他的工作階段以取得金鑰分享請求。", "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.": "金鑰分享請求已自動傳送到您其他的工作階段。如果您在您其他的工作階段上拒絕或忽略金鑰分享請求,點擊此處以再此請求此工作階段的金鑰。", "If your other sessions do not have the key for this message you will not be able to decrypt them.": "如果您的其他工作階段沒有此訊息的金鑰,您就無法解密它們。", @@ -2079,11 +2079,11 @@ "Sends a message as html, without interpreting it as markdown": "以 html 形式傳送訊息,不將其翻譯為 markdown", "Cancel replying to a message": "取消回覆訊息", "Sign in with SSO": "使用單一登入系統登入", - "Use Single Sign On to continue": "使用單一登入繼續", - "Confirm adding this email address by using Single Sign On to prove your identity.": "透過使用單一登入來證明您的身份以確認新增此電子郵件地址。", + "Use Single Sign On to continue": "使用單一登入來繼續", + "Confirm adding this email address by using Single Sign On to prove your identity.": "使用單一登入來證明身份,以確認新增該電子郵件位址。", "Single Sign On": "單一登入", "Confirm adding email": "確任新增電子郵件", - "Click the button below to confirm adding this email address.": "點擊下方按鈕以確認新增此電子郵件地址。", + "Click the button below to confirm adding this email address.": "點擊下方按鈕以確認新增此電子郵件位址。", "Confirm adding this phone number by using Single Sign On to prove your identity.": "透過使用單一登入來證明您的身份以確認新增此電話號碼。", "Confirm adding phone number": "確任新增電話號碼", "Click the button below to confirm adding this phone number.": "點擊下方按鈕以確認新增此電話號碼。", From 1f489662fb9d61e6ac81ca35c2b17f5b913d5a8b Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Mon, 22 Mar 2021 20:31:02 +0000 Subject: [PATCH 165/183] Translated using Weblate (Czech) Currently translated at 100.0% (2889 of 2889 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 90941ced30..0efb3df22a 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -50,7 +50,7 @@ "unknown error code": "neznámý kód chyby", "OK": "OK", "Failed to forget room %(errCode)s": "Nepodařilo se zapomenout místnost %(errCode)s", - "Dismiss": "Zahodit", + "Dismiss": "Zavřít", "powered by Matrix": "používá protokol Matrix", "Custom Server Options": "Vlastní nastavení serveru", "Add a widget": "Přidat widget", From 8e33e0343dcc1a064a36ca0b38cad9157380578d Mon Sep 17 00:00:00 2001 From: Graeme Power Date: Mon, 22 Mar 2021 17:44:49 +0000 Subject: [PATCH 166/183] Translated using Weblate (Irish) Currently translated at 23.2% (671 of 2889 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ga/ --- src/i18n/strings/ga.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/ga.json b/src/i18n/strings/ga.json index 4580e538ce..c98107b767 100644 --- a/src/i18n/strings/ga.json +++ b/src/i18n/strings/ga.json @@ -3,7 +3,7 @@ "Signing In...": "ag Síniú Isteach…", "Sign in": "Sínigh Isteach", "Already have an account? Sign in here": "An bhfuil cuntas agat cheana? Sínigh isteach anseo", - "Show less": "Taispeái níos lú", + "Show less": "Taispeáin níos lú", "Show more": "Taispeáin níos mó", "Show %(count)s more|one": "Taispeáin %(count)s níos mó", "Show %(count)s more|other": "Taispeáin %(count)s níos mó", @@ -662,5 +662,14 @@ "Dismiss": "Cuir uait", "Use Single Sign On to continue": "Lean ar aghaidh le SSO", "This phone number is already in use": "Úsáidtear an uimhir ghutháin seo chean féin", - "This email address is already in use": "Úsáidtear an seoladh ríomhphoist seo chean féin" + "This email address is already in use": "Úsáidtear an seoladh ríomhphoist seo chean féin", + "Sign out and remove encryption keys?": "Sínigh amach agus scrios eochracha criptiúcháin?", + "Clear Storage and Sign Out": "Scrios Stóras agus Sínigh Amach", + "You're signed out": "Tá tú sínithe amach", + "Sign out": "Sínigh amach", + "Are you sure you want to sign out?": "An bhfuil tú cinnte go dteastaíonn uait sínigh amach?", + "Signed Out": "Sínithe Amach", + "Unable to query for supported registration methods.": "Ní féidir iarratas a dhéanamh faoi modhanna cláraithe tacaithe.", + "Host account on": "Óstáil cuntas ar", + "Create account": "Déan cuntas a chruthú" } From f340b8f7edfb339cf4c168bdeaf5e73c27d273b4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 16:42:21 +0000 Subject: [PATCH 167/183] Set invite PL requirement for public spaces to 0 explicitly --- src/components/views/spaces/SpaceCreateMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx index 6269de1c50..9ee6edc489 100644 --- a/src/components/views/spaces/SpaceCreateMenu.tsx +++ b/src/components/views/spaces/SpaceCreateMenu.tsx @@ -88,6 +88,7 @@ const SpaceCreateMenu = ({ onFinished }) => { power_level_content_override: { // Only allow Admins to write to the timeline to prevent hidden sync spam events_default: 100, + ...Visibility.Public ? { invite: 0 } : {}, }, }, spinner: false, From 56dbd5f628713816cfdc7773716b41dc5bcb6eb5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 16:45:53 +0000 Subject: [PATCH 168/183] Remove unused autoJoin prop and move viaServers logic into RVS this fixes the issue where autoJoining ignored viaServers --- src/components/structures/LoggedInView.tsx | 6 ------ src/components/structures/MatrixChat.tsx | 2 -- src/components/structures/RoomView.tsx | 10 ++-------- src/stores/RoomViewStore.tsx | 5 ++++- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 60a2bf4ada..20a3b811c5 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -74,7 +74,6 @@ function canElementReceiveInput(el) { interface IProps { matrixClient: MatrixClient; onRegistered: (credentials: IMatrixClientCreds) => Promise; - viaServers?: string[]; hideToSRUsers: boolean; resizeNotifier: ResizeNotifier; // eslint-disable-next-line camelcase @@ -143,9 +142,6 @@ class LoggedInView extends React.Component { // transitioned to PWLU) onRegistered: PropTypes.func, - // Used by the RoomView to handle joining rooms - viaServers: PropTypes.arrayOf(PropTypes.string), - // and lots and lots of other stuff. }; @@ -625,11 +621,9 @@ class LoggedInView extends React.Component { case PageTypes.RoomView: pageElement = { page_type: PageTypes.RoomView, threepidInvite: roomInfo.threepid_invite, roomOobData: roomInfo.oob_data, - viaServers: roomInfo.via_servers, ready: true, roomJustCreatedOpts: roomInfo.justCreatedOpts, }, () => { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 706cd5ded8..8a9c7cabd9 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -112,10 +112,6 @@ interface IProps { inviterName?: string; }; - // Servers the RoomView can use to try and assist joins - viaServers?: string[]; - - autoJoin?: boolean; resizeNotifier: ResizeNotifier; justCreatedOpts?: IOpts; @@ -450,9 +446,7 @@ export default class RoomView extends React.Component { // now not joined because the js-sdk peeking API will clobber our historical room, // making it impossible to indicate a newly joined room. if (!joining && roomId) { - if (this.props.autoJoin) { - this.onJoinButtonClicked(); - } else if (!room && shouldPeek) { + if (!room && shouldPeek) { console.info("Attempting to peek into room %s", roomId); this.setState({ peekLoading: true, @@ -1123,7 +1117,7 @@ export default class RoomView extends React.Component { const signUrl = this.props.threepidInvite?.signUrl; dis.dispatch({ action: 'join_room', - opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers }, + opts: { inviteSignUrl: signUrl }, _type: "unknown", // TODO: instrumentation }); return Promise.resolve(); diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index f4c0c1b15c..601c77cdf3 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -273,7 +273,10 @@ class RoomViewStore extends Store { const cli = MatrixClientPeg.get(); const address = this.state.roomAlias || this.state.roomId; try { - await retry(() => cli.joinRoom(address, payload.opts), NUM_JOIN_RETRY, (err) => { + await retry(() => cli.joinRoom(address, { + viaServers: payload.via_servers, + ...payload.opts, + }), NUM_JOIN_RETRY, (err) => { // if we received a Gateway timeout then retry return err.httpStatus === 504; }); From d9f3e70b0bf165ff809d05e4294557cf6ea7ded5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 16:46:30 +0000 Subject: [PATCH 169/183] Fix joining over federation from Space Home (via servers) --- src/components/structures/SpaceRoomView.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 16028f0975..06b4fe5983 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -258,20 +258,26 @@ const SpaceLanding = ({ space }) => { ; } - const [loading, roomsMap, relations, numRooms] = useAsyncMemo(async () => { + const [loading, roomsMap, relations, viaMap, numRooms] = useAsyncMemo(async () => { try { const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join"); const parentChildRelations = new EnhancedMap>(); + const viaMap = new EnhancedMap>(); data.events.map((ev: ISpaceSummaryEvent) => { if (ev.type === EventType.SpaceChild) { parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); } + + if (Array.isArray(ev.content["via"])) { + const set = viaMap.getOrCreate(ev.state_key, new Set()); + ev.content["via"].forEach(via => set.add(via)); + } }); const roomsMap = new Map(data.rooms.map(r => [r.room_id, r])); const numRooms = data.rooms.filter(r => r.room_type !== RoomType.Space).length; - return [false, roomsMap, parentChildRelations, numRooms]; + return [false, roomsMap, parentChildRelations, viaMap, numRooms]; } catch (e) { console.error(e); // TODO } @@ -292,7 +298,7 @@ const SpaceLanding = ({ space }) => { relations={relations} parents={new Set()} onViewRoomClick={(roomId, autoJoin) => { - showRoom(roomsMap.get(roomId), [], autoJoin); + showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); }} /> ; From 6d9496cc224fbfe23c29800b9fe24aadc7e14906 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 17:02:12 +0000 Subject: [PATCH 170/183] Consolidate space summary api logic between space room view and directory --- .../structures/SpaceRoomDirectory.tsx | 51 +++++++++++-------- src/components/structures/SpaceRoomView.tsx | 43 +++++----------- 2 files changed, 44 insertions(+), 50 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 877a4283f1..0dfb33379d 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -15,7 +15,8 @@ limitations under the License. */ import React, {useMemo, useState} from "react"; -import Room from "matrix-js-sdk/src/models/room"; +import {Room} from "matrix-js-sdk/src/models/room"; +import {MatrixClient} from "matrix-js-sdk/src/client"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import classNames from "classnames"; import {sortBy} from "lodash"; @@ -232,7 +233,7 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi interface IHierarchyLevelProps { spaceId: string; rooms: Map; - relations: EnhancedMap>; + relations: Map>; parents: Set; selectedMap?: Map>; onViewRoomClick(roomId: string, autoJoin: boolean): void; @@ -308,23 +309,15 @@ export const HierarchyLevel = ({ }; -const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinished }) => { +// mutate argument refreshToken to force a reload +export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [ + ISpaceSummaryRoom[], + Map>, + Map>, + Map>, +] | [] => { // TODO pagination - const cli = MatrixClientPeg.get(); - const userId = cli.getUserId(); - const [query, setQuery] = useState(initialText); - - const onCreateRoomClick = () => { - dis.dispatch({ - action: 'view_create_room', - public: true, - }); - onFinished(); - }; - - const [selected, setSelected] = useState(new Map>()); // Map> - - const [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => { + return useAsyncMemo(async () => { try { const data = await cli.getSpaceSummary(space.roomId); @@ -342,13 +335,31 @@ const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinis } }); - return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap]; + return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; } catch (e) { console.error(e); // TODO } return []; - }, [space], []); + }, [space, refreshToken], []); +}; + +const SpaceRoomDirectory: React.FC = ({ space, initialText = "", onFinished }) => { + const cli = MatrixClientPeg.get(); + const userId = cli.getUserId(); + const [query, setQuery] = useState(initialText); + + const onCreateRoomClick = () => { + dis.dispatch({ + action: 'view_create_room', + public: true, + }); + onFinished(); + }; + + const [selected, setSelected] = useState(new Map>()); // Map> + + const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space); const roomsMap = useMemo(() => { if (!rooms) return null; diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 06b4fe5983..3ef10363b9 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {RefObject, useContext, useRef, useState} from "react"; +import React, {RefObject, useContext, useMemo, useRef, useState} from "react"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; import {EventSubscription} from "fbemitter"; @@ -46,7 +46,7 @@ import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanel import {useStateArray} from "../../hooks/useStateArray"; import SpacePublicShare from "../views/spaces/SpacePublicShare"; import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space"; -import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom} from "./SpaceRoomDirectory"; +import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom, useSpaceSummary} from "./SpaceRoomDirectory"; import {useAsyncMemo} from "../../hooks/useAsyncMemo"; import {EnhancedMap} from "../../utils/maps"; import AutoHideScrollbar from "./AutoHideScrollbar"; @@ -228,7 +228,7 @@ const SpaceLanding = ({ space }) => { const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); - const [_, forceUpdate] = useStateToggle(false); // TODO + const [refreshToken, forceUpdate] = useStateToggle(false); let addRoomButtons; if (canAddRooms) { @@ -258,32 +258,13 @@ const SpaceLanding = ({ space }) => { ; } - const [loading, roomsMap, relations, viaMap, numRooms] = useAsyncMemo(async () => { - try { - const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join"); - - const parentChildRelations = new EnhancedMap>(); - const viaMap = new EnhancedMap>(); - data.events.map((ev: ISpaceSummaryEvent) => { - if (ev.type === EventType.SpaceChild) { - parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev); - } - - if (Array.isArray(ev.content["via"])) { - const set = viaMap.getOrCreate(ev.state_key, new Set()); - ev.content["via"].forEach(via => set.add(via)); - } - }); - - const roomsMap = new Map(data.rooms.map(r => [r.room_id, r])); - const numRooms = data.rooms.filter(r => r.room_type !== RoomType.Space).length; - return [false, roomsMap, parentChildRelations, viaMap, numRooms]; - } catch (e) { - console.error(e); // TODO - } - - return [false]; - }, [space, _], [true]); + const [rooms, relations, viaMap] = useSpaceSummary(cli, space, refreshToken); + const [roomsMap, numRooms] = useMemo(() => { + if (!rooms) return []; + const roomsMap = new Map(rooms.map(r => [r.room_id, r])); + const numRooms = rooms.filter(r => r.room_type !== RoomType.Space).length; + return [roomsMap, numRooms]; + }, [rooms]); let previewRooms; if (roomsMap) { @@ -302,7 +283,7 @@ const SpaceLanding = ({ space }) => { }} /> ; - } else if (loading) { + } else if (!rooms) { previewRooms = ; } else { previewRooms =

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

    ; @@ -647,6 +628,8 @@ export default class SpaceRoomView extends React.PureComponent { }; private goToFirstRoom = async () => { + // TODO actually go to the first room + const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId); if (childRooms.length) { const room = childRooms[0]; From ee5d0d68421f0510d8a084f77954a9869e9e67a4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 17:05:21 +0000 Subject: [PATCH 171/183] Fix alignment bug with space panel on spaces with subspaces in Chrome --- res/css/structures/_SpacePanel.scss | 3 --- src/components/views/spaces/SpaceTreeLevel.tsx | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index ffe67ce6ab..33f4dc0588 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -146,9 +146,6 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton_toggleCollapse { width: $gutterSize; - // negative margin to place it correctly even with the complex - // 4px selection border each space button has when active - margin-right: -4px; height: 20px; mask-position: center; mask-size: 20px; diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 1b86bb7898..1da6720eea 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -37,7 +37,7 @@ import { showSpaceSettings, } from "../../../utils/space"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {ButtonEvent} from "../elements/AccessibleButton"; +import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import Modal from "../../../Modal"; import SpacePublicShare from "./SpacePublicShare"; @@ -353,7 +353,7 @@ export class SpaceItem extends React.PureComponent { const avatarSize = isNested ? 24 : 32; const toggleCollapseButton = childSpaces && childSpaces.length ? -
    ; - } else if (this.state.activeSpace) { + } else if (this.props.activeSpace) { explorePrompt =
    { _t("Quick actions") }
    - { this.state.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && From 20ea1436fcb2631f376eec73d3bd87cdf5d1e607 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 19:43:33 +0000 Subject: [PATCH 173/183] Update iconography for spaces --- res/css/structures/_LeftPanel.scss | 4 ++++ res/css/structures/_SpacePanel.scss | 8 ++++++-- res/css/views/rooms/_RoomList.scss | 3 +++ src/components/structures/LeftPanel.tsx | 4 +++- src/components/views/spaces/SpaceTreeLevel.tsx | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 10357117da..7c3cd1c513 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -130,6 +130,10 @@ $roomListCollapsedWidth: 68px; mask-repeat: no-repeat; background: $secondary-fg-color; } + + &.mx_LeftPanel_exploreButton_space::before { + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); + } } } diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 33f4dc0588..5cca4aca11 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -339,11 +339,15 @@ $activeBorderColor: $secondary-fg-color; } .mx_SpacePanel_iconPlus::before { - mask-image: url('$(res)/img/element-icons/plus.svg'); + mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg'); + } + + .mx_SpacePanel_iconHash::before { + mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg'); } .mx_SpacePanel_iconExplore::before { - mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); } } diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 641b434af4..22440fa6db 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -27,6 +27,9 @@ limitations under the License. .mx_RoomList_iconExplore::before { mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); } +.mx_RoomList_iconBrowse::before { + mask-image: url('$(res)/img/element-icons/roomlist/browse.svg'); +} .mx_RoomList_iconDialpad::before { mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg'); } diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index eb21301738..2861cfd7e7 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -392,7 +392,9 @@ export default class LeftPanel extends React.Component { onEnter={this.onEnter} /> diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 1da6720eea..ca6f90fa91 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -258,7 +258,7 @@ export class SpaceItem extends React.PureComponent { onClick={this.onNewRoomClick} /> From 62172ba43d633b950b5f6a093ca41e4fb1a104d8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 19:43:42 +0000 Subject: [PATCH 174/183] i18n --- src/i18n/strings/en_EN.json | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5f1003bf29..ba44586d79 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1011,11 +1011,12 @@ "Share invite link": "Share invite link", "Invite people": "Invite people", "Invite with email or username": "Invite with email or username", - "Invite members": "Invite members", + "Invite to %(spaceName)s": "Invite to %(spaceName)s", "Share your public space": "Share your public space", "Settings": "Settings", "Leave space": "Leave space", - "New room": "New room", + "Create new room": "Create new room", + "Add existing room": "Add existing room", "Space Home": "Space Home", "Members": "Members", "Explore rooms": "Explore rooms", @@ -1478,6 +1479,7 @@ "Add a topic to help people know what it is about.": "Add a topic to help people know what it is about.", "You created this room.": "You created this room.", "%(displayName)s created this room.": "%(displayName)s created this room.", + "Invite to just this room": "Invite to just this room", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", "No pinned messages.": "No pinned messages.", @@ -1524,11 +1526,8 @@ "Start chat": "Start chat", "Rooms": "Rooms", "Add room": "Add room", - "Create new room": "Create new room", "You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space", - "Add existing room": "Add existing room", "You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space", - "Explore space rooms": "Explore space rooms", "Explore community rooms": "Explore community rooms", "Explore public rooms": "Explore public rooms", "Low priority": "Low priority", @@ -1540,6 +1539,7 @@ "Can't see what you’re looking for?": "Can't see what you’re looking for?", "Start a new chat": "Start a new chat", "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|other": "%(count)s results", "%(count)s results|one": "%(count)s result", @@ -2001,14 +2001,13 @@ "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", "Space selection": "Space selection", - "Add existing spaces/rooms": "Add existing spaces/rooms", + "Add existing rooms": "Add existing rooms", "Filter your rooms and spaces": "Filter your rooms and spaces", "Spaces": "Spaces", "Don't want to add an existing room?": "Don't want to add an existing room?", "Create a new room": "Create a new room", - "Applying...": "Applying...", - "Apply": "Apply", "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", @@ -2200,7 +2199,6 @@ "Start a conversation with someone using their name or username (like ).": "Start a conversation with someone using their name or username (like ).", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here", "Go": "Go", - "Invite to %(spaceName)s": "Invite to %(spaceName)s", "Unnamed Space": "Unnamed Space", "Invite to %(roomName)s": "Invite to %(roomName)s", "Invite someone using their name, email address, username (like ) or share this space.": "Invite someone using their name, email address, username (like ) or share this space.", From a61f4c1811c266afacd18434cc628ffb1f986f82 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 19:58:35 +0000 Subject: [PATCH 175/183] Auto-select space if joined whilst being viewed --- src/stores/SpaceStore.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index e4b537169e..bcf95a82be 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -34,6 +34,7 @@ import {setHasDiff} from "../utils/sets"; import {objectDiff} from "../utils/objects"; import {arrayHasDiff} from "../utils/arrays"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; +import RoomViewStore from "./RoomViewStore"; type SpaceKey = string | symbol; @@ -368,6 +369,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.onRoomsUpdate(); } + // if the user was looking at the room and then joined select that space + if (room.getMyMembership() === "join" && room.roomId === RoomViewStore.getRoomId()) { + this.setActiveSpace(room); + } + const numSuggestedRooms = this._suggestedRooms.length; this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId); if (numSuggestedRooms !== this._suggestedRooms.length) { From d705e3020f2636a776fff53226b392927837b888 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 20:01:27 +0000 Subject: [PATCH 176/183] delint --- src/components/structures/SpaceRoomView.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 3ef10363b9..cea59093ac 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -46,9 +46,7 @@ import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanel import {useStateArray} from "../../hooks/useStateArray"; import SpacePublicShare from "../views/spaces/SpacePublicShare"; import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space"; -import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom, useSpaceSummary} from "./SpaceRoomDirectory"; -import {useAsyncMemo} from "../../hooks/useAsyncMemo"; -import {EnhancedMap} from "../../utils/maps"; +import {HierarchyLevel, ISpaceSummaryRoom, showRoom, useSpaceSummary} from "./SpaceRoomDirectory"; import AutoHideScrollbar from "./AutoHideScrollbar"; import MemberAvatar from "../views/avatars/MemberAvatar"; import {useStateToggle} from "../../hooks/useStateToggle"; From d3cdd38ff4d734e062a9d822425688526d756b5b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 24 Mar 2021 20:11:23 +0000 Subject: [PATCH 177/183] delint some more --- src/components/views/rooms/RoomList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index d7fbe0b4df..e83b07f71b 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -49,7 +49,7 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con import AccessibleButton from "../elements/AccessibleButton"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import CallHandler from "../../../CallHandler"; -import SpaceStore, {SUGGESTED_ROOMS, UPDATE_SELECTED_SPACE} from "../../../stores/SpaceStore"; +import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore"; import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import RoomAvatar from "../avatars/RoomAvatar"; From 510f08e9f1c923a0dfe3b1fd73ab1fc92af2a772 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 25 Mar 2021 10:11:52 +0000 Subject: [PATCH 178/183] Update failed invites copy --- src/components/views/dialogs/InviteDialog.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- src/utils/MultiInviter.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 4ea53349bd..8ad262cff9 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -673,7 +673,7 @@ export default class InviteDialog extends React.PureComponent {return {userId: u, errorText: this.errors[u].errorText};}), onInviteAnyways: () => inviteUnknowns(), onGiveUp: () => { From aa1f468235becac7db7eecc0488d330cac6a2557 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Thu, 25 Mar 2021 14:36:40 +0200 Subject: [PATCH 179/183] fix: room id sticked with event id --- res/css/structures/_ViewSource.scss | 8 -------- src/components/structures/ViewSource.js | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss index 0126c16599..248eab5d88 100644 --- a/res/css/structures/_ViewSource.scss +++ b/res/css/structures/_ViewSource.scss @@ -14,14 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_ViewSource_label_left { - float: left; -} - -.mx_ViewSource_label_right { - float: right; -} - .mx_ViewSource_separator { clear: both; border-bottom: 1px solid #e5e5e5; diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index be9be4db81..6fe99dd464 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -176,8 +176,8 @@ export default class ViewSource extends React.Component { return (
    -
    Room ID: {roomId}
    -
    Event ID: {eventId}
    +
    Room ID: {roomId}
    +
    Event ID: {eventId}
    {isEditing ? this.editSourceContent() : this.viewSourceContent()}
    From b5bc6251cffdb996674d62c3ee7c5a917b6cd5cb Mon Sep 17 00:00:00 2001 From: Marek Matys <57749215+thermaq@users.noreply.github.com> Date: Thu, 25 Mar 2021 14:05:06 +0100 Subject: [PATCH 180/183] fix password change popup message Fixed bad error message when providing bad old password during password change. Signed-off-by: Marek Matys --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index b7dbfa4a3b..b1ad9f3d23 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -206,10 +206,10 @@ export default class GeneralUserSettingsTab extends React.Component { _onPasswordChangeError = (err) => { // TODO: Figure out a design that doesn't involve replacing the current dialog - let errMsg = err.error || ""; + let errMsg = err.error || err.message || ""; if (err.httpStatus === 403) { errMsg = _t("Failed to change password. Is your password correct?"); - } else if (err.httpStatus) { + } else if (!errMsg) { errMsg += ` (HTTP status ${err.httpStatus})`; } const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); From 97c7f3753bf86cce373d5cb3624f488c329f39b0 Mon Sep 17 00:00:00 2001 From: Panagiotis <27917356+panoschal@users.noreply.github.com> Date: Thu, 25 Mar 2021 20:58:39 +0200 Subject: [PATCH 181/183] fix: save editor state when reply is open --- src/components/views/rooms/SendMessageComposer.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 51899a0e45..aca2066d34 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -472,12 +472,17 @@ export default class SendMessageComposer extends React.Component { } } + // should save state when editor has contents or reply is open + _shouldSaveStoredEditorState = () => { + return !this.model.isEmpty || this.props.replyToEvent; + } + _saveStoredEditorState = () => { - if (this.model.isEmpty) { - this._clearStoredEditorState(); - } else { + if (this._shouldSaveStoredEditorState()) { const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent); localStorage.setItem(this._editorStateKey, JSON.stringify(item)); + } else { + this._clearStoredEditorState(); } } From 5e3a7c48f4a006e731e54bdddfc83004c665e482 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 25 Mar 2021 19:37:55 +0000 Subject: [PATCH 182/183] Fix edge case with redaction grouper messing up continuations --- src/components/structures/MessagePanel.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6d03c849c4..41a3015721 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -46,6 +46,9 @@ function shouldFormContinuation(prevEvent, mxEvent) { // check if within the max continuation period if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false; + // As we summarise redactions, do not continue a redacted event onto a non-redacted one and vice-versa + if (mxEvent.isRedacted() !== prevEvent.isRedacted()) return false; + // Some events should appear as continuations from previous events of different types. if (mxEvent.getType() !== prevEvent.getType() && (!continuedTypes.includes(mxEvent.getType()) || @@ -1125,7 +1128,7 @@ class RedactionGrouper { } getNewPrevEvent() { - return this.events[0]; + return this.events[this.events.length - 1]; } } From f1a9c5ae939e8c56c7c8745a3ceb730bb88fabbd Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 25 Mar 2021 17:38:34 -0400 Subject: [PATCH 183/183] Fix line numbers when missing trailing newline _addLineNumbers expected code blocks to contain a trailing newline, but this is not always the case. Signed-off-by: Robin Townsend --- src/components/views/messages/TextualBody.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index b0eb6f2f35..353f40b6a9 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -216,12 +216,12 @@ export default class TextualBody extends React.Component { } _addLineNumbers(pre) { + // Calculate number of lines in pre + const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length; pre.innerHTML = '' + pre.innerHTML + ''; const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0]; - // Calculate number of lines in pre - const number = pre.innerHTML.split(/\n/).length; // Iterate through lines starting with 1 (number of the first line is 1) - for (let i = 1; i < number; i++) { + for (let i = 1; i <= number; i++) { lineNumbers.innerHTML += '' + i + ''; } }