From e33f4ba9c0e8c29cf074f566cbb69c27007f6c57 Mon Sep 17 00:00:00 2001 From: Resynth <resynth1943@tutanota.com> Date: Sun, 25 Oct 2020 22:04:39 +0000 Subject: [PATCH 01/64] Warn on Access Token reveal Signed-off-by: Resynth <resynth1943@tutanota.com> --- .../views/dialogs/AccessTokenDialog.tsx | 39 +++++++++++++++++++ .../settings/tabs/user/HelpUserSettingsTab.js | 14 ++++++- 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/components/views/dialogs/AccessTokenDialog.tsx diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx new file mode 100644 index 0000000000..81c48f219a --- /dev/null +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -0,0 +1,39 @@ + /* +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { _t } from '../../../languageHandler'; +import QuestionDialog from './QuestionDialog'; + +type IProps = Exclude< + React.ComponentProps<QuestionDialog>, + "title" | "danger" | "description" + >; + +export default function AccessTokenDialog (props: IProps) { + return ( + <QuestionDialog + {...props} + title="Reveal Access Token" + danger={true} + description={_t( + "Do not reveal your Access Token to anyone, under any circumstances. " + + "Sharing your Access Token with someone would allow them to login to " + + "your account, and access your private information." + )} + ></QuestionDialog> + ); +} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index 85ba22a353..585a54ff86 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -27,6 +27,7 @@ import * as sdk from "../../../../../"; import PlatformPeg from "../../../../../PlatformPeg"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import UpdateCheckButton from "../../UpdateCheckButton"; +import AccessTokenDialog from '../../../dialogs/AccessTokenDialog'; export default class HelpUserSettingsTab extends React.Component { static propTypes = { @@ -148,6 +149,17 @@ export default class HelpUserSettingsTab extends React.Component { ); } + onAccessTokenSpoilerClick = async (event) => { + // React throws away the event before we can use it (we are async, after all). + event.persist(); + + // We make the user accept a scary popup to combat Social Engineering. No peeking! + await Modal.createTrackedDialog('Reveal Access Token', '', AccessTokenDialog).finished; + + // Pass it onto the handler. + this._showSpoiler(event); + } + render() { const brand = SdkConfig.get().brand; @@ -266,7 +278,7 @@ export default class HelpUserSettingsTab extends React.Component { {_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br /> {_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br /> {_t("Access Token:") + ' '} - <AccessibleButton element="span" onClick={this._showSpoiler} + <AccessibleButton element="span" onClick={this.onAccessTokenSpoilerClick} data-spoiler={MatrixClientPeg.get().getAccessToken()}> <{ _t("click to reveal") }> </AccessibleButton> From ae29168e077be47a83628f10b7765d6bcc9a7a0a Mon Sep 17 00:00:00 2001 From: Resynth <resynth1943@tutanota.com> Date: Sun, 25 Oct 2020 22:05:44 +0000 Subject: [PATCH 02/64] Lint Signed-off-by: Resynth <resynth1943@tutanota.com> --- src/components/views/dialogs/AccessTokenDialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index 81c48f219a..f95effd523 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -1,4 +1,4 @@ - /* +/* Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,7 @@ type IProps = Exclude< "title" | "danger" | "description" >; -export default function AccessTokenDialog (props: IProps) { +export default function AccessTokenDialog(props: IProps) { return ( <QuestionDialog {...props} @@ -32,7 +32,7 @@ export default function AccessTokenDialog (props: IProps) { description={_t( "Do not reveal your Access Token to anyone, under any circumstances. " + "Sharing your Access Token with someone would allow them to login to " + - "your account, and access your private information." + "your account, and access your private information.", )} ></QuestionDialog> ); From 76edd551e5833cb1c94c1025fa069aeb79a9faa2 Mon Sep 17 00:00:00 2001 From: Resynth <resynth1943@tutanota.com> Date: Mon, 26 Oct 2020 00:34:14 +0000 Subject: [PATCH 03/64] Fix i18n Signed-off-by: Resynth <resynth1943@tutanota.com> --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index eda69d68ea..ad6593f704 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1585,6 +1585,7 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", + "Do not reveal your Access Token to anyone, under any circumstances. Sharing your Access Token with someone would allow them to login to your account, and access your private information.": "Do not reveal your Access Token to anyone, under any circumstances. Sharing your Access Token with someone would allow them to login to your account, and access your private information.", "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", From a3212c0477ec0fc9166a56a49a9579bae4712d5f Mon Sep 17 00:00:00 2001 From: Resynth <resynth1943@tutanota.com> Date: Mon, 26 Oct 2020 23:46:53 +0000 Subject: [PATCH 04/64] Fix weird formatting Signed-off-by: Resynth <resynth1943@tutanota.com> --- src/components/views/dialogs/AccessTokenDialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index f95effd523..2d96d8ec20 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -1,5 +1,6 @@ /* Copyright 2017 Vector Creations Ltd +Copyright 2020 Resynth <resynth1943.net> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,7 +22,7 @@ import QuestionDialog from './QuestionDialog'; type IProps = Exclude< React.ComponentProps<QuestionDialog>, "title" | "danger" | "description" - >; +>; export default function AccessTokenDialog(props: IProps) { return ( From 34fbed3fbbbbb0ffc17a15631d256b855080b1ee Mon Sep 17 00:00:00 2001 From: Qt Resynth <resynth1943@tutanota.com> Date: Fri, 20 Nov 2020 01:15:58 +0000 Subject: [PATCH 05/64] Update src/components/views/dialogs/AccessTokenDialog.tsx --- src/components/views/dialogs/AccessTokenDialog.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index 2d96d8ec20..6a943eb5a8 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -31,9 +31,7 @@ export default function AccessTokenDialog(props: IProps) { title="Reveal Access Token" danger={true} description={_t( - "Do not reveal your Access Token to anyone, under any circumstances. " + - "Sharing your Access Token with someone would allow them to login to " + - "your account, and access your private information.", + "Your access token gives full access to your account. Do not share it with anyone." )} ></QuestionDialog> ); From 0e4d656e4bfc0fd16d4628ac44668f6aa2f5ae83 Mon Sep 17 00:00:00 2001 From: Resynth <resynth1943@tutanota.com> Date: Fri, 20 Nov 2020 18:21:39 +0000 Subject: [PATCH 06/64] Update src/i18n/strings/en_EN.json --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ad6593f704..8005f1cdef 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1585,7 +1585,7 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", - "Do not reveal your Access Token to anyone, under any circumstances. Sharing your Access Token with someone would allow them to login to your account, and access your private information.": "Do not reveal your Access Token to anyone, under any circumstances. Sharing your Access Token with someone would allow them to login to your account, and access your private information.", + "Your access token gives full access to your account. Do not share it with anyone.", "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", From 1b48b08525f75035b9a09993124a12d442d0f2e2 Mon Sep 17 00:00:00 2001 From: Resynth <resynth1943@tutanota.com> Date: Fri, 20 Nov 2020 18:23:18 +0000 Subject: [PATCH 07/64] Update src/i18n/strings/en_EN.json --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8005f1cdef..a57a68e7b4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1585,7 +1585,7 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", - "Your access token gives full access to your account. Do not share it with anyone.", + "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", From d44aab6d321d04deb6733d9ace5f54b5fe1104bf Mon Sep 17 00:00:00 2001 From: Resynth <resynth1943@tutanota.com> Date: Mon, 23 Nov 2020 12:57:06 +0000 Subject: [PATCH 08/64] Update src/components/views/dialogs/AccessTokenDialog.tsx Co-authored-by: Michael Telatynski <7t3chguy@googlemail.com> --- src/components/views/dialogs/AccessTokenDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index 6a943eb5a8..c0b1b929df 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -31,7 +31,7 @@ export default function AccessTokenDialog(props: IProps) { title="Reveal Access Token" danger={true} description={_t( - "Your access token gives full access to your account. Do not share it with anyone." + "Your access token gives full access to your account. Do not share it with anyone.", )} ></QuestionDialog> ); From 62e9d7f46bfb6ee07b012904a993d443ee87590e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Sat, 6 Mar 2021 09:02:15 +0100 Subject: [PATCH 09/64] Cleaner imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallView.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index b4dad3b19a..fbd30cbc9b 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -22,8 +22,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t, _td } from '../../../languageHandler'; import VideoFeed, { VideoFeedType } from "./VideoFeed"; import RoomAvatar from "../avatars/RoomAvatar"; -import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; -import { CallEvent } from 'matrix-js-sdk/src/webrtc/call'; +import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call'; import classNames from 'classnames'; import AccessibleButton from '../elements/AccessibleButton'; import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard'; From bb13dc49a6d89a1f1a50c762b3c241fc5d4b9757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Sun, 7 Mar 2021 08:13:35 +0100 Subject: [PATCH 10/64] Make CallView use CallFeed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- res/css/views/voip/_CallView.scss | 1 + res/css/views/voip/_VideoFeed.scss | 18 +++- src/CallHandler.tsx | 12 +-- src/components/views/voip/CallView.tsx | 122 ++++++++++++++++++------ src/components/views/voip/VideoFeed.tsx | 100 ++++++++++++++----- 5 files changed, 185 insertions(+), 68 deletions(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 7e2d12e539..ed3ff2afa9 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -112,6 +112,7 @@ limitations under the License. z-index: 30; border-radius: 8px; overflow: hidden; + display: flex; } .mx_CallView_video_hold { diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 8ead8bba3e..46cdf4f52c 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -14,11 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_VideoFeed_remote { - width: 100%; - height: 100%; +.mx_VideoFeed_voice { + // We don't want to collide with the call controls that have 52px of height + padding-bottom: 52px; + background-color: $inverted-bg-color; +} + +.mx_VideoFeed_video { background-color: #000; - z-index: 50; +} + +.mx_VideoFeed_remote { + flex: 1; + display: flex; + justify-content: center; + align-items: center; } .mx_VideoFeed_local { diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 42a38c7a54..e090270c95 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -621,7 +621,6 @@ export default class CallHandler { private async placeCall( roomId: string, type: PlaceCallType, - localElement: HTMLVideoElement, remoteElement: HTMLVideoElement, ) { Analytics.trackEvent('voip', 'placeCall', 'type', type); CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false); @@ -643,10 +642,7 @@ export default class CallHandler { if (type === PlaceCallType.Voice) { call.placeVoiceCall(); } else if (type === 'video') { - call.placeVideoCall( - remoteElement, - localElement, - ); + call.placeVideoCall(); } else if (type === PlaceCallType.ScreenSharing) { const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); if (screenCapErrorString) { @@ -660,8 +656,6 @@ export default class CallHandler { } call.placeScreenSharingCall( - remoteElement, - localElement, async () : Promise<DesktopCapturerSource> => { const {finished} = Modal.createDialog(DesktopCapturerSourcePicker); const [source] = await finished; @@ -715,14 +709,12 @@ export default class CallHandler { } else if (members.length === 2) { console.info(`Place ${payload.type} call in ${payload.room_id}`); - this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element); + this.placeCall(payload.room_id, payload.type); } else { // > 2 dis.dispatch({ action: "place_conference_call", room_id: payload.room_id, type: payload.type, - remote_element: payload.remote_element, - local_element: payload.local_element, }); } } diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index fbd30cbc9b..86b17dcab2 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -20,7 +20,7 @@ import dis from '../../../dispatcher/dispatcher'; import CallHandler from '../../../CallHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t, _td } from '../../../languageHandler'; -import VideoFeed, { VideoFeedType } from "./VideoFeed"; +import VideoFeed from './VideoFeed'; import RoomAvatar from "../avatars/RoomAvatar"; import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call'; import classNames from 'classnames'; @@ -30,6 +30,7 @@ import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} f import CallContextMenu from '../context_menus/CallContextMenu'; import { avatarUrlForMember } from '../../../Avatar'; import DialpadContextMenu from '../context_menus/DialpadContextMenu'; +import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed'; interface IProps { // The call for us to display @@ -58,6 +59,7 @@ interface IState { controlsVisible: boolean, showMoreMenu: boolean, showDialpad: boolean, + feeds: CallFeed[], } function getFullScreenElement() { @@ -112,6 +114,7 @@ export default class CallView extends React.Component<IProps, IState> { controlsVisible: true, showMoreMenu: false, showDialpad: false, + feeds: this.props.call.getFeeds(), } this.updateCallListeners(null, this.props.call); @@ -169,11 +172,13 @@ export default class CallView extends React.Component<IProps, IState> { oldCall.removeListener(CallEvent.State, this.onCallState); oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold); oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold); + oldCall.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged); } if (newCall) { newCall.on(CallEvent.State, this.onCallState); newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold); newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold); + newCall.on(CallEvent.FeedsChanged, this.onFeedsChanged); } } @@ -183,6 +188,10 @@ export default class CallView extends React.Component<IProps, IState> { }); }; + private onFeedsChanged = (newFeeds: Array<CallFeed>) => { + this.setState({feeds: newFeeds}); + } + private onCallLocalHoldUnhold = () => { this.setState({ isLocalOnHold: this.props.call.isLocalOnHold(), @@ -486,44 +495,71 @@ export default class CallView extends React.Component<IProps, IState> { }); } - if (this.props.call.type === CallType.Video) { - let localVideoFeed = null; - let onHoldContent = null; - let onHoldBackground = null; - const backgroundStyle: CSSProperties = {}; - const containerClasses = classNames({ - mx_CallView_video: true, - mx_CallView_video_hold: isOnHold, - }); - if (isOnHold) { - onHoldContent = <div className="mx_CallView_video_holdContent"> - {onHoldText} - </div>; + const avatarSize = this.props.pipMode ? 76 : 160; + if (isOnHold) { + if (this.props.call.type === CallType.Video) { + const containerClasses = classNames({ + mx_CallView_video: true, + mx_CallView_video_hold: isOnHold, + }); + let onHoldContent = null; + let onHoldBackground = null; + const backgroundStyle: CSSProperties = {}; + onHoldContent = ( + <div className="mx_CallView_video_holdContent"> + {onHoldText} + </div> + ); const backgroundAvatarUrl = avatarUrlForMember( - // is it worth getting the size of the div to pass here? + // is it worth getting the size of the div to pass here? this.props.call.getOpponentMember(), 1024, 1024, 'crop', ); backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')'; onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />; - } - if (!this.state.vidMuted) { - localVideoFeed = <VideoFeed type={VideoFeedType.Local} call={this.props.call} />; - } - contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}> - {onHoldBackground} - <VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize} /> - {localVideoFeed} - {onHoldContent} - {callControls} - </div>; - } else { - const avatarSize = this.props.pipMode ? 76 : 160; + contentView = ( + <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}> + {onHoldBackground} + {onHoldContent} + {callControls} + </div> + ); + } else { + const classes = classNames({ + mx_CallView_voice: true, + mx_CallView_voice_hold: isOnHold, + }); + + contentView =( + <div className={classes} onMouseMove={this.onMouseMove}> + <div className="mx_CallView_voice_avatarsContainer"> + <div + className="mx_CallView_voice_avatarContainer" + style={{width: avatarSize, height: avatarSize}} + > + <RoomAvatar + room={callRoom} + height={avatarSize} + width={avatarSize} + /> + </div> + </div> + <div className="mx_CallView_voice_holdText">{onHoldText}</div> + {callControls} + </div> + ); + } + } else if (this.props.call.noIncomingFeeds()) { + // Here we're reusing the css classes from voice on hold, because + // I am lazy. If this gets merged, the CallView might be subject + // to change anyway - I might take an axe to this file in order to + // try to get other things working const classes = classNames({ mx_CallView_voice: true, - mx_CallView_voice_hold: isOnHold, }); + // Saying "Connecting" here isn't really true, but the best thing + // I can come up with, but this might be subject to change as well contentView = <div className={classes} onMouseMove={this.onMouseMove}> <div className="mx_CallView_voice_avatarsContainer"> <div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}> @@ -534,7 +570,33 @@ export default class CallView extends React.Component<IProps, IState> { /> </div> </div> - <div className="mx_CallView_voice_holdText">{onHoldText}</div> + <div className="mx_CallView_voice_holdText">{_t("Connecting")}</div> + {callControls} + </div>; + } else { + const containerClasses = classNames({ + mx_CallView_video: true, + }); + + // TODO: Later the CallView should probably be reworked to support any + // number of feeds but now we can always expect there to be two feeds + const feeds = this.state.feeds.map((feed, i) => { + // Here we check to hide local audio feeds to achieve the same UI/UX + // as before. But once again this might be subject to change + if (feed.isAudioOnly() && feed.isLocal()) return; + return ( + <VideoFeed + key={i} + feed={feed} + call={this.props.call} + pipMode={this.props.pipMode} + onResize={this.props.onResize} + /> + ); + }); + + contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}> + {feeds} {callControls} </div>; } diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 1e950f3a2a..be674630b3 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -18,50 +18,81 @@ import classnames from 'classnames'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import React, {createRef} from 'react'; import SettingsStore from "../../../settings/SettingsStore"; - -export enum VideoFeedType { - Local, - Remote, -} +import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { logger } from 'matrix-js-sdk/src/logger'; +import MemberAvatar from "../avatars/MemberAvatar" +import CallHandler from '../../../CallHandler'; interface IProps { call: MatrixCall, - type: VideoFeedType, + feed: CallFeed, + + // Whether this call view is for picture-in-pictue mode + // otherwise, it's the larger call view when viewing the room the call is in. + // This is sort of a proxy for a number of things but we currently have no + // need to control those things separately, so this is simpler. + pipMode?: boolean; // a callback which is called when the video element is resized // due to a change in video metadata onResize?: (e: Event) => void, } -export default class VideoFeed extends React.Component<IProps> { +interface IState { + audioOnly: boolean; +} + +export default class VideoFeed extends React.Component<IProps, IState> { private vid = createRef<HTMLVideoElement>(); - componentDidMount() { - this.vid.current.addEventListener('resize', this.onResize); - this.setVideoElement(); + constructor(props: IProps) { + super(props); + + this.state = { + audioOnly: this.props.feed.isAudioOnly(), + }; } - componentDidUpdate(prevProps) { - if (this.props.call !== prevProps.call) { - this.setVideoElement(); + componentDidMount() { + this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); + if (!this.vid.current) return; + // A note on calling methods on media elements: + // We used to have queues per media element to serialise all calls on those elements. + // The reason given for this was that load() and play() were racing. However, we now + // never call load() explicitly so this seems unnecessary. However, serialising every + // operation was causing bugs where video would not resume because some play command + // had got stuck and all media operations were queued up behind it. If necessary, we + // should serialise the ones that need to be serialised but then be able to interrupt + // them with another load() which will cancel the pending one, but since we don't call + // load() explicitly, it shouldn't be a problem. - Dave + this.vid.current.srcObject = this.props.feed.stream; + this.vid.current.autoplay = true; + this.vid.current.muted = true; + try { + this.vid.current.play(); + } catch (e) { + logger.info("Failed to play video element with feed", this.props.feed, e); } } componentWillUnmount() { + this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); + if (!this.vid.current) return; this.vid.current.removeEventListener('resize', this.onResize); + this.vid.current.pause(); + this.vid.current.srcObject = null; } - private setVideoElement() { - if (this.props.type === VideoFeedType.Local) { - this.props.call.setLocalVideoElement(this.vid.current); - } else { - this.props.call.setRemoteVideoElement(this.vid.current); - } + onNewStream = (newStream: MediaStream) => { + this.setState({ audioOnly: this.props.feed.isAudioOnly()}); + if (!this.vid.current) return; + this.vid.current.srcObject = newStream; } onResize = (e) => { - if (this.props.onResize) { + if (this.props.onResize && !this.props.feed.isLocal()) { this.props.onResize(e); } }; @@ -69,14 +100,35 @@ export default class VideoFeed extends React.Component<IProps> { render() { const videoClasses = { mx_VideoFeed: true, - mx_VideoFeed_local: this.props.type === VideoFeedType.Local, - mx_VideoFeed_remote: this.props.type === VideoFeedType.Remote, + mx_VideoFeed_local: this.props.feed.isLocal(), + mx_VideoFeed_remote: !this.props.feed.isLocal(), + mx_VideoFeed_voice: this.state.audioOnly, + mx_VideoFeed_video: !this.state.audioOnly, mx_VideoFeed_mirror: ( - this.props.type === VideoFeedType.Local && + this.props.feed.isLocal() && SettingsStore.getValue('VideoView.flipVideoHorizontally') ), }; - return <video className={classnames(videoClasses)} ref={this.vid} />; + if (this.state.audioOnly) { + const callRoomId = CallHandler.roomIdForCall(this.props.call); + const callRoom = MatrixClientPeg.get().getRoom(callRoomId); + const member = callRoom.getMember(this.props.feed.userId); + const avatarSize = this.props.pipMode ? 76 : 160; + + return ( + <div className={classnames(videoClasses)} > + <MemberAvatar + member={member} + height={avatarSize} + width={avatarSize} + /> + </div> + ); + } else { + return ( + <video className={classnames(videoClasses)} ref={this.vid} /> + ); + } } } From 025ac4610155f0b83c2d4c001b608d73e8ffb940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 10 Mar 2021 12:18:36 +0100 Subject: [PATCH 11/64] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 38460a5f6e..ef20d51d53 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -873,6 +873,7 @@ "You held the call <a>Switch</a>": "You held the call <a>Switch</a>", "You held the call <a>Resume</a>": "You held the call <a>Resume</a>", "%(peerName)s held the call": "%(peerName)s held the call", + "Connecting": "Connecting", "Video Call": "Video Call", "Voice Call": "Voice Call", "Fill Screen": "Fill Screen", From ab60c9b5da974c8f68418773e4956e0a7b3b6040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 9 Mar 2021 08:08:59 +0100 Subject: [PATCH 12/64] Add HTMLAudioElement and HTMLVideoElement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/@types/global.d.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 4aa6df5488..ef3922327a 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -110,6 +110,16 @@ declare global { interface HTMLAudioElement { type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string); + } + + interface HTMLVideoElement { + type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string); } interface Element { From 841041123642c7274d285529873d3b92f52d2cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 10 Mar 2021 08:31:01 +0100 Subject: [PATCH 13/64] Handle audio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/CallHandler.tsx | 22 -------- src/CallMediaHandler.js | 3 - src/components/views/voip/VideoFeed.tsx | 75 +++++++++++++++++-------- 3 files changed, 52 insertions(+), 48 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index e090270c95..60c440d145 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -137,21 +137,6 @@ export enum PlaceCallType { ScreenSharing = 'screensharing', } -function getRemoteAudioElement(): HTMLAudioElement { - // this needs to be somewhere at the top of the DOM which - // always exists to avoid audio interruptions. - // Might as well just use DOM. - const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement; - if (!remoteAudioElement) { - console.error( - "Failed to find remoteAudio element - cannot play audio!" + - "You need to add an <audio/> to the DOM.", - ); - return null; - } - return remoteAudioElement; -} - export default class CallHandler { private calls = new Map<string, MatrixCall>(); // roomId -> call private audioPromises = new Map<AudioID, Promise<void>>(); @@ -538,11 +523,6 @@ export default class CallHandler { } } - private setCallAudioElement(call: MatrixCall) { - const audioElement = getRemoteAudioElement(); - if (audioElement) call.setRemoteAudioElement(audioElement); - } - private setCallState(call: MatrixCall, status: CallState) { const mappedRoomId = CallHandler.roomIdForCall(call); @@ -635,7 +615,6 @@ export default class CallHandler { this.calls.set(roomId, call); this.setCallListeners(call); - this.setCallAudioElement(call); this.setActiveCallRoomId(roomId); @@ -787,7 +766,6 @@ export default class CallHandler { const call = this.calls.get(payload.room_id); call.answer(); - this.setCallAudioElement(call); this.setActiveCallRoomId(payload.room_id); CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false); dis.dispatch({ diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js index 8d56467c57..2a619fe7aa 100644 --- a/src/CallMediaHandler.js +++ b/src/CallMediaHandler.js @@ -50,18 +50,15 @@ export default { }, loadDevices: function() { - const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput"); const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); - Matrix.setMatrixCallAudioOutput(audioOutDeviceId); Matrix.setMatrixCallAudioInput(audioDeviceId); Matrix.setMatrixCallVideoInput(videoDeviceId); }, setAudioOutput: function(deviceId) { SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); - Matrix.setMatrixCallAudioOutput(deviceId); }, setAudioInput: function(deviceId) { diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index be674630b3..4ad41b322e 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -23,6 +23,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { logger } from 'matrix-js-sdk/src/logger'; import MemberAvatar from "../avatars/MemberAvatar" import CallHandler from '../../../CallHandler'; +import CallMediaHandler from "../../../CallMediaHandler"; interface IProps { call: MatrixCall, @@ -45,7 +46,8 @@ interface IState { } export default class VideoFeed extends React.Component<IProps, IState> { - private vid = createRef<HTMLVideoElement>(); + private video = createRef<HTMLVideoElement>(); + private audio = createRef<HTMLAudioElement>(); constructor(props: IProps) { super(props); @@ -57,38 +59,64 @@ export default class VideoFeed extends React.Component<IProps, IState> { componentDidMount() { this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); - if (!this.vid.current) return; - // A note on calling methods on media elements: - // We used to have queues per media element to serialise all calls on those elements. - // The reason given for this was that load() and play() were racing. However, we now - // never call load() explicitly so this seems unnecessary. However, serialising every - // operation was causing bugs where video would not resume because some play command - // had got stuck and all media operations were queued up behind it. If necessary, we - // should serialise the ones that need to be serialised but then be able to interrupt - // them with another load() which will cancel the pending one, but since we don't call - // load() explicitly, it shouldn't be a problem. - Dave - this.vid.current.srcObject = this.props.feed.stream; - this.vid.current.autoplay = true; - this.vid.current.muted = true; + + const audioOutput = CallMediaHandler.getAudioOutput(); + const currentMedia = this.getCurrentMedia(); + + currentMedia.srcObject = this.props.feed.stream; + currentMedia.autoplay = true; + currentMedia.muted = false; + try { - this.vid.current.play(); + if (audioOutput) { + // This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where + // it fails. + // It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID + // back to the default after the call is over - Dave + currentMedia.setSinkId(audioOutput); + } } catch (e) { - logger.info("Failed to play video element with feed", this.props.feed, e); + console.error("Couldn't set requested audio output device: using default", e); + logger.warn("Couldn't set requested audio output device: using default", e); + } + + try { + // A note on calling methods on media elements: + // We used to have queues per media element to serialise all calls on those elements. + // The reason given for this was that load() and play() were racing. However, we now + // never call load() explicitly so this seems unnecessary. However, serialising every + // operation was causing bugs where video would not resume because some play command + // had got stuck and all media operations were queued up behind it. If necessary, we + // should serialise the ones that need to be serialised but then be able to interrupt + // them with another load() which will cancel the pending one, but since we don't call + // load() explicitly, it shouldn't be a problem. - Dave + currentMedia.play() + } catch (e) { + logger.info("Failed to play media element with feed", this.props.feed, e); } } componentWillUnmount() { this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); - if (!this.vid.current) return; - this.vid.current.removeEventListener('resize', this.onResize); - this.vid.current.pause(); - this.vid.current.srcObject = null; + this.video.current?.removeEventListener('resize', this.onResize); + + const currentMedia = this.getCurrentMedia(); + currentMedia.pause(); + currentMedia.srcObject = null; + // As per comment in componentDidMount, setting the sink ID back to the + // default once the call is over makes setSinkId work reliably. - Dave + // Since we are not using the same element anymore, the above doesn't + // seem to be necessary - Šimon + } + + getCurrentMedia() { + return this.audio.current || this.video.current; } onNewStream = (newStream: MediaStream) => { this.setState({ audioOnly: this.props.feed.isAudioOnly()}); - if (!this.vid.current) return; - this.vid.current.srcObject = newStream; + const currentMedia = this.getCurrentMedia(); + currentMedia.srcObject = newStream; } onResize = (e) => { @@ -123,11 +151,12 @@ export default class VideoFeed extends React.Component<IProps, IState> { height={avatarSize} width={avatarSize} /> + <audio ref={this.audio}></audio> </div> ); } else { return ( - <video className={classnames(videoClasses)} ref={this.vid} /> + <video className={classnames(videoClasses)} ref={this.video} /> ); } } From 6fdc7a0860131ad5be0950e6e6365d520b801785 Mon Sep 17 00:00:00 2001 From: Aaron Raimist <aaron@raim.ist> Date: Fri, 12 Mar 2021 05:45:22 -0600 Subject: [PATCH 14/64] remove old copyright notice, this is a new file Signed-off-by: Aaron Raimist <aaron@raim.ist> --- src/components/views/dialogs/AccessTokenDialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx index c0b1b929df..220047ac21 100644 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ b/src/components/views/dialogs/AccessTokenDialog.tsx @@ -1,5 +1,4 @@ /* -Copyright 2017 Vector Creations Ltd Copyright 2020 Resynth <resynth1943.net> Licensed under the Apache License, Version 2.0 (the "License"); From ec824c714a5cfd443b00d1f7b2dbc6a3b5acbd39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 17 Mar 2021 16:09:58 +0100 Subject: [PATCH 15/64] Make sure video plays onNewStream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/VideoFeed.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index be674630b3..9874dd07df 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -89,6 +89,7 @@ export default class VideoFeed extends React.Component<IProps, IState> { this.setState({ audioOnly: this.props.feed.isAudioOnly()}); if (!this.vid.current) return; this.vid.current.srcObject = newStream; + this.vid.current.play(); } onResize = (e) => { From 6c5a30109430c561c737dd816b3711fafdc8bffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 17 Mar 2021 16:10:50 +0100 Subject: [PATCH 16/64] Make sure video plays onNewStream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/VideoFeed.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 4ad41b322e..2b75ba73da 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -117,6 +117,7 @@ export default class VideoFeed extends React.Component<IProps, IState> { this.setState({ audioOnly: this.props.feed.isAudioOnly()}); const currentMedia = this.getCurrentMedia(); currentMedia.srcObject = newStream; + currentMedia.play(); } onResize = (e) => { From 4c64dacba42c2b52480cdb2d9d1d7d8cba8550d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Sat, 3 Apr 2021 09:16:08 +0200 Subject: [PATCH 17/64] Fix class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index b831e8c87b..a2ca914373 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -601,7 +601,7 @@ export default class CallView extends React.Component<IProps, IState> { /> </div> </div> - <div className="mx_CallView_voice_holdText">{_t("Connecting")}</div> + <div className="mx_CallView_holdTransferContent">{_t("Connecting")}</div> {callControls} </div>; } else { From c5952f7e236140503a53e253771bc152744fdceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Sun, 4 Apr 2021 08:02:51 +0200 Subject: [PATCH 18/64] Remove VideoFeedType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/VideoFeed.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index ce91f2332a..925e25cee9 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -26,11 +26,6 @@ import CallHandler from '../../../CallHandler'; import CallMediaHandler from "../../../CallMediaHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -export enum VideoFeedType { - Local, - Remote, -} - interface IProps { call: MatrixCall, From 346784e53072dfc44d6a1fa0df28b6709949ebd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Sun, 4 Apr 2021 08:33:53 +0200 Subject: [PATCH 19/64] Add getMember() to CallFeed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/VideoFeed.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 925e25cee9..ad199f18ae 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -19,10 +19,8 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import React, {createRef} from 'react'; import SettingsStore from "../../../settings/SettingsStore"; import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { logger } from 'matrix-js-sdk/src/logger'; import MemberAvatar from "../avatars/MemberAvatar" -import CallHandler from '../../../CallHandler'; import CallMediaHandler from "../../../CallMediaHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; @@ -143,9 +141,7 @@ export default class VideoFeed extends React.Component<IProps, IState> { }; if (this.state.audioOnly) { - const callRoomId = CallHandler.roomIdForCall(this.props.call); - const callRoom = MatrixClientPeg.get().getRoom(callRoomId); - const member = callRoom.getMember(this.props.feed.userId); + const member = this.props.feed.getMember(); const avatarSize = this.props.pipMode ? 76 : 160; return ( From 9324dec0d64d8d6c4d96b73dea1d8e6234dc016a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Sun, 4 Apr 2021 08:50:25 +0200 Subject: [PATCH 20/64] Rename audioOnly to videoMuted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes more sense and will match a possible mute events MSC Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallView.tsx | 2 +- src/components/views/voip/VideoFeed.tsx | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index a2ca914373..8e99437218 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -614,7 +614,7 @@ export default class CallView extends React.Component<IProps, IState> { const feeds = this.state.feeds.map((feed, i) => { // Here we check to hide local audio feeds to achieve the same UI/UX // as before. But once again this might be subject to change - if (feed.isAudioOnly() && feed.isLocal()) return; + if (feed.isVideoMuted() && feed.isLocal()) return; return ( <VideoFeed key={i} diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index ad199f18ae..8e518981f8 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -41,7 +41,8 @@ interface IProps { } interface IState { - audioOnly: boolean; + audioMuted: boolean; + videoMuted: boolean; } @@ -54,7 +55,8 @@ export default class VideoFeed extends React.Component<IProps, IState> { super(props); this.state = { - audioOnly: this.props.feed.isAudioOnly(), + audioMuted: this.props.feed.isAudioMuted(), + videoMuted: this.props.feed.isVideoMuted(), }; } @@ -115,7 +117,10 @@ export default class VideoFeed extends React.Component<IProps, IState> { } onNewStream = (newStream: MediaStream) => { - this.setState({ audioOnly: this.props.feed.isAudioOnly()}); + this.setState({ + audioMuted: this.props.feed.isAudioMuted(), + videoMuted: this.props.feed.isVideoMuted(), + }); const currentMedia = this.getCurrentMedia(); currentMedia.srcObject = newStream; currentMedia.play(); @@ -132,15 +137,15 @@ export default class VideoFeed extends React.Component<IProps, IState> { mx_VideoFeed: true, mx_VideoFeed_local: this.props.feed.isLocal(), mx_VideoFeed_remote: !this.props.feed.isLocal(), - mx_VideoFeed_voice: this.state.audioOnly, - mx_VideoFeed_video: !this.state.audioOnly, + mx_VideoFeed_voice: this.state.videoMuted, + mx_VideoFeed_video: !this.state.videoMuted, mx_VideoFeed_mirror: ( this.props.feed.isLocal() && SettingsStore.getValue('VideoView.flipVideoHorizontally') ), }; - if (this.state.audioOnly) { + if (this.state.videoMuted) { const member = this.props.feed.getMember(); const avatarSize = this.props.pipMode ? 76 : 160; From 16e6f84f89d2f64b3fe68c51a6da5078d945e86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Sun, 4 Apr 2021 09:04:17 +0200 Subject: [PATCH 21/64] Display local feeds when connecting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallView.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 8e99437218..ea5411486f 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -589,9 +589,25 @@ export default class CallView extends React.Component<IProps, IState> { mx_CallView_voice: true, }); + const feeds = this.state.feeds.map((feed, i) => { + // Here we check to hide local audio feeds to achieve the same UI/UX + // as before. But once again this might be subject to change + if (feed.isVideoMuted() && feed.isLocal()) return; + return ( + <VideoFeed + key={i} + feed={feed} + call={this.props.call} + pipMode={this.props.pipMode} + onResize={this.props.onResize} + /> + ); + }); + // Saying "Connecting" here isn't really true, but the best thing // I can come up with, but this might be subject to change as well contentView = <div className={classes} onMouseMove={this.onMouseMove}> + {feeds} <div className="mx_CallView_voice_avatarsContainer"> <div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}> <RoomAvatar From a3da5ee6e6fec3dfce097a16bdfeeae6a0754aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Thu, 8 Apr 2021 14:32:53 +0200 Subject: [PATCH 22/64] Don't play audio if the feed is local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/VideoFeed.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 8e518981f8..663c2e4613 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -68,7 +68,8 @@ export default class VideoFeed extends React.Component<IProps, IState> { currentMedia.srcObject = this.props.feed.stream; currentMedia.autoplay = true; - currentMedia.muted = false; + // Don't play audio if the feed is local + currentMedia.muted = this.props.feed.isLocal(); try { if (audioOutput) { From 56b15edc58e5c7dc09f765674c659ca77f18c698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Mon, 12 Apr 2021 16:19:05 +0200 Subject: [PATCH 23/64] Properly handle media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This might have resulted in the wrong speaker being used or worse Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/VideoFeed.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 663c2e4613..43696defb9 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -63,6 +63,10 @@ export default class VideoFeed extends React.Component<IProps, IState> { componentDidMount() { this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); + this.playMedia(); + } + + playMedia() { const audioOutput = CallMediaHandler.getAudioOutput(); const currentMedia = this.getCurrentMedia(); @@ -117,14 +121,12 @@ export default class VideoFeed extends React.Component<IProps, IState> { return this.audio.current || this.video.current; } - onNewStream = (newStream: MediaStream) => { + onNewStream = () => { this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); - const currentMedia = this.getCurrentMedia(); - currentMedia.srcObject = newStream; - currentMedia.play(); + this.playMedia(); } onResize = (e) => { From 33fd09d7771fb014b97c1ae0425c5e33c96722ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 13 Apr 2021 20:21:03 +0200 Subject: [PATCH 24/64] Make private MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/VideoFeed.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 43696defb9..23e51626d9 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -66,7 +66,7 @@ export default class VideoFeed extends React.Component<IProps, IState> { this.playMedia(); } - playMedia() { + private playMedia() { const audioOutput = CallMediaHandler.getAudioOutput(); const currentMedia = this.getCurrentMedia(); @@ -117,11 +117,11 @@ export default class VideoFeed extends React.Component<IProps, IState> { // seem to be necessary - Šimon } - getCurrentMedia() { + private getCurrentMedia() { return this.audio.current || this.video.current; } - onNewStream = () => { + private onNewStream = () => { this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), @@ -129,7 +129,7 @@ export default class VideoFeed extends React.Component<IProps, IState> { this.playMedia(); } - onResize = (e) => { + private onResize = (e) => { if (this.props.onResize && !this.props.feed.isLocal()) { this.props.onResize(e); } From 2cfd4659e13543caa4cc913be06ca199a820d34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Fri, 16 Apr 2021 12:50:23 +0200 Subject: [PATCH 25/64] Add separate mx_CallView_content class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- res/css/views/voip/_CallView.scss | 12 ++++++------ src/components/views/voip/CallView.tsx | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 90a3ca4209..0a3865479a 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -64,14 +64,17 @@ limitations under the License. } } -.mx_CallView_voice { +.mx_CallView_content { position: relative; display: flex; - flex-direction: column; + border-radius: 8px; +} + +.mx_CallView_voice { align-items: center; justify-content: center; + flex-direction: column; background-color: $inverted-bg-color; - border-radius: 8px; } .mx_CallView_voice_avatarsContainer { @@ -108,11 +111,8 @@ limitations under the License. .mx_CallView_video { width: 100%; height: 100%; - position: relative; z-index: 30; - border-radius: 8px; overflow: hidden; - display: flex; } .mx_CallView_video_hold { diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index ea5411486f..544e54ec9d 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -536,6 +536,7 @@ export default class CallView extends React.Component<IProps, IState> { if (isOnHold || transfereeCall) { if (this.props.call.type === CallType.Video) { const containerClasses = classNames({ + mx_CallView_content: true, mx_CallView_video: true, mx_CallView_video_hold: isOnHold, }); @@ -557,6 +558,7 @@ export default class CallView extends React.Component<IProps, IState> { ); } else { const classes = classNames({ + mx_CallView_content: true, mx_CallView_voice: true, mx_CallView_voice_hold: isOnHold, }); @@ -586,6 +588,7 @@ export default class CallView extends React.Component<IProps, IState> { // to change anyway - I might take an axe to this file in order to // try to get other things working const classes = classNames({ + mx_CallView_content: true, mx_CallView_voice: true, }); @@ -622,6 +625,7 @@ export default class CallView extends React.Component<IProps, IState> { </div>; } else { const containerClasses = classNames({ + mx_CallView_content: true, mx_CallView_video: true, }); From 758112dda95d36c0a09891b9e86482a92ee49ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Mon, 19 Apr 2021 07:42:32 +0200 Subject: [PATCH 26/64] Add missing somicolons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallView.tsx | 2 +- src/components/views/voip/VideoFeed.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 544e54ec9d..549e3da188 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -192,7 +192,7 @@ export default class CallView extends React.Component<IProps, IState> { private onFeedsChanged = (newFeeds: Array<CallFeed>) => { this.setState({feeds: newFeeds}); - } + }; private onCallLocalHoldUnhold = () => { this.setState({ diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 23e51626d9..f3ffdd62c5 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -127,7 +127,7 @@ export default class VideoFeed extends React.Component<IProps, IState> { videoMuted: this.props.feed.isVideoMuted(), }); this.playMedia(); - } + }; private onResize = (e) => { if (this.props.onResize && !this.props.feed.isLocal()) { From fbb8cfb1884fc62beb3be22a5baef21b84803418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Fri, 23 Apr 2021 19:41:55 +0200 Subject: [PATCH 27/64] Rework how media element are handled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- res/css/views/voip/_VideoFeed.scss | 2 + src/components/views/voip/VideoFeed.tsx | 81 +++++++++++++++---------- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 46cdf4f52c..6f51353552 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -22,6 +22,8 @@ limitations under the License. .mx_VideoFeed_video { background-color: #000; + width: 100%; + height: 100%; } .mx_VideoFeed_remote { diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index f3ffdd62c5..75206e177f 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -62,32 +62,40 @@ export default class VideoFeed extends React.Component<IProps, IState> { componentDidMount() { this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); - - this.playMedia(); + this.playAllMedia(); } - private playMedia() { - const audioOutput = CallMediaHandler.getAudioOutput(); - const currentMedia = this.getCurrentMedia(); + componentWillUnmount() { + this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); + this.video.current?.removeEventListener('resize', this.onResize); + this.stopAllMedia(); + } - currentMedia.srcObject = this.props.feed.stream; - currentMedia.autoplay = true; - // Don't play audio if the feed is local - currentMedia.muted = this.props.feed.isLocal(); + private playMediaElement(element: HTMLVideoElement | HTMLAudioElement) { + if (element instanceof HTMLAudioElement) { + const audioOutput = CallMediaHandler.getAudioOutput(); - try { - if (audioOutput) { - // This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where - // it fails. - // It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID - // back to the default after the call is over - Dave - currentMedia.setSinkId(audioOutput); + // Don't play audio if the feed is local + element.muted = this.props.feed.isLocal(); + + if (audioOutput && !element.muted) { + try { + // This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where + // it fails. + // It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID + // back to the default after the call is over - Dave + element.setSinkId(audioOutput); + } catch (e) { + console.error("Couldn't set requested audio output device: using default", e); + logger.warn("Couldn't set requested audio output device: using default", e); + } } - } catch (e) { - console.error("Couldn't set requested audio output device: using default", e); - logger.warn("Couldn't set requested audio output device: using default", e); + } else { + element.muted = true; } + element.srcObject = this.props.feed.stream; + element.autoplay = true; try { // A note on calling methods on media elements: // We used to have queues per media element to serialise all calls on those elements. @@ -98,27 +106,30 @@ export default class VideoFeed extends React.Component<IProps, IState> { // should serialise the ones that need to be serialised but then be able to interrupt // them with another load() which will cancel the pending one, but since we don't call // load() explicitly, it shouldn't be a problem. - Dave - currentMedia.play() + element.play() } catch (e) { logger.info("Failed to play media element with feed", this.props.feed, e); } } - componentWillUnmount() { - this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); - this.video.current?.removeEventListener('resize', this.onResize); + private stopMediaElement(element: HTMLAudioElement | HTMLVideoElement) { + element.pause(); + element.src = null; - const currentMedia = this.getCurrentMedia(); - currentMedia.pause(); - currentMedia.srcObject = null; // As per comment in componentDidMount, setting the sink ID back to the // default once the call is over makes setSinkId work reliably. - Dave // Since we are not using the same element anymore, the above doesn't // seem to be necessary - Šimon } - private getCurrentMedia() { - return this.audio.current || this.video.current; + private playAllMedia() { + this.playMediaElement(this.audio.current); + if (this.video.current) this.playMediaElement(this.video.current); + } + + private stopAllMedia() { + this.stopMediaElement(this.audio.current) + if (this.video.current) this.stopMediaElement(this.video.current); } private onNewStream = () => { @@ -126,7 +137,7 @@ export default class VideoFeed extends React.Component<IProps, IState> { audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); - this.playMedia(); + this.playAllMedia(); }; private onResize = (e) => { @@ -141,13 +152,16 @@ export default class VideoFeed extends React.Component<IProps, IState> { mx_VideoFeed_local: this.props.feed.isLocal(), mx_VideoFeed_remote: !this.props.feed.isLocal(), mx_VideoFeed_voice: this.state.videoMuted, - mx_VideoFeed_video: !this.state.videoMuted, mx_VideoFeed_mirror: ( this.props.feed.isLocal() && SettingsStore.getValue('VideoView.flipVideoHorizontally') ), }; + const audio = ( + <audio ref={this.audio} /> + ); + if (this.state.videoMuted) { const member = this.props.feed.getMember(); const avatarSize = this.props.pipMode ? 76 : 160; @@ -159,12 +173,15 @@ export default class VideoFeed extends React.Component<IProps, IState> { height={avatarSize} width={avatarSize} /> - <audio ref={this.audio}></audio> + {audio} </div> ); } else { return ( - <video className={classnames(videoClasses)} ref={this.video} /> + <div className={classnames(videoClasses)}> + <video className="mx_VideoFeed_video" ref={this.video} /> + {audio} + </div> ); } } From 81164fe152e08578296096b178b6220470800965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Mon, 26 Apr 2021 16:09:21 +0200 Subject: [PATCH 28/64] Add comment about the js-sdk and new incoming feeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallView.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 549e3da188..925d5cf109 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -629,8 +629,9 @@ export default class CallView extends React.Component<IProps, IState> { mx_CallView_video: true, }); - // TODO: Later the CallView should probably be reworked to support any - // number of feeds but now we can always expect there to be two feeds + // TODO: Later the CallView should probably be reworked to support + // any number of feeds but now we can always expect there to be two + // feeds. This is because the js-sdk ignores any new incoming streams const feeds = this.state.feeds.map((feed, i) => { // Here we check to hide local audio feeds to achieve the same UI/UX // as before. But once again this might be subject to change From e79f94d01ee6fe2edf276989c04051d9c32f387f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Mon, 26 Apr 2021 20:49:11 +0200 Subject: [PATCH 29/64] Somewhat fix the local video issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- res/css/views/voip/_VideoFeed.scss | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 6f51353552..9307a372e1 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -20,17 +20,18 @@ limitations under the License. background-color: $inverted-bg-color; } -.mx_VideoFeed_video { - background-color: #000; - width: 100%; - height: 100%; -} .mx_VideoFeed_remote { flex: 1; display: flex; justify-content: center; align-items: center; + + .mx_VideoFeed_video { + background-color: #000; + width: 100%; + height: 100%; + } } .mx_VideoFeed_local { @@ -41,6 +42,12 @@ limitations under the License. top: 10px; z-index: 100; border-radius: 4px; + + .mx_VideoFeed_video { + background-color: transparent; + width: 100%; + height: 100%; + } } .mx_VideoFeed_mirror { From 9af176f5e2964ec48d26e73bc4d2880eb8d4900a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 10:20:49 +0200 Subject: [PATCH 30/64] Add AudioFeed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/AudioFeed.tsx | 97 +++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/components/views/voip/AudioFeed.tsx diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx new file mode 100644 index 0000000000..c78f0c0fc8 --- /dev/null +++ b/src/components/views/voip/AudioFeed.tsx @@ -0,0 +1,97 @@ +/* +Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {createRef} from 'react'; +import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; +import { logger } from 'matrix-js-sdk/src/logger'; +import CallMediaHandler from "../../../CallMediaHandler"; + +interface IProps { + feed: CallFeed, +} + +export default class AudioFeed extends React.Component<IProps> { + private element = createRef<HTMLAudioElement>(); + + componentDidMount() { + this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); + this.playMedia(); + } + + componentWillUnmount() { + this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); + this.stopMedia(); + } + + private playMedia() { + const element = this.element.current; + const audioOutput = CallMediaHandler.getAudioOutput(); + + if (audioOutput) { + try { + // This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where + // it fails. + // It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID + // back to the default after the call is over - Dave + element.setSinkId(audioOutput); + } catch (e) { + console.error("Couldn't set requested audio output device: using default", e); + logger.warn("Couldn't set requested audio output device: using default", e); + } + } + + element.muted = false; + element.srcObject = this.props.feed.stream; + element.autoplay = true; + + try { + // A note on calling methods on media elements: + // We used to have queues per media element to serialise all calls on those elements. + // The reason given for this was that load() and play() were racing. However, we now + // never call load() explicitly so this seems unnecessary. However, serialising every + // operation was causing bugs where video would not resume because some play command + // had got stuck and all media operations were queued up behind it. If necessary, we + // should serialise the ones that need to be serialised but then be able to interrupt + // them with another load() which will cancel the pending one, but since we don't call + // load() explicitly, it shouldn't be a problem. - Dave + element.play() + } catch (e) { + logger.info("Failed to play media element with feed", this.props.feed, e); + } + } + + private stopMedia() { + const element = this.element.current; + + element.pause(); + element.src = null; + + // As per comment in componentDidMount, setting the sink ID back to the + // default once the call is over makes setSinkId work reliably. - Dave + // Since we are not using the same element anymore, the above doesn't + // seem to be necessary - Šimon + } + + private onNewStream = () => { + this.playMedia(); + }; + + render() { + return ( + <audio ref={this.element} /> + ); + } +} From 08251a761dcd6d5e804b95075ef46d6af1c984ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 10:58:32 +0200 Subject: [PATCH 31/64] Add AudioFeedArrayForCall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- .../views/voip/AudioFeedArrayForCall.tsx | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/components/views/voip/AudioFeedArrayForCall.tsx diff --git a/src/components/views/voip/AudioFeedArrayForCall.tsx b/src/components/views/voip/AudioFeedArrayForCall.tsx new file mode 100644 index 0000000000..fac2a22dcd --- /dev/null +++ b/src/components/views/voip/AudioFeedArrayForCall.tsx @@ -0,0 +1,73 @@ +/* +Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import AudioFeed from "./AudioFeed" +import { CallEvent, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; +import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; + +interface IProps { + call: MatrixCall; +} + +interface IState { + feeds: Array<CallFeed>; + onHold: boolean; +} + +export default class AudioFeedArrayForCall extends React.Component<IProps, IState> { + constructor(props: IProps) { + super(props); + + this.state = { + feeds: [], + onHold: false, + }; + } + + componentDidMount() { + this.props.call.addListener(CallEvent.FeedsChanged, this.onFeedsChanged); + this.props.call.addListener(CallEvent.HoldUnhold, this.onHoldUnhold); + } + + componentWillUnmount() { + this.props.call.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged); + this.props.call.removeListener(CallEvent.HoldUnhold, this.onHoldUnhold); + } + + onFeedsChanged = () => { + this.setState({ + feeds: this.props.call.getRemoteFeeds(), + }); + } + + onHoldUnhold = (onHold: boolean) => { + this.setState({onHold: onHold}); + } + + render() { + // If we are onHold don't render any audio elements + if (this.state.onHold) return null; + + const feeds = this.state.feeds.map((feed, i) => { + return ( + <AudioFeed feed={feed} key={i} /> + ); + }); + + return feeds; + } +} From b88033accc3db8e12abf9ea41b0e885f6077d804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 11:01:36 +0200 Subject: [PATCH 32/64] Make CallHandler into an EventEmitter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/CallHandler.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 4a5c7c41b4..8e8f120852 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -86,6 +86,7 @@ import { Action } from './dispatcher/actions'; import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring"; +import EventEmitter from 'events'; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -137,7 +138,11 @@ export enum PlaceCallType { ScreenSharing = 'screensharing', } -export default class CallHandler { +export enum CallHandlerEvent { + CallsChanged = "calls_changed", +} + +export default class CallHandler extends EventEmitter { private calls = new Map<string, MatrixCall>(); // roomId -> call // Calls started as an attended transfer, ie. with the intention of transferring another // call with a different party to this one. @@ -482,6 +487,7 @@ export default class CallHandler { } this.calls.set(mappedRoomId, newCall); + this.emit(CallHandlerEvent.CallsChanged, this.calls); this.setCallListeners(newCall); this.setCallState(newCall, newCall.state); }); @@ -618,6 +624,7 @@ export default class CallHandler { const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId); this.calls.set(roomId, call); + this.emit(CallHandlerEvent.CallsChanged, this.calls); if (transferee) { this.transferees[call.callId] = transferee; } @@ -745,6 +752,7 @@ export default class CallHandler { Analytics.trackEvent('voip', 'receiveCall', 'type', call.type); this.calls.set(mappedRoomId, call) + this.emit(CallHandlerEvent.CallsChanged, this.calls); this.setCallListeners(call); // get ready to send encrypted events in the room, so if the user does answer From a220b8b572294031bf49c449d22590ed064eb565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 11:59:08 +0200 Subject: [PATCH 33/64] Wire up AudioFeedArrayForCall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/structures/LoggedInView.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 0255a3bf35..c4b9696807 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -59,6 +59,9 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi import { IOpts } from "../../createRoom"; import SpacePanel from "../views/spaces/SpacePanel"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import CallHandler, { CallHandlerEvent } from '../../CallHandler'; +import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall'; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -119,6 +122,7 @@ interface IState { usageLimitEventContent?: IUsageLimit; usageLimitEventTs?: number; useCompactLayout: boolean; + activeCalls: Array<MatrixCall>; } /** @@ -160,6 +164,7 @@ class LoggedInView extends React.Component<IProps, IState> { // use compact timeline view useCompactLayout: SettingsStore.getValue('useCompactLayout'), usageLimitDismissed: false, + activeCalls: [], }; // stash the MatrixClient in case we log out before we are unmounted @@ -175,6 +180,7 @@ class LoggedInView extends React.Component<IProps, IState> { componentDidMount() { document.addEventListener('keydown', this._onNativeKeyDown, false); + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); this._updateServerNoticeEvents(); @@ -199,6 +205,7 @@ class LoggedInView extends React.Component<IProps, IState> { componentWillUnmount() { document.removeEventListener('keydown', this._onNativeKeyDown, false); + CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents); @@ -206,6 +213,12 @@ class LoggedInView extends React.Component<IProps, IState> { this.resizer.detach(); } + private onCallsChanged = () => { + this.setState({ + activeCalls: CallHandler.sharedInstance().getAllActiveCalls(), + }); + }; + // Child components assume that the client peg will not be null, so give them some // sort of assurance here by only allowing a re-render if the client is truthy. // @@ -661,6 +674,12 @@ class LoggedInView extends React.Component<IProps, IState> { bodyClasses += ' mx_MatrixChat_useCompactLayout'; } + const audioFeedArraysForCalls = this.state.activeCalls.map((call) => { + return ( + <AudioFeedArrayForCall call={call} key={call.callId} /> + ); + }); + return ( <MatrixClientContext.Provider value={this._matrixClient}> <div @@ -685,6 +704,7 @@ class LoggedInView extends React.Component<IProps, IState> { <CallContainer /> <NonUrgentToastContainer /> <HostSignupContainer /> + {audioFeedArraysForCalls} </MatrixClientContext.Provider> ); } From b612b252e194b2a0044d8323502cd66a9a788af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 11:59:26 +0200 Subject: [PATCH 34/64] Fix a type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/VideoFeed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 75206e177f..d47e4d376a 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -29,7 +29,7 @@ interface IProps { feed: CallFeed, - // Whether this call view is for picture-in-pictue mode + // Whether this call view is for picture-in-picture mode // otherwise, it's the larger call view when viewing the room the call is in. // This is sort of a proxy for a number of things but we currently have no // need to control those things separately, so this is simpler. From f3a7ffca602bc0b248cca207a11f0ebb45172988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 12:02:41 +0200 Subject: [PATCH 35/64] Remove audio element from VideoFeed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/VideoFeed.tsx | 67 ++++++------------------- 1 file changed, 16 insertions(+), 51 deletions(-) diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index d47e4d376a..d22fa055ce 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -21,7 +21,6 @@ import SettingsStore from "../../../settings/SettingsStore"; import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; import { logger } from 'matrix-js-sdk/src/logger'; import MemberAvatar from "../avatars/MemberAvatar" -import CallMediaHandler from "../../../CallMediaHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; interface IProps { @@ -45,11 +44,9 @@ interface IState { videoMuted: boolean; } - @replaceableComponent("views.voip.VideoFeed") export default class VideoFeed extends React.Component<IProps, IState> { - private video = createRef<HTMLVideoElement>(); - private audio = createRef<HTMLAudioElement>(); + private element = createRef<HTMLVideoElement>(); constructor(props: IProps) { super(props); @@ -62,38 +59,20 @@ export default class VideoFeed extends React.Component<IProps, IState> { componentDidMount() { this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); - this.playAllMedia(); + this.playMedia(); } componentWillUnmount() { this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); - this.video.current?.removeEventListener('resize', this.onResize); - this.stopAllMedia(); + this.element.current?.removeEventListener('resize', this.onResize); + this.stopMedia(); } - private playMediaElement(element: HTMLVideoElement | HTMLAudioElement) { - if (element instanceof HTMLAudioElement) { - const audioOutput = CallMediaHandler.getAudioOutput(); - - // Don't play audio if the feed is local - element.muted = this.props.feed.isLocal(); - - if (audioOutput && !element.muted) { - try { - // This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where - // it fails. - // It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID - // back to the default after the call is over - Dave - element.setSinkId(audioOutput); - } catch (e) { - console.error("Couldn't set requested audio output device: using default", e); - logger.warn("Couldn't set requested audio output device: using default", e); - } - } - } else { - element.muted = true; - } - + private playMedia() { + const element = this.element.current; + if (!element) return; + // We play audio in AudioFeed, not here + element.muted = true; element.srcObject = this.props.feed.stream; element.autoplay = true; try { @@ -112,7 +91,10 @@ export default class VideoFeed extends React.Component<IProps, IState> { } } - private stopMediaElement(element: HTMLAudioElement | HTMLVideoElement) { + private stopMedia() { + const element = this.element.current; + if (!element) return; + element.pause(); element.src = null; @@ -122,22 +104,12 @@ export default class VideoFeed extends React.Component<IProps, IState> { // seem to be necessary - Šimon } - private playAllMedia() { - this.playMediaElement(this.audio.current); - if (this.video.current) this.playMediaElement(this.video.current); - } - - private stopAllMedia() { - this.stopMediaElement(this.audio.current) - if (this.video.current) this.stopMediaElement(this.video.current); - } - private onNewStream = () => { this.setState({ audioMuted: this.props.feed.isAudioMuted(), videoMuted: this.props.feed.isVideoMuted(), }); - this.playAllMedia(); + this.playMedia(); }; private onResize = (e) => { @@ -152,16 +124,13 @@ export default class VideoFeed extends React.Component<IProps, IState> { mx_VideoFeed_local: this.props.feed.isLocal(), mx_VideoFeed_remote: !this.props.feed.isLocal(), mx_VideoFeed_voice: this.state.videoMuted, + mx_VideoFeed_video: !this.state.videoMuted, mx_VideoFeed_mirror: ( this.props.feed.isLocal() && SettingsStore.getValue('VideoView.flipVideoHorizontally') ), }; - const audio = ( - <audio ref={this.audio} /> - ); - if (this.state.videoMuted) { const member = this.props.feed.getMember(); const avatarSize = this.props.pipMode ? 76 : 160; @@ -173,15 +142,11 @@ export default class VideoFeed extends React.Component<IProps, IState> { height={avatarSize} width={avatarSize} /> - {audio} </div> ); } else { return ( - <div className={classnames(videoClasses)}> - <video className="mx_VideoFeed_video" ref={this.video} /> - {audio} - </div> + <video className={classnames(videoClasses)} ref={this.element} /> ); } } From dacb161a64004b00cdd878ed6e4453a88c3edb9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 12:03:01 +0200 Subject: [PATCH 36/64] Fix the look of video feeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- res/css/views/voip/_VideoFeed.scss | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 9307a372e1..7d85ac264e 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -22,31 +22,28 @@ limitations under the License. .mx_VideoFeed_remote { - flex: 1; + width: 100%; + height: 100%; display: flex; justify-content: center; align-items: center; - .mx_VideoFeed_video { + &.mx_VideoFeed_video { background-color: #000; - width: 100%; - height: 100%; } } .mx_VideoFeed_local { - width: 25%; - height: 25%; + max-width: 25%; + max-height: 25%; position: absolute; right: 10px; top: 10px; z-index: 100; border-radius: 4px; - .mx_VideoFeed_video { + &.mx_VideoFeed_video { background-color: transparent; - width: 100%; - height: 100%; } } From a6ad574f4e1e4e69f94698772688a9caa69804c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 12:05:21 +0200 Subject: [PATCH 37/64] Fix typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 925d5cf109..e1a0289d9e 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -40,11 +40,11 @@ interface IProps { // Another ongoing call to display information about secondaryCall?: MatrixCall, - // a callback which is called when the content in the callview changes + // a callback which is called when the content in the CallView changes // in a way that is likely to cause a resize. onResize?: any; - // Whether this call view is for picture-in-pictue mode + // Whether this call view is for picture-in-picture mode // otherwise, it's the larger call view when viewing the room the call is in. // This is sort of a proxy for a number of things but we currently have no // need to control those things separately, so this is simpler. From e367725cce27790bf01b43a1ec5ea9c44db27d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 12:05:46 +0200 Subject: [PATCH 38/64] Fix casing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index e1a0289d9e..7be473f78f 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -312,7 +312,7 @@ export default class CallView extends React.Component<IProps, IState> { } // we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire - // Note that this assumes we always have a callview on screen at any given time + // Note that this assumes we always have a CallView on screen at any given time // CallHandler would probably be a better place for this private onNativeKeyDown = ev => { let handled = false; From 8d014b7fa2dc2b34de2a23ef5bb1a4e998ad3dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 27 Apr 2021 12:23:21 +0200 Subject: [PATCH 39/64] Use getLocalFeeds() for better clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 7be473f78f..273337ff1e 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -592,10 +592,10 @@ export default class CallView extends React.Component<IProps, IState> { mx_CallView_voice: true, }); - const feeds = this.state.feeds.map((feed, i) => { + const feeds = this.props.call.getLocalFeeds().map((feed, i) => { // Here we check to hide local audio feeds to achieve the same UI/UX // as before. But once again this might be subject to change - if (feed.isVideoMuted() && feed.isLocal()) return; + if (feed.isVideoMuted()) return; return ( <VideoFeed key={i} From 6754a0b48388d6349c499f9c7b231bf32c0d6fa4 Mon Sep 17 00:00:00 2001 From: Aaron Raimist <aaron@raim.ist> Date: Tue, 27 Apr 2021 19:12:20 -0500 Subject: [PATCH 40/64] Switch to <details> Signed-off-by: Aaron Raimist <aaron@raim.ist> --- .../tabs/user/_HelpUserSettingsTab.scss | 31 +++++++++++++ .../views/dialogs/AccessTokenDialog.tsx | 38 ---------------- .../settings/tabs/user/HelpUserSettingsTab.js | 44 +++++++++++++------ src/i18n/strings/en_EN.json | 7 ++- 4 files changed, 64 insertions(+), 56 deletions(-) delete mode 100644 src/components/views/dialogs/AccessTokenDialog.tsx diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss index 109edfff81..0f879d209e 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss @@ -22,3 +22,34 @@ limitations under the License. .mx_HelpUserSettingsTab span.mx_AccessibleButton { word-break: break-word; } + +.mx_HelpUserSettingsTab code { + word-break: break-all; + user-select: all; +} + +.mx_HelpUserSettingsTab_accessToken { + display: flex; + justify-content: space-between; + border-radius: 5px; + border: solid 1px $light-fg-color; + margin-bottom: 10px; + margin-top: 10px; + padding: 10px; +} + +.mx_HelpUserSettingsTab_accessToken_copy { + flex-shrink: 0; + cursor: pointer; + margin-left: 20px; + display: inherit; +} + +.mx_HelpUserSettingsTab_accessToken_copy > div { + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; + margin-left: 5px; + width: 20px; + height: 20px; + background-repeat: no-repeat; +} diff --git a/src/components/views/dialogs/AccessTokenDialog.tsx b/src/components/views/dialogs/AccessTokenDialog.tsx deleted file mode 100644 index c0b1b929df..0000000000 --- a/src/components/views/dialogs/AccessTokenDialog.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd -Copyright 2020 Resynth <resynth1943.net> - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import { _t } from '../../../languageHandler'; -import QuestionDialog from './QuestionDialog'; - -type IProps = Exclude< - React.ComponentProps<QuestionDialog>, - "title" | "danger" | "description" ->; - -export default function AccessTokenDialog(props: IProps) { - return ( - <QuestionDialog - {...props} - title="Reveal Access Token" - danger={true} - description={_t( - "Your access token gives full access to your account. Do not share it with anyone.", - )} - ></QuestionDialog> - ); -} diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js index e52bc1a3fb..cf1a28ef76 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -20,6 +20,7 @@ import PropTypes from 'prop-types'; import {_t, getCurrentLanguage} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; +import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton'; import SdkConfig from "../../../../../SdkConfig"; import createRoom from "../../../../../createRoom"; import Modal from "../../../../../Modal"; @@ -27,8 +28,11 @@ import * as sdk from "../../../../../"; import PlatformPeg from "../../../../../PlatformPeg"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import UpdateCheckButton from "../../UpdateCheckButton"; -import AccessTokenDialog from '../../../dialogs/AccessTokenDialog'; import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import {copyPlaintext} from "../../../../../utils/strings"; +import * as ContextMenu from "../../../../structures/ContextMenu"; +import {toRightOf} from "../../../../structures/ContextMenu"; + @replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab") export default class HelpUserSettingsTab extends React.Component { @@ -151,15 +155,18 @@ export default class HelpUserSettingsTab extends React.Component { ); } - onAccessTokenSpoilerClick = async (event) => { - // React throws away the event before we can use it (we are async, after all). - event.persist(); + onAccessTokenCopyClick = async (e) => { + e.preventDefault(); + const target = e.target; // copy target before we go async and React throws it away - // We make the user accept a scary popup to combat Social Engineering. No peeking! - await Modal.createTrackedDialog('Reveal Access Token', '', AccessTokenDialog).finished; - - // Pass it onto the handler. - this._showSpoiler(event); + const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken()); + const buttonRect = target.getBoundingClientRect(); + const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu'); + const {close} = ContextMenu.createMenu(GenericTextContextMenu, { + ...toRightOf(buttonRect, 2), + message: successful ? _t('Copied!') : _t('Failed to copy'), + }); + target.onmouseleave = close; } render() { @@ -279,11 +286,20 @@ export default class HelpUserSettingsTab extends React.Component { <div className='mx_SettingsTab_subsectionText'> {_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br /> {_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br /> - {_t("Access Token:") + ' '} - <AccessibleButton element="span" onClick={this.onAccessTokenSpoilerClick} - data-spoiler={MatrixClientPeg.get().getAccessToken()}> - <{ _t("click to reveal") }> - </AccessibleButton> + <br /> + <details> + <summary>{_t("Access Token")}</summary><br /> + { _t("Your access token gives full access to your account." + + " Do not share it with anyone." ) } + <div className="mx_HelpUserSettingsTab_accessToken"> + <code>{MatrixClientPeg.get().getAccessToken()}</code> + <AccessibleTooltipButton + title={_t("Copy")} + onClick={this.onAccessTokenCopyClick} + className="mx_HelpUserSettingsTab_accessToken_copy" + /> + </div> + </details><br /> <div className='mx_HelpUserSettingsTab_debugButton'> <AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'> {_t("Clear cache and reload")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b110af8749..99451dabd6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1251,8 +1251,9 @@ "olm version:": "olm version:", "Homeserver is": "Homeserver is", "Identity Server is": "Identity Server is", - "Access Token:": "Access Token:", - "click to reveal": "click to reveal", + "Access Token": "Access Token", + "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", + "Copy": "Copy", "Clear cache and reload": "Clear cache and reload", "Labs": "Labs", "Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.", @@ -2016,7 +2017,6 @@ "Add a new server...": "Add a new server...", "%(networkName)s rooms": "%(networkName)s rooms", "Matrix rooms": "Matrix rooms", - "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", "Space selection": "Space selection", "Add existing rooms": "Add existing rooms", "Filter your rooms and spaces": "Filter your rooms and spaces", @@ -2336,7 +2336,6 @@ "Share Community": "Share Community", "Share Room Message": "Share Room Message", "Link to selected message": "Link to selected message", - "Copy": "Copy", "Command Help": "Command Help", "Failed to save space settings.": "Failed to save space settings.", "Space settings": "Space settings", From 9a16fcb6fc27868fe5f452a6df70b3c7980dbdd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 28 Apr 2021 10:31:49 +0200 Subject: [PATCH 41/64] Emit in removeCallForRoom() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/CallHandler.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 8e8f120852..e9c85db84e 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -552,6 +552,7 @@ export default class CallHandler extends EventEmitter { private removeCallForRoom(roomId: string) { this.calls.delete(roomId); + this.emit(CallHandlerEvent.CallsChanged, this.calls); } private showICEFallbackPrompt() { From e5b61f063249c0f3f6d74bd1eae303b230c4e9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 28 Apr 2021 11:29:05 +0200 Subject: [PATCH 42/64] Keep rendering AudioFeeds on hold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- .../views/voip/AudioFeedArrayForCall.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/components/views/voip/AudioFeedArrayForCall.tsx b/src/components/views/voip/AudioFeedArrayForCall.tsx index fac2a22dcd..bfe232d799 100644 --- a/src/components/views/voip/AudioFeedArrayForCall.tsx +++ b/src/components/views/voip/AudioFeedArrayForCall.tsx @@ -25,7 +25,6 @@ interface IProps { interface IState { feeds: Array<CallFeed>; - onHold: boolean; } export default class AudioFeedArrayForCall extends React.Component<IProps, IState> { @@ -34,18 +33,15 @@ export default class AudioFeedArrayForCall extends React.Component<IProps, IStat this.state = { feeds: [], - onHold: false, }; } componentDidMount() { this.props.call.addListener(CallEvent.FeedsChanged, this.onFeedsChanged); - this.props.call.addListener(CallEvent.HoldUnhold, this.onHoldUnhold); } componentWillUnmount() { this.props.call.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged); - this.props.call.removeListener(CallEvent.HoldUnhold, this.onHoldUnhold); } onFeedsChanged = () => { @@ -54,20 +50,11 @@ export default class AudioFeedArrayForCall extends React.Component<IProps, IStat }); } - onHoldUnhold = (onHold: boolean) => { - this.setState({onHold: onHold}); - } - render() { - // If we are onHold don't render any audio elements - if (this.state.onHold) return null; - - const feeds = this.state.feeds.map((feed, i) => { + return this.state.feeds.map((feed, i) => { return ( <AudioFeed feed={feed} key={i} /> ); }); - - return feeds; } } From 40748d3c9471ebb0ff968485e7ca6661a2acb5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 28 Apr 2021 11:49:07 +0200 Subject: [PATCH 43/64] Make CallHandler emit CallChangeRoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let's hope I changed the tests correctly Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/CallHandler.tsx | 6 ++---- src/components/views/voip/CallPreview.tsx | 26 +++++++++++++---------- src/dispatcher/actions.ts | 3 --- test/CallHandler-test.ts | 11 +++++----- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 4f33714fef..0268ebfe46 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -141,6 +141,7 @@ export enum PlaceCallType { export enum CallHandlerEvent { CallsChanged = "calls_changed", + CallChangeRoom = "call_change_room", } export default class CallHandler extends EventEmitter { @@ -537,10 +538,7 @@ export default class CallHandler extends EventEmitter { this.removeCallForRoom(mappedRoomId); mappedRoomId = newMappedRoomId; this.calls.set(mappedRoomId, call); - dis.dispatch({ - action: Action.CallChangeRoom, - call, - }); + this.emit(CallHandlerEvent.CallChangeRoom, call); } } }); diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index d31afddec9..80fd64a820 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -19,7 +19,7 @@ import React from 'react'; import CallView from "./CallView"; import RoomViewStore from '../../../stores/RoomViewStore'; -import CallHandler from '../../../CallHandler'; +import CallHandler, { CallHandlerEvent } from '../../../CallHandler'; import dis from '../../../dispatcher/dispatcher'; import { ActionPayload } from '../../../dispatcher/payloads'; import PersistentApp from "../elements/PersistentApp"; @@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore"; import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -import { Action } from '../../../dispatcher/actions'; const SHOW_CALL_IN_STATES = [ CallState.Connected, @@ -110,12 +109,14 @@ export default class CallPreview extends React.Component<IProps, IState> { } public componentDidMount() { + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls); this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } public componentWillUnmount() { + CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls); MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); if (this.roomStoreToken) { this.roomStoreToken.remove(); @@ -143,21 +144,24 @@ export default class CallPreview extends React.Component<IProps, IState> { switch (payload.action) { // listen for call state changes to prod the render method, which // may hide the global CallView if the call it is tracking is dead - case Action.CallChangeRoom: case 'call_state': { - const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls( - CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId), - ); - - this.setState({ - primaryCall: primaryCall, - secondaryCall: secondaryCalls[0], - }); + this.updateCalls(); break; } } }; + private updateCalls = () => { + const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls( + CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId), + ); + + this.setState({ + primaryCall: primaryCall, + secondaryCall: secondaryCalls[0], + }); + }; + private onCallRemoteHold = () => { const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls( CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId), diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 46c962f160..cd32c3743f 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -114,9 +114,6 @@ export enum Action { */ VirtualRoomSupportUpdated = "virtual_room_support_updated", - // Probably would be better to have a VoIP states in a store and have the store emit changes - CallChangeRoom = "call_change_room", - /** * Fired when an upload has started. Should be used with UploadStartedPayload. */ diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 754610b223..f7e2c27993 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -16,7 +16,7 @@ limitations under the License. import './skinned-sdk'; -import CallHandler, { PlaceCallType } from '../src/CallHandler'; +import CallHandler, { PlaceCallType, CallHandlerEvent } from '../src/CallHandler'; import { stubClient, mkStubRoom } from './test-utils'; import { MatrixClientPeg } from '../src/MatrixClientPeg'; import dis from '../src/dispatcher/dispatcher'; @@ -172,11 +172,9 @@ describe('CallHandler', () => { let callRoomChangeEventCount = 0; const roomChangePromise = new Promise<void>(resolve => { - dispatchHandle = dis.register(payload => { - if (payload.action === Action.CallChangeRoom) { - ++callRoomChangeEventCount; - resolve(); - } + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, () => { + ++callRoomChangeEventCount; + resolve(); }); }); @@ -202,6 +200,7 @@ describe('CallHandler', () => { await roomChangePromise; dis.unregister(dispatchHandle); + CallHandler.sharedInstance().removeAllListeners(); // If everything's gone well, we should have seen only one room change // event and the call should now be in user 3's room. From 653591e8066fe44e9a7e31ef1acc1a93430d86b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 28 Apr 2021 11:56:26 +0200 Subject: [PATCH 44/64] Use CallChangeRoom in CallViewForRoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/components/views/voip/CallViewForRoom.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx index 7540dbc8d9..0c785f758d 100644 --- a/src/components/views/voip/CallViewForRoom.tsx +++ b/src/components/views/voip/CallViewForRoom.tsx @@ -16,13 +16,12 @@ limitations under the License. import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import React from 'react'; -import CallHandler from '../../../CallHandler'; +import CallHandler, { CallHandlerEvent } from '../../../CallHandler'; import CallView from './CallView'; import dis from '../../../dispatcher/dispatcher'; import {Resizable} from "re-resizable"; import ResizeNotifier from "../../../utils/ResizeNotifier"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -import { Action } from '../../../dispatcher/actions'; interface IProps { // What room we should display the call for @@ -55,25 +54,30 @@ export default class CallViewForRoom extends React.Component<IProps, IState> { public componentDidMount() { this.dispatcherRef = dis.register(this.onAction); + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCall); } public componentWillUnmount() { dis.unregister(this.dispatcherRef); + CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall); } private onAction = (payload) => { switch (payload.action) { - case Action.CallChangeRoom: case 'call_state': { - const newCall = this.getCall(); - if (newCall !== this.state.call) { - this.setState({call: newCall}); - } + this.updateCall(); break; } } }; + private updateCall = () => { + const newCall = this.getCall(); + if (newCall !== this.state.call) { + this.setState({call: newCall}); + } + }; + private getCall(): MatrixCall { const call = CallHandler.sharedInstance().getCallForRoom(this.props.roomId); From b6324a816f2b3d4c19a514c0eb51d9fcf52e70a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 28 Apr 2021 13:33:15 +0200 Subject: [PATCH 45/64] Use CallHandler correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- test/CallHandler-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index f7e2c27993..4b326cde25 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -172,7 +172,7 @@ describe('CallHandler', () => { let callRoomChangeEventCount = 0; const roomChangePromise = new Promise<void>(resolve => { - CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, () => { + callHandler.addListener(CallHandlerEvent.CallChangeRoom, () => { ++callRoomChangeEventCount; resolve(); }); @@ -200,7 +200,7 @@ describe('CallHandler', () => { await roomChangePromise; dis.unregister(dispatchHandle); - CallHandler.sharedInstance().removeAllListeners(); + callHandler.removeAllListeners(); // If everything's gone well, we should have seen only one room change // event and the call should now be in user 3's room. From 9aaf321e4eaf670c8d71f37a73fe965c31684a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 28 Apr 2021 13:39:09 +0200 Subject: [PATCH 46/64] Remove dis call which doesn't seem to be necessary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- test/CallHandler-test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts index 4b326cde25..1e3f92e788 100644 --- a/test/CallHandler-test.ts +++ b/test/CallHandler-test.ts @@ -199,7 +199,6 @@ describe('CallHandler', () => { fakeCall.emit(CallEvent.AssertedIdentityChanged); await roomChangePromise; - dis.unregister(dispatchHandle); callHandler.removeAllListeners(); // If everything's gone well, we should have seen only one room change From 626a4ccc34c902b1dc234d01ede39318f97d345b Mon Sep 17 00:00:00 2001 From: Aaron Raimist <aaron@raim.ist> Date: Fri, 30 Apr 2021 21:45:33 -0500 Subject: [PATCH 47/64] Make warning bold, close copied tooltip on escape Signed-off-by: Aaron Raimist <aaron@raim.ist> --- .../views/settings/tabs/user/HelpUserSettingsTab.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 9046c074e6..45395bd10c 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -60,6 +60,12 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState> }); } + componentWillUnmount() { + // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close + // the tooltip otherwise, such as pressing Escape + if (this.closeCopiedTooltip) this.closeCopiedTooltip(); + } + private onClearCacheAndReload = (e) => { if (!PlatformPeg.get()) return; @@ -168,7 +174,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState> ...toRightOf(buttonRect, 2), message: successful ? _t('Copied!') : _t('Failed to copy'), }); - target.onmouseleave = close; + this.closeCopiedTooltip = target.onmouseleave = close; } render() { @@ -290,8 +296,8 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState> <br /> <details> <summary>{_t("Access Token")}</summary><br /> - { _t("Your access token gives full access to your account." - + " Do not share it with anyone." ) } + <b>{_t("Your access token gives full access to your account." + + " Do not share it with anyone." )}</b> <div className="mx_HelpUserSettingsTab_accessToken"> <code>{MatrixClientPeg.get().getAccessToken()}</code> <AccessibleTooltipButton From 35c1e545218cfdc21933da75d60be93a9223c602 Mon Sep 17 00:00:00 2001 From: Aaron Raimist <aaron@raim.ist> Date: Fri, 30 Apr 2021 21:54:57 -0500 Subject: [PATCH 48/64] lint Signed-off-by: Aaron Raimist <aaron@raim.ist> --- src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index 45395bd10c..3fa0be478c 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -42,6 +42,8 @@ interface IState { @replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab") export default class HelpUserSettingsTab extends React.Component<IProps, IState> { + protected closeCopiedTooltip: () => void; + constructor(props) { super(props); From 0fe6a389d40c24deec0f9b427836df89011ae87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Tue, 4 May 2021 12:36:22 +0200 Subject: [PATCH 49/64] Add a note about sharing your IP with P2P calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 85e8e54258..43df51981a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -833,7 +833,7 @@ "Match system theme": "Match system theme", "Use a system font": "Use a system font", "System font name": "System font name", - "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)", "Send analytics data": "Send analytics data", "Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session", "Never send encrypted messages to unverified sessions in this room from this session": "Never send encrypted messages to unverified sessions in this room from this session", diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 2a26eeac13..1497a2208d 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -438,7 +438,10 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "webRtcAllowPeerToPeer": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, - displayName: _td('Allow Peer-to-Peer for 1:1 calls'), + displayName: _td( + "Allow Peer-to-Peer for 1:1 calls " + + "(if you enable this, the other party might be able to see your IP address)", + ), default: true, invertedSettingName: 'webRtcForceTURN', }, From 3eea1b836927a5bfc15313a80feb4ff5d35c1c30 Mon Sep 17 00:00:00 2001 From: Jaiwanth <jaiwanth2011@gmail.com> Date: Tue, 4 May 2021 16:42:22 +0530 Subject: [PATCH 50/64] Add cleanup functions for image view --- src/components/views/elements/ImageView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index fcacae2d39..05d487a9eb 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -114,6 +114,8 @@ export default class ImageView extends React.Component<IProps, IState> { componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); + window.removeEventListener("resize", this.calculateZoom); + this.image.current.removeEventListener("load", this.calculateZoom); } private calculateZoom = () => { From ac61c8eca8c7d22f16a2fae6e0b2fb75b11abcf5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 May 2021 12:27:27 +0100 Subject: [PATCH 51/64] Adhere to updated sort order for space children --- .../structures/SpaceRoomDirectory.tsx | 7 +++++- src/stores/SpaceStore.tsx | 22 +++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 8d6c9f0a70..db7fd13753 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -39,6 +39,7 @@ import {mediaFromMxc} from "../../customisations/Media"; import InfoTooltip from "../views/elements/InfoTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip"; import {useStateToggle} from "../../hooks/useStateToggle"; +import {getOrder} from "../../stores/SpaceStore"; interface IHierarchyProps { space: Room; @@ -254,7 +255,11 @@ export const HierarchyLevel = ({ const space = cli.getRoom(spaceId); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null); + const children = Array.from(relations.get(spaceId)?.values() || []); + const sortedChildren = sortBy(children, ev => { + // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting + return getOrder(ev.content.order, null, ev.state_key); + }); const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const roomId = ev.state_key; if (!rooms.has(roomId)) return result; diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index ec6227e45e..db93a23216 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {sortBy, throttle} from "lodash"; +import {ListIteratee, Many, sortBy, throttle} from "lodash"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixEvent} from "matrix-js-sdk/src/models/event"; @@ -61,15 +61,18 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, }, [[], []]); }; -const getOrder = (ev: MatrixEvent): string | null => { - const content = ev.getContent(); - if (typeof content.order === "string" && Array.from(content.order).every((c: string) => { +// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id` +export const getOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => { + let validatedOrder: string = null; + + if (typeof order === "string" && Array.from(order).every((c: string) => { const charCode = c.charCodeAt(0); return charCode >= 0x20 && charCode <= 0x7F; })) { - return content.order; + validatedOrder = order; } - return null; + + return [validatedOrder, creationTs, roomId]; } const getRoomFn: FetchRoomFn = (room: Room) => { @@ -193,7 +196,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { private getChildren(spaceId: string): Room[] { const room = this.matrixClient?.getRoom(spaceId); const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via); - return sortBy(childEvents, getOrder) + return sortBy(childEvents, ev => { + const roomId = ev.getStateKey(); + const childRoom = this.matrixClient?.getRoom(roomId); + const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs(); + return getOrder(ev.getContent().order, createTs, roomId); + }) .map(ev => this.matrixClient.getRoom(ev.getStateKey())) .filter(room => room?.getMyMembership() === "join" || room?.getMyMembership() === "invite") || []; } From 48237949ad6821e61c839fff14bb86738fab0575 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 May 2021 16:44:29 +0100 Subject: [PATCH 52/64] Only aggregate DM notifications on the Space Panel in the Home Space --- src/stores/SpaceStore.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index ec6227e45e..22307d9f2e 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -406,7 +406,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { this.spaceFilteredRooms.forEach((roomIds, s) => { // Update NotificationStates - this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => roomIds.has(room.roomId))); + this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { + if (roomIds.has(room.roomId)) { + // Don't aggregate notifications for DMs except in the Home Space + if (s !== HOME_SPACE) { + return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) + || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); + } + + return true; + } + + return false; + })); }); }, 100, {trailing: true, leading: true}); From a94c1a90c15c5fb29c31869b3a15ddde31ece576 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Tue, 4 May 2021 20:45:15 -0600 Subject: [PATCH 53/64] Update colours and sizing for voice messages Fixes https://github.com/vector-im/element-web/issues/17162 --- .../views/rooms/_VoiceRecordComposerTile.scss | 10 ++++++---- .../voice_messages/_PlaybackContainer.scss | 15 +++++++-------- res/themes/dark/css/_dark.scss | 10 +++++----- res/themes/legacy-dark/css/_legacy-dark.scss | 8 ++++---- .../legacy-light/css/_legacy-light.scss | 12 ++++++------ res/themes/light/css/_light.scss | 19 +++++++++++-------- .../voice_messages/LiveRecordingWaveform.tsx | 7 +++---- src/voice/Playback.ts | 2 +- src/voice/VoiceRecording.ts | 2 ++ 9 files changed, 45 insertions(+), 40 deletions(-) diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index 15daf81672..c0775b8371 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -39,7 +39,7 @@ limitations under the License. width: 14px; // w&h are size of icon height: 18px; vertical-align: middle; - margin-right: 7px; // distance from left edge of waveform container (container has some margin too) + margin-right: 11px; // distance from left edge of waveform container (container has some margin too) background-color: $voice-record-icon-color; mask-repeat: no-repeat; mask-size: contain; @@ -55,7 +55,9 @@ limitations under the License. position: relative; // important for the live circle &.mx_VoiceRecordComposerTile_recording { - padding-left: 16px; // +10px for the live circle, +6px for regular padding + // We are putting the circle in this padding, so we need +10px from the regular + // padding on the left side. + padding-left: 22px; &::before { animation: recording-pulse 2s infinite; @@ -65,8 +67,8 @@ limitations under the License. width: 10px; height: 10px; position: absolute; - left: 8px; - top: 16px; // vertically center + left: 12px; // 12px from the left edge for container padding + top: 18px; // vertically center (middle align with clock) border-radius: 10px; } } diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/voice_messages/_PlaybackContainer.scss index 49bd81ef81..64e8f445e1 100644 --- a/res/css/views/voice_messages/_PlaybackContainer.scss +++ b/res/css/views/voice_messages/_PlaybackContainer.scss @@ -19,8 +19,9 @@ limitations under the License. // Container for live recording and playback controls .mx_VoiceMessagePrimaryContainer { - padding: 6px; // makes us 4px taller than the send/stop button - padding-right: 5px; // there's 1px from the waveform itself, so account for that + // 7px top and bottom for visual design. 12px left & right, but the waveform (right) + // has a 1px padding on it that we want to account for. + padding: 7px 12px 7px 11px; background-color: $voice-record-waveform-bg-color; border-radius: 12px; @@ -30,11 +31,9 @@ limitations under the License. color: $voice-record-waveform-fg-color; font-size: $font-14px; + line-height: $font-24px; .mx_Waveform { - // We want the bars to be 2px shorter than the play/pause button in the waveform control - height: 28px; // default is 30px, so we're subtracting the 2px border off the bars - .mx_Waveform_bar { background-color: $voice-record-waveform-incomplete-fg-color; @@ -47,8 +46,8 @@ limitations under the License. } .mx_Clock { - padding-right: 4px; // isolate from waveform - padding-left: 8px; // isolate from live circle - width: 40px; // we're not using a monospace font, so fake it + width: 42px; // we're not using a monospace font, so fake it + padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended. + padding-left: 8px; // isolate from recording circle / play control } } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index cfdda41619..334bf581f3 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -42,13 +42,13 @@ $preview-bar-bg-color: $header-panel-bg-color; $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); $inverted-bg-color: $base-color; -$voice-record-stop-border-color: #6F7882; // "Quarterly" +$voice-record-stop-border-color: #6F7882; // "Quartary" $voice-record-waveform-bg-color: #394049; // "Dark Tile" -$voice-record-waveform-fg-color: $tertiary-fg-color; -$voice-record-waveform-incomplete-fg-color: #5b646d; -$voice-record-icon-color: $tertiary-fg-color; +$voice-record-waveform-fg-color: $secondary-fg-color; +$voice-record-waveform-incomplete-fg-color: #6F7882; // "Quartary" +$voice-record-icon-color: #6F7882; // "Quartary" $voice-playback-button-bg-color: $tertiary-fg-color; -$voice-playback-button-fg-color: $bg-color; +$voice-playback-button-fg-color: #21262C; // "Separator" // used by AddressSelector $selected-color: $room-highlight-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 6413a99ce0..6a162bbda7 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -127,11 +127,11 @@ $groupFilterPanel-divider-color: $roomlist-header-color; // See non-legacy dark for variable information $voice-record-stop-border-color: #6F7882; $voice-record-waveform-bg-color: #394049; -$voice-record-waveform-fg-color: $tertiary-fg-color; -$voice-record-waveform-incomplete-fg-color: #5b646d; -$voice-record-icon-color: $tertiary-fg-color; +$voice-record-waveform-fg-color: $secondary-fg-color; +$voice-record-waveform-incomplete-fg-color: #6F7882; +$voice-record-icon-color: #6F7882; $voice-playback-button-bg-color: $tertiary-fg-color; -$voice-playback-button-fg-color: $bg-color; +$voice-playback-button-fg-color: #21262C; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 2151724071..cc0f54853e 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -192,15 +192,15 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) $groupFilterPanel-divider-color: $roomlist-header-color; // See non-legacy _light for variable information -$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-symbol-color: #ff4b55; -$voice-record-waveform-bg-color: #E3E8F0; -$voice-record-waveform-fg-color: $muted-fg-color; -$voice-record-waveform-incomplete-fg-color: #C1C6CD; $voice-record-live-circle-color: #ff4b55; -$voice-record-icon-color: $muted-fg-color; +$voice-record-stop-border-color: #E3E8F0; +$voice-record-waveform-bg-color: #E3E8F0; +$voice-record-waveform-fg-color: $secondary-fg-color; +$voice-record-waveform-incomplete-fg-color: #C1C6CD; +$voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $primary-bg-color; -$voice-playback-button-fg-color: $muted-fg-color; +$voice-playback-button-fg-color: $secondary-fg-color; $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 1763fcdd48..193b7f816c 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -182,15 +182,18 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) $groupFilterPanel-divider-color: $roomlist-header-color; -$voice-record-stop-border-color: #E3E8F0; -$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes -$voice-record-waveform-bg-color: #E3E8F0; -$voice-record-waveform-fg-color: $muted-fg-color; -$voice-record-waveform-incomplete-fg-color: #C1C6CD; -$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes -$voice-record-icon-color: $muted-fg-color; +// These two don't change between themes. They are the $warning-color, but we don't +// want custom themes to affect them by accident. +$voice-record-stop-symbol-color: #ff4b55; +$voice-record-live-circle-color: #ff4b55; + +$voice-record-stop-border-color: #E3E8F0; // "Separator" +$voice-record-waveform-bg-color: #E3E8F0; // "Separator" +$voice-record-waveform-fg-color: $secondary-fg-color; +$voice-record-waveform-incomplete-fg-color: #C1C6CD; // "Quartary" +$voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $primary-bg-color; -$voice-playback-button-fg-color: $muted-fg-color; +$voice-playback-button-fg-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx index e7c34c9177..aab89f6ab1 100644 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/voice_messages/LiveRecordingWaveform.tsx @@ -15,12 +15,11 @@ limitations under the License. */ import React from "react"; -import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; +import {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {arrayFastResample, arraySeed} from "../../../utils/arrays"; import {percentageOf} from "../../../utils/numbers"; import Waveform from "./Waveform"; -import {PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback"; interface IProps { recorder: VoiceRecording; @@ -38,14 +37,14 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I public constructor(props) { super(props); - this.state = {heights: arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES)}; + this.state = {heights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES)}; this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); } private onRecordingUpdate = (update: IRecordingUpdate) => { // The waveform and the downsample target are pretty close, so we should be fine to // do this, despite the docs on arrayFastResample. - const bars = arrayFastResample(Array.from(update.waveform), PLAYBACK_WAVEFORM_SAMPLES); + const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES); this.setState({ // The incoming data is between zero and one, but typically even screaming into a // microphone won't send you over 0.6, so we artificially adjust the gain for the diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index ca48680ebd..b2525c2719 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -29,7 +29,7 @@ export enum PlaybackState { Playing = "playing", // active progress through timeline } -export const PLAYBACK_WAVEFORM_SAMPLES = 35; +export const PLAYBACK_WAVEFORM_SAMPLES = 38; const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); export class Playback extends EventEmitter implements IDestroyable { diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index eb705200ca..62496ce3d9 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -33,6 +33,8 @@ const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus const TARGET_MAX_LENGTH = 120; // 2 minutes in seconds. Somewhat arbitrary, though longer == larger files. const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary. +export const RECORDING_PLAYBACK_SAMPLES = 43; + export interface IRecordingUpdate { waveform: number[]; // floating points between 0 (low) and 1 (high). timeSeconds: number; // float From ed43d9257937b6e205dd939a60a453f1bd9fbefc Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Tue, 4 May 2021 20:55:35 -0600 Subject: [PATCH 54/64] Match designs more closely with +1 sample --- src/voice/Playback.ts | 2 +- src/voice/VoiceRecording.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/voice/Playback.ts b/src/voice/Playback.ts index b2525c2719..caa5241e1a 100644 --- a/src/voice/Playback.ts +++ b/src/voice/Playback.ts @@ -29,7 +29,7 @@ export enum PlaybackState { Playing = "playing", // active progress through timeline } -export const PLAYBACK_WAVEFORM_SAMPLES = 38; +export const PLAYBACK_WAVEFORM_SAMPLES = 39; const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); export class Playback extends EventEmitter implements IDestroyable { diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index 62496ce3d9..c4a0a78ce5 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -33,7 +33,7 @@ const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus const TARGET_MAX_LENGTH = 120; // 2 minutes in seconds. Somewhat arbitrary, though longer == larger files. const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary. -export const RECORDING_PLAYBACK_SAMPLES = 43; +export const RECORDING_PLAYBACK_SAMPLES = 44; export interface IRecordingUpdate { waveform: number[]; // floating points between 0 (low) and 1 (high). From ccdc9fbef6af3e3fd674517bde732168b3e7cfa8 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Tue, 4 May 2021 21:15:22 -0600 Subject: [PATCH 55/64] Fix issue where composer styles were being applied to the timeline --- res/css/views/rooms/_VoiceRecordComposerTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index c0775b8371..b87211a847 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -46,7 +46,7 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/trashcan.svg'); } -.mx_VoiceMessagePrimaryContainer { +.mx_VoiceRecordComposerTile_recording.mx_VoiceMessagePrimaryContainer { // Note: remaining class properties are in the PlayerContainer CSS. margin: 6px; // force the composer area to put a gutter around us From 374d8c1c4c44c57df0691db531b13efcea968260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 5 May 2021 08:43:24 +0200 Subject: [PATCH 56/64] Update link to Android SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73afe34df0..81a4a00515 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Platform Targets: * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. * Mobile Web is not currently a target platform - instead please use the native iOS (https://github.com/matrix-org/matrix-ios-kit) and Android - (https://github.com/matrix-org/matrix-android-sdk) SDKs. + (https://github.com/vector-im/element-android) SDKs. All code lands on the `develop` branch - `master` is only used for stable releases. **Please file PRs against `develop`!!** From 07f5b6e8c48bd11d593408b72b27e443e754288c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 11:45:12 +0100 Subject: [PATCH 57/64] Add retry mechanism and progress bar to add existing to space dialog --- .../dialogs/_AddExistingToSpaceDialog.scss | 77 ++++++++-- .../dialogs/AddExistingToSpaceDialog.tsx | 134 ++++++++++++------ src/i18n/strings/en_EN.json | 9 +- 3 files changed, 162 insertions(+), 58 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 84f965e6bd..c29c9791a6 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -101,7 +101,7 @@ limitations under the License. .mx_BaseAvatar { display: inline-flex; - margin: 5px 16px 5px 5px; + margin: auto 16px auto 5px; vertical-align: middle; } @@ -160,31 +160,32 @@ limitations under the License. } } - .mx_AddExistingToSpaceDialog_errorText { - font-weight: $font-semi-bold; - font-size: $font-12px; - line-height: $font-15px; - color: $notice-primary-color; - margin-bottom: 28px; - } - .mx_AddExistingToSpace { display: contents; } .mx_AddExistingToSpaceDialog_footer { display: flex; - margin-top: 32px; + margin-top: 20px; > span { flex-grow: 1; - font-size: $font-14px; + font-size: $font-12px; line-height: $font-15px; - font-weight: $font-semi-bold; + color: $secondary-fg-color; - .mx_AccessibleButton { - font-size: inherit; - display: inline-block; + .mx_ProgressBar { + height: 8px; + width: 100%; + + @mixin ProgressBarBorderRadius "8px"; + } + + .mx_AddExistingToSpaceDialog_progressText { + margin-top: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; } > * { @@ -192,8 +193,54 @@ limitations under the License. } } + .mx_AddExistingToSpaceDialog_error { + padding-left: 12px; + + > img { + align-self: center; + } + + .mx_AddExistingToSpaceDialog_errorHeading { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $notice-primary-color; + } + + .mx_AddExistingToSpaceDialog_errorCaption { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $primary-fg-color; + } + } + .mx_AccessibleButton { display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 36px; + } + + .mx_AddExistingToSpaceDialog_retryButton { + margin-left: 12px; + padding-left: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + background-color: $primary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + left: 0; + } } .mx_AccessibleButton_kind_link { diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 2253b525e0..a33248200c 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -29,12 +29,13 @@ import RoomAvatar from "../avatars/RoomAvatar"; import {getDisplayAliasForRoom} from "../../../Rooms"; import AccessibleButton from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {allSettled} from "../../../utils/promise"; +import {sleep} from "../../../utils/promise"; import DMRoomMap from "../../../utils/DMRoomMap"; import {calculateRoomVia} from "../../../utils/permalinks/Permalinks"; import StyledCheckbox from "../elements/StyledCheckbox"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; +import ProgressBar from "../elements/ProgressBar"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -46,7 +47,11 @@ const Entry = ({ room, checked, onChange }) => { return <label className="mx_AddExistingToSpace_entry"> <RoomAvatar room={room} height={32} width={32} /> <span className="mx_AddExistingToSpace_entry_name">{ room.name }</span> - <StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} /> + <StyledCheckbox + onChange={onChange ? (e) => onChange(e.target.checked) : null} + checked={checked} + disabled={!onChange} + /> </label>; }; @@ -104,9 +109,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, key={room.roomId} room={room} checked={selected.has(room)} - onChange={(checked) => { + onChange={onChange ? (checked) => { onChange(checked, room); - }} + } : null} />; }) } </div> @@ -120,9 +125,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, key={space.roomId} room={space} checked={selected.has(space)} - onChange={(checked) => { + onChange={onChange ? (checked) => { onChange(checked, space); - }} + } : null} />; }) } </div> @@ -136,9 +141,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, key={room.roomId} room={room} checked={selected.has(room)} - onChange={(checked) => { + onChange={onChange ? (checked) => { onChange(checked, room); - }} + } : null} />; }) } </div> @@ -156,8 +161,8 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>()); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); + const [progress, setProgress] = useState<number>(null); + const [error, setError] = useState<Error>(null); let spaceOptionSection; if (existingSubspaces.length > 0) { @@ -197,6 +202,82 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, </div> </React.Fragment>; + const addRooms = async () => { + setError(null); + setProgress(0); + + let error; + + for (const room of selectedToAdd) { + const via = calculateRoomVia(room); + try { + await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { + if (e.errcode === "M_LIMIT_EXCEEDED") { + await sleep(e.data.retry_after_ms); + return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry + } + + throw e; + }); + setProgress(i => i + 1); + } catch (e) { + console.error("Failed to add rooms to space", e); + setError(error = e); + break; + } + } + + if (!error) { + onFinished(true); + } + }; + + const busy = progress !== null; + + let footer; + if (error) { + footer = <> + <img + src={require("../../../../res/img/element-icons/warning-badge.svg")} + height="24" + width="24" + alt="" + /> + + <span className="mx_AddExistingToSpaceDialog_error"> + <div className="mx_AddExistingToSpaceDialog_errorHeading">{ _t("Not all selected were added") }</div> + <div className="mx_AddExistingToSpaceDialog_errorCaption">{ _t("Try again") }</div> + </span> + + <AccessibleButton className="mx_AddExistingToSpaceDialog_retryButton" onClick={addRooms}> + { _t("Retry") } + </AccessibleButton> + </>; + } else if (busy) { + footer = <span> + <ProgressBar value={progress} max={selectedToAdd.size} /> + <div className="mx_AddExistingToSpaceDialog_progressText"> + { _t("Adding rooms... (%(progress)s out of %(count)s)", { + count: selectedToAdd.size, + progress, + }) } + </div> + </span>; + } else { + footer = <> + <span> + <div>{ _t("Want to add a new room instead?") }</div> + <AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link"> + { _t("Create a new room") } + </AccessibleButton> + </span> + + <AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}> + { _t("Add") } + </AccessibleButton> + </>; + } + return <BaseDialog title={title} className="mx_AddExistingToSpaceDialog" @@ -204,50 +285,23 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFinished={onFinished} fixedWidth={false} > - { error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> } - <MatrixClientContext.Provider value={cli}> <AddExistingToSpace space={space} selected={selectedToAdd} - onChange={(checked, room) => { + onChange={!busy && !error ? (checked, room) => { if (checked) { selectedToAdd.add(room); } else { selectedToAdd.delete(room); } setSelectedToAdd(new Set(selectedToAdd)); - }} + } : null} /> </MatrixClientContext.Provider> <div className="mx_AddExistingToSpaceDialog_footer"> - <span> - <div>{ _t("Don't want to add an existing room?") }</div> - <AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link"> - { _t("Create a new room") } - </AccessibleButton> - </span> - - <AccessibleButton - kind="primary" - disabled={busy || selectedToAdd.size < 1} - onClick={async () => { - // TODO rate limiting - setBusy(true); - try { - await allSettled(Array.from(selectedToAdd).map((room) => - SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room)))); - onFinished(true); - } catch (e) { - console.error("Failed to add rooms to space", e); - setError(_t("Failed to add rooms to space")); - } - setBusy(false); - }} - > - { busy ? _t("Adding...") : _t("Add") } - </AccessibleButton> + { footer } </div> </BaseDialog>; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2e5431620d..6888148873 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2033,10 +2033,11 @@ "Direct Messages": "Direct Messages", "Space selection": "Space selection", "Add existing rooms": "Add existing rooms", - "Don't want to add an existing room?": "Don't want to add an existing room?", + "Not all selected were added": "Not all selected were added", + "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", + "Want to add a new room instead?": "Want to add a new room instead?", "Create a new room": "Create a new room", - "Failed to add rooms to space": "Failed to add rooms to space", - "Adding...": "Adding...", "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", @@ -2675,6 +2676,8 @@ "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", "Creating rooms...": "Creating rooms...", + "Failed to add rooms to space": "Failed to add rooms to space", + "Adding...": "Adding...", "What do you want to organise?": "What do you want to organise?", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", "Share %(name)s": "Share %(name)s", From acce9a4548c449f7347f0c077f333375b5ec253b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 11:50:55 +0100 Subject: [PATCH 58/64] Fix rounded progress bars --- res/css/views/dialogs/_AddExistingToSpaceDialog.scss | 2 +- res/css/views/elements/_ProgressBar.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index c29c9791a6..524f107165 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -178,7 +178,7 @@ limitations under the License. height: 8px; width: 100%; - @mixin ProgressBarBorderRadius "8px"; + @mixin ProgressBarBorderRadius 8px; } .mx_AddExistingToSpaceDialog_progressText { diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss index 770978e921..c075ac74ff 100644 --- a/res/css/views/elements/_ProgressBar.scss +++ b/res/css/views/elements/_ProgressBar.scss @@ -21,7 +21,7 @@ progress.mx_ProgressBar { appearance: none; border: none; - @mixin ProgressBarBorderRadius "6px"; + @mixin ProgressBarBorderRadius 6px; @mixin ProgressBarColour $progressbar-fg-color; @mixin ProgressBarBgColour $progressbar-bg-color; ::-webkit-progress-value { From 886959f32df0170ad65c1a95c3f8cdd9f56e325e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 11:54:14 +0100 Subject: [PATCH 59/64] port rate limiting code over to space creation wizard's add existing rooms --- src/components/structures/SpaceRoomView.tsx | 27 ++++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index cdf9dc02d3..a7a95d711c 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -52,7 +52,7 @@ import {useStateToggle} from "../../hooks/useStateToggle"; import SpaceStore from "../../stores/SpaceStore"; import FacePile from "../views/elements/FacePile"; import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; -import {allSettled} from "../../utils/promise"; +import {sleep} from "../../utils/promise"; import {calculateRoomVia} from "../../utils/permalinks/Permalinks"; interface IProps { @@ -389,15 +389,24 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => { let buttonLabel = _t("Skip for now"); if (selectedToAdd.size > 0) { onClick = async () => { - // TODO rate limiting setBusy(true); - try { - await allSettled(Array.from(selectedToAdd).map((room) => - SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room)))); - onFinished(true); - } catch (e) { - console.error("Failed to add rooms to space", e); - setError(_t("Failed to add rooms to space")); + + for (const room of selectedToAdd) { + const via = calculateRoomVia(room); + try { + await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { + if (e.errcode === "M_LIMIT_EXCEEDED") { + await sleep(e.data.retry_after_ms); + return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry + } + + throw e; + }); + } catch (e) { + console.error("Failed to add rooms to space", e); + setError(_t("Failed to add rooms to space")); + break; + } } setBusy(false); }; From 4279e99e4c2df546fea8536b8c6a46ea34d748a2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 15:27:31 +0100 Subject: [PATCH 60/64] Improve performance of search all spaces and space switching --- src/stores/room-list/RoomListStore.ts | 12 +++++++----- src/stores/room-list/algorithms/Algorithm.ts | 1 - 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 77beeb4ba1..58eb6ed317 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -668,7 +668,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> { * and thus might not cause an update to the store immediately. * @param {IFilterCondition} filter The filter condition to add. */ - public addFilter(filter: IFilterCondition): void { + public async addFilter(filter: IFilterCondition): Promise<void> { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log("Adding filter condition:", filter); @@ -680,12 +680,14 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> { promise = this.recalculatePrefiltering(); } else { this.filterConditions.push(filter); - if (this.algorithm) { - this.algorithm.addFilterCondition(filter); - } // Runtime filters with spaces disable prefiltering for the search all spaces effect if (SettingsStore.getValue("feature_spaces")) { - promise = this.recalculatePrefiltering(); + // this has to be awaited so that `setKnownRooms` is called in time for the `addFilterCondition` below + // this way the runtime filters are only evaluated on one dataset and not both. + await this.recalculatePrefiltering(); + } + if (this.algorithm) { + this.algorithm.addFilterCondition(filter); } } promise.then(() => this.updateFn.trigger()); diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 83ee803115..c0268d57a8 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -579,7 +579,6 @@ export class Algorithm extends EventEmitter { this.cachedRooms = newTags; this.updateTagsFromCache(); - this.recalculateFilteredRooms(); // Now that we've finished generation, we need to update the sticky room to what // it was. It's entirely possible that it changed lists though, so if it did then From 7f396bedd03d52036ba123a28672651ba02d7698 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 15:59:02 +0100 Subject: [PATCH 61/64] add comment --- src/stores/room-list/algorithms/Algorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index c0268d57a8..f3f0b178dd 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -577,7 +577,7 @@ export class Algorithm extends EventEmitter { await this.generateFreshTags(newTags); - this.cachedRooms = newTags; + this.cachedRooms = newTags; // this recalculates the filtered rooms for us this.updateTagsFromCache(); // Now that we've finished generation, we need to update the sticky room to what From 570c082573942a65fd1cf6052d498138c97f5f15 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Wed, 5 May 2021 09:58:45 -0600 Subject: [PATCH 62/64] Promote colour to a variable I refuse to try and type this variable name freehand. --- res/themes/dark/css/_dark.scss | 7 ++++--- res/themes/light/css/_light.scss | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 334bf581f3..d31b67d4ab 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6; $header-panel-text-secondary-color: #c8c8cd; $text-primary-color: #ffffff; $text-secondary-color: #B9BEC6; +$quaternary-fg-color: #6F7882; $search-bg-color: #181b21; $search-placeholder-color: #61708b; $room-highlight-color: #343a46; @@ -42,11 +43,11 @@ $preview-bar-bg-color: $header-panel-bg-color; $groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82); $inverted-bg-color: $base-color; -$voice-record-stop-border-color: #6F7882; // "Quartary" +$voice-record-stop-border-color: $quaternary-fg-color; $voice-record-waveform-bg-color: #394049; // "Dark Tile" $voice-record-waveform-fg-color: $secondary-fg-color; -$voice-record-waveform-incomplete-fg-color: #6F7882; // "Quartary" -$voice-record-icon-color: #6F7882; // "Quartary" +$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; +$voice-record-icon-color: $quaternary-fg-color; $voice-playback-button-bg-color: $tertiary-fg-color; $voice-playback-button-fg-color: #21262C; // "Separator" diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 193b7f816c..240e78b1e2 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16); $primary-fg-color: #2e2f32; $secondary-fg-color: #737D8C; $tertiary-fg-color: #8D99A5; +$quaternary-fg-color: #C1C6CD; $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) @@ -190,7 +191,7 @@ $voice-record-live-circle-color: #ff4b55; $voice-record-stop-border-color: #E3E8F0; // "Separator" $voice-record-waveform-bg-color: #E3E8F0; // "Separator" $voice-record-waveform-fg-color: $secondary-fg-color; -$voice-record-waveform-incomplete-fg-color: #C1C6CD; // "Quartary" +$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; $voice-record-icon-color: $tertiary-fg-color; $voice-playback-button-bg-color: $primary-bg-color; $voice-playback-button-fg-color: $secondary-fg-color; From 39ef376a793a26efcb34576a66f3aee21976826a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= <simon.bra.ag@gmail.com> Date: Wed, 5 May 2021 18:06:42 +0200 Subject: [PATCH 63/64] Use Android SDK instead Co-authored-by: J. Ryan Stinnett <jryans@gmail.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 81a4a00515..b3e96ef001 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Platform Targets: * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. * Mobile Web is not currently a target platform - instead please use the native iOS (https://github.com/matrix-org/matrix-ios-kit) and Android - (https://github.com/vector-im/element-android) SDKs. + (https://github.com/matrix-org/matrix-android-sdk2) SDKs. All code lands on the `develop` branch - `master` is only used for stable releases. **Please file PRs against `develop`!!** From 2b703e8574a9d46b75ce75706ff00dec33dd0801 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 5 May 2021 17:30:14 +0100 Subject: [PATCH 64/64] tweak code style --- src/stores/SpaceStore.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index db93a23216..5c5870a993 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -201,9 +201,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> { const childRoom = this.matrixClient?.getRoom(roomId); const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs(); return getOrder(ev.getContent().order, createTs, roomId); - }) - .map(ev => this.matrixClient.getRoom(ev.getStateKey())) - .filter(room => room?.getMyMembership() === "join" || room?.getMyMembership() === "invite") || []; + }).map(ev => { + return this.matrixClient.getRoom(ev.getStateKey()); + }).filter(room => { + return room?.getMyMembership() === "join" || room?.getMyMembership() === "invite"; + }) || []; } public getChildRooms(spaceId: string): Room[] {