;
};
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index 2ebc84ec7c..ec9c71ccbe 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -1312,7 +1312,7 @@ export default class InviteDialog extends React.PureComponent {
+const warnSelfDemote = async (isSpace: boolean) => {
const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
title: _t("Demote yourself?"),
description:
@@ -727,7 +727,7 @@ const MuteToggleButton: React.FC = ({member, room, powerLevels,
// if muting self, warn as it may be irreversible
if (target === cli.getUserId()) {
try {
- if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
+ if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
return;
@@ -816,7 +816,7 @@ const RoomAdminToolsContainer: React.FC = ({
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
kickButton = ;
}
- if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
+ if (me.powerLevel >= redactPowerLevel && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
redactButton = (
);
@@ -1095,7 +1095,7 @@ const PowerLevelEditor: React.FC<{
} else if (myUserId === target) {
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
try {
- if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
+ if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
}
@@ -1325,10 +1325,10 @@ const BasicUserInfo: React.FC<{
if (!isRoomEncrypted) {
if (!cryptoEnabled) {
text = _t("This client does not support end-to-end encryption.");
- } else if (room && !room.isSpaceRoom()) {
+ } else if (room && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
text = _t("Messages in this room are not end-to-end encrypted.");
}
- } else if (!room.isSpaceRoom()) {
+ } else if (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom()) {
text = _t("Messages in this room are end-to-end encrypted.");
}
@@ -1405,7 +1405,7 @@ const BasicUserInfo: React.FC<{
canInvite={roomPermissions.canInvite}
isIgnored={isIgnored}
member={member}
- isSpace={room?.isSpaceRoom()}
+ isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()}
/>
{ adminToolsContainer }
@@ -1567,7 +1567,7 @@ const UserInfo: React.FC = ({
previousPhase = RightPanelPhases.RoomMemberInfo;
refireParams = {member: member};
} else if (room) {
- previousPhase = previousPhase = room.isSpaceRoom()
+ previousPhase = previousPhase = SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()
? RightPanelPhases.SpaceMemberList
: RightPanelPhases.RoomMemberList;
}
@@ -1616,7 +1616,7 @@ const UserInfo: React.FC = ({
}
let scopeHeader;
- if (room?.isSpaceRoom()) {
+ if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
scopeHeader =
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js
index 593132a283..fbc0e477a1 100644
--- a/src/components/views/rooms/MemberList.js
+++ b/src/components/views/rooms/MemberList.js
@@ -30,6 +30,7 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import {replaceableComponent} from "../../../utils/replaceableComponent";
+import SettingsStore from "../../../settings/SettingsStore";
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
@@ -460,7 +461,7 @@ export default class MemberList extends React.Component {
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat && chat.roomId === this.props.roomId) {
inviteButtonText = _t("Invite to this community");
- } else if (room.isSpaceRoom()) {
+ } else if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
inviteButtonText = _t("Invite to this space");
}
@@ -492,7 +493,7 @@ export default class MemberList extends React.Component {
let previousPhase = RightPanelPhases.RoomSummary;
// We have no previousPhase for when viewing a MemberList from a Space
let scopeHeader;
- if (room?.isSpaceRoom()) {
+ if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
previousPhase = undefined;
scopeHeader =
diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.tsx b/src/components/views/rooms/ThirdPartyMemberInfo.tsx
index 78fdc8b404..792e6ac8f7 100644
--- a/src/components/views/rooms/ThirdPartyMemberInfo.tsx
+++ b/src/components/views/rooms/ThirdPartyMemberInfo.tsx
@@ -26,6 +26,7 @@ import {isValid3pidInvite} from "../../../RoomInvite";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import {replaceableComponent} from "../../../utils/replaceableComponent";
+import SettingsStore from "../../../settings/SettingsStore";
interface IProps {
event: MatrixEvent;
@@ -135,7 +136,7 @@ export default class ThirdPartyMemberInfo extends React.Component
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
+
+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 {
+ private element = createRef();
+
+ 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 (
+
+ );
+ }
+}
diff --git a/src/components/views/voip/AudioFeedArrayForCall.tsx b/src/components/views/voip/AudioFeedArrayForCall.tsx
new file mode 100644
index 0000000000..bfe232d799
--- /dev/null
+++ b/src/components/views/voip/AudioFeedArrayForCall.tsx
@@ -0,0 +1,60 @@
+/*
+Copyright 2021 Šimon Brandner
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import 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;
+}
+
+export default class AudioFeedArrayForCall extends React.Component {
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ feeds: [],
+ };
+ }
+
+ componentDidMount() {
+ this.props.call.addListener(CallEvent.FeedsChanged, this.onFeedsChanged);
+ }
+
+ componentWillUnmount() {
+ this.props.call.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged);
+ }
+
+ onFeedsChanged = () => {
+ this.setState({
+ feeds: this.props.call.getRemoteFeeds(),
+ });
+ }
+
+ render() {
+ return this.state.feeds.map((feed, i) => {
+ return (
+
+ );
+ });
+ }
+}
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 {
}
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 {
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/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx
index 6745713845..c084dacaa8 100644
--- a/src/components/views/voip/CallView.tsx
+++ b/src/components/views/voip/CallView.tsx
@@ -20,10 +20,9 @@ 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 } 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';
@@ -31,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';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
@@ -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.
@@ -60,6 +60,7 @@ interface IState {
controlsVisible: boolean,
showMoreMenu: boolean,
showDialpad: boolean,
+ feeds: CallFeed[],
}
function getFullScreenElement() {
@@ -115,6 +116,7 @@ export default class CallView extends React.Component {
controlsVisible: true,
showMoreMenu: false,
showDialpad: false,
+ feeds: this.props.call.getFeeds(),
}
this.updateCallListeners(null, this.props.call);
@@ -172,11 +174,13 @@ export default class CallView extends React.Component {
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);
}
}
@@ -186,6 +190,10 @@ export default class CallView extends React.Component {
});
};
+ private onFeedsChanged = (newFeeds: Array) => {
+ this.setState({feeds: newFeeds});
+ };
+
private onCallLocalHoldUnhold = () => {
this.setState({
isLocalOnHold: this.props.call.isLocalOnHold(),
@@ -304,7 +312,7 @@ export default class CallView extends React.Component {
}
// 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;
@@ -474,6 +482,8 @@ export default class CallView extends React.Component {
{contextMenuButton}
;
+ const avatarSize = this.props.pipMode ? 76 : 160;
+
// The 'content' for the call, ie. the videos for a video call and profile picture
// for voice calls (fills the bg)
let contentView: React.ReactNode;
@@ -524,41 +534,85 @@ export default class CallView extends React.Component {
;
}
- if (this.props.call.type === CallType.Video) {
- let localVideoFeed = null;
- let onHoldBackground = null;
- const backgroundStyle: CSSProperties = {};
- const containerClasses = classNames({
- mx_CallView_video: true,
- mx_CallView_video_hold: isOnHold,
- });
- if (isOnHold) {
+ // This is a bit messy. I can't see a reason to have two onHold/transfer screens
+ 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,
+ });
+ let onHoldBackground = null;
+ const backgroundStyle: CSSProperties = {};
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 = ;
- }
- if (!this.state.vidMuted) {
- localVideoFeed = ;
- }
- contentView =
+ );
+ }
+ } 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_content: true,
mx_CallView_voice: true,
- mx_CallView_voice_hold: isOnHold,
});
+ 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()) return;
+ return (
+
+ );
+ });
+
+ // 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 =
+ {feeds}
{
/>
- {holdTransferContent}
+
{_t("Connecting")}
+ {callControls}
+
;
+ } else {
+ const containerClasses = classNames({
+ mx_CallView_content: true,
+ 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. 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
+ if (feed.isVideoMuted() && feed.isLocal()) return;
+ return (
+
+ );
+ });
+
+ contentView =
+ {feeds}
{callControls}
;
}
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 {
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);
diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx
index 2981fb6c04..d22fa055ce 100644
--- a/src/components/views/voip/VideoFeed.tsx
+++ b/src/components/views/voip/VideoFeed.tsx
@@ -18,52 +18,102 @@ import classnames from 'classnames';
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 { logger } from 'matrix-js-sdk/src/logger';
+import MemberAvatar from "../avatars/MemberAvatar"
import {replaceableComponent} from "../../../utils/replaceableComponent";
-export enum VideoFeedType {
- Local,
- Remote,
-}
-
interface IProps {
call: MatrixCall,
- type: VideoFeedType,
+ feed: CallFeed,
+
+ // 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.
+ pipMode?: boolean;
// a callback which is called when the video element is resized
// due to a change in video metadata
onResize?: (e: Event) => void,
}
-@replaceableComponent("views.voip.VideoFeed")
-export default class VideoFeed extends React.Component {
- private vid = createRef();
+interface IState {
+ audioMuted: boolean;
+ videoMuted: boolean;
+}
- componentDidMount() {
- this.vid.current.addEventListener('resize', this.onResize);
- this.setVideoElement();
+@replaceableComponent("views.voip.VideoFeed")
+export default class VideoFeed extends React.Component {
+ private element = createRef();
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ audioMuted: this.props.feed.isAudioMuted(),
+ videoMuted: this.props.feed.isVideoMuted(),
+ };
}
- componentDidUpdate(prevProps) {
- if (this.props.call !== prevProps.call) {
- this.setVideoElement();
- }
+ componentDidMount() {
+ this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
+ this.playMedia();
}
componentWillUnmount() {
- this.vid.current.removeEventListener('resize', this.onResize);
+ this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
+ this.element.current?.removeEventListener('resize', this.onResize);
+ this.stopMedia();
}
- private setVideoElement() {
- if (this.props.type === VideoFeedType.Local) {
- this.props.call.setLocalVideoElement(this.vid.current);
- } else {
- this.props.call.setRemoteVideoElement(this.vid.current);
+ 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 {
+ // 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);
}
}
- onResize = (e) => {
- if (this.props.onResize) {
+ private stopMedia() {
+ const element = this.element.current;
+ if (!element) return;
+
+ 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.setState({
+ audioMuted: this.props.feed.isAudioMuted(),
+ videoMuted: this.props.feed.isVideoMuted(),
+ });
+ this.playMedia();
+ };
+
+ private onResize = (e) => {
+ if (this.props.onResize && !this.props.feed.isLocal()) {
this.props.onResize(e);
}
};
@@ -71,14 +121,33 @@ export default class VideoFeed extends React.Component {
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.videoMuted,
+ mx_VideoFeed_video: !this.state.videoMuted,
mx_VideoFeed_mirror: (
- this.props.type === VideoFeedType.Local &&
+ this.props.feed.isLocal() &&
SettingsStore.getValue('VideoView.flipVideoHorizontally')
),
};
- return ;
+ if (this.state.videoMuted) {
+ const member = this.props.feed.getMember();
+ const avatarSize = this.props.pipMode ? 76 : 160;
+
+ return (
+
+
+
+ );
+ } else {
+ return (
+
+ );
+ }
}
}
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/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index f665b4c460..dcad970300 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -785,13 +785,6 @@
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"Change notification settings": "Change notification settings",
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
- "Spaces": "Spaces",
- "Spaces are new ways to group rooms and people.": "Spaces are new ways to group rooms and people.",
- "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.",
- "Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta available for web, desktop and Android. Thank you for trying the beta.",
- "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.",
- "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
- "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Send and receive voice messages (in development)": "Send and receive voice messages (in development)",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
@@ -892,6 +885,7 @@
"You held the call Switch": "You held the call Switch",
"You held the call Resume": "You held the call Resume",
"%(peerName)s held the call": "%(peerName)s held the call",
+ "Connecting": "Connecting",
"Video Call": "Video Call",
"Voice Call": "Voice Call",
"Fill Screen": "Fill Screen",
@@ -1264,7 +1258,7 @@
"Copy": "Copy",
"Clear cache and reload": "Clear cache and reload",
"Labs": "Labs",
- "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.",
+ "Customise your experience with experimental labs features. Learn more.": "Customise your experience with experimental labs features. Learn more.",
"Ignored/Blocked": "Ignored/Blocked",
"Error adding ignored user/server": "Error adding ignored user/server",
"Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.",
@@ -2037,6 +2031,7 @@
"%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix rooms",
"Filter your rooms and spaces": "Filter your rooms and spaces",
+ "Spaces": "Spaces",
"Direct Messages": "Direct Messages",
"Space selection": "Space selection",
"Add existing rooms": "Add existing rooms",
@@ -2469,11 +2464,6 @@
"Revoke permissions": "Revoke permissions",
"Move left": "Move left",
"Move right": "Move right",
- "Spaces is a beta feature": "Spaces is a beta feature",
- "Tap for more info": "Tap for more info",
- "Beta": "Beta",
- "Leave the beta": "Leave the beta",
- "Join the beta": "Join the beta",
"Avatar": "Avatar",
"This room is public": "This room is public",
"Away": "Away",
@@ -2607,7 +2597,6 @@
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
"Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
- "Communities are changing to Spaces": "Communities are changing to Spaces",
"You’re all caught up": "You’re all caught up",
"You have no visible notifications.": "You have no visible notifications.",
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
@@ -2668,6 +2657,7 @@
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces",
"%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space",
"%(count)s rooms and 1 space|one": "%(count)s room and 1 space",
+ "Select a room below first": "Select a room below first",
"Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later",
"Removing...": "Removing...",
"Mark as not suggested": "Mark as not suggested",
@@ -2680,9 +2670,6 @@
"Public space": "Public space",
"Private space": "Private space",
" invites you": " invites you",
- "To view %(spaceName)s, turn on the Spaces beta": "To view %(spaceName)s, turn on the Spaces beta",
- "To join %(spaceName)s, turn on the Spaces beta": "To join %(spaceName)s, turn on the Spaces beta",
- "Add existing rooms & spaces": "Add existing rooms & spaces",
"Welcome to ": "Welcome to ",
"Random": "Random",
"Support": "Support",
@@ -2707,7 +2694,6 @@
"Inviting...": "Inviting...",
"Invite your teammates": "Invite your teammates",
"Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.",
- "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.",
"Invite by username": "Invite by username",
"What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?",
"Let's create a room for each of them.": "Let's create a room for each of them.",
diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts
index 393f4f27a1..1a78a1b485 100644
--- a/src/stores/BreadcrumbsStore.ts
+++ b/src/stores/BreadcrumbsStore.ts
@@ -122,7 +122,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient {
}
private async appendRoom(room: Room) {
- if (room.isSpaceRoom() && SettingsStore.getValue("feature_spaces")) return; // hide space rooms
+ if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return; // hide space rooms
let updated = false;
const rooms = (this.state.rooms || []).slice(); // cheap clone
diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts
index f3f0b178dd..024c484c41 100644
--- a/src/stores/room-list/algorithms/Algorithm.ts
+++ b/src/stores/room-list/algorithms/Algorithm.ts
@@ -199,8 +199,10 @@ export class Algorithm extends EventEmitter {
}
private async doUpdateStickyRoom(val: Room) {
- // no-op sticky rooms for spaces - they're effectively virtual rooms
- if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") val = null;
+ if (SettingsStore.getValue("feature_spaces") && val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
+ // no-op sticky rooms for spaces - they're effectively virtual rooms
+ val = null;
+ }
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
// otherwise we risk duplicating rooms.
diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts
index f212b1f9d9..c07c2b0b26 100644
--- a/src/stores/room-list/filters/VisibilityProvider.ts
+++ b/src/stores/room-list/filters/VisibilityProvider.ts
@@ -50,7 +50,7 @@ export class VisibilityProvider {
}
// hide space rooms as they'll be shown in the SpacePanel
- if (room.isSpaceRoom() && SettingsStore.getValue("feature_spaces")) {
+ if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
return false;
}
diff --git a/src/utils/space.tsx b/src/utils/space.tsx
index 3f2b6f9bb4..c14dc988d2 100644
--- a/src/utils/space.tsx
+++ b/src/utils/space.tsx
@@ -83,6 +83,7 @@ export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
if (shouldCreate) {
await createRoom(opts);
}
+ return shouldCreate;
};
export const showSpaceInvite = (space: Room, initialText = "") => {
diff --git a/test/CallHandler-test.ts b/test/CallHandler-test.ts
index 754610b223..1e3f92e788 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(resolve => {
- dispatchHandle = dis.register(payload => {
- if (payload.action === Action.CallChangeRoom) {
- ++callRoomChangeEventCount;
- resolve();
- }
+ callHandler.addListener(CallHandlerEvent.CallChangeRoom, () => {
+ ++callRoomChangeEventCount;
+ resolve();
});
});
@@ -201,7 +199,7 @@ 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
// event and the call should now be in user 3's room.
diff --git a/test/end-to-end-tests/yarn.lock b/test/end-to-end-tests/yarn.lock
index 7f2cefb92e..97b348fe50 100644
--- a/test/end-to-end-tests/yarn.lock
+++ b/test/end-to-end-tests/yarn.lock
@@ -435,9 +435,9 @@ jsprim@^1.2.2:
verror "1.10.0"
lodash@^4.15.0, lodash@^4.17.11:
- version "4.17.19"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
- integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
mime-db@~1.38.0:
version "1.38.0"
diff --git a/yarn.lock b/yarn.lock
index acdca26e55..9b33c4387b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5580,9 +5580,9 @@ lodash.sortby@^4.7.0:
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1:
- version "4.17.20"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
- integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@^4.0.0:
version "4.0.0"
@@ -8070,9 +8070,9 @@ typescript@^4.1.3:
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
ua-parser-js@^0.7.18:
- version "0.7.23"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.23.tgz#704d67f951e13195fbcd3d78818577f5bc1d547b"
- integrity sha512-m4hvMLxgGHXG3O3fQVAyyAQpZzDOvwnhOTjYz5Xmr7r/+LpkNy3vJXdVRWgd1TkAb7NGROZuSy96CrlNVjA7KA==
+ version "0.7.28"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
+ integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
unhomoglyph@^1.0.6:
version "1.0.6"