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/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx
index 36ab423885..411b0f9b5e 100644
--- a/src/components/views/spaces/SpacePanel.tsx
+++ b/src/components/views/spaces/SpacePanel.tsx
@@ -26,13 +26,11 @@ import {SpaceItem} from "./SpaceTreeLevel";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import SpaceStore, {
- HOME_SPACE,
UPDATE_INVITED_SPACES,
UPDATE_SELECTED_SPACE,
UPDATE_TOP_LEVEL_SPACES,
} from "../../../stores/SpaceStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
-import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
import NotificationBadge from "../rooms/NotificationBadge";
import {
RovingAccessibleButton,
@@ -40,13 +38,15 @@ import {
RovingTabIndexProvider,
} from "../../../accessibility/RovingTabIndex";
import {Key} from "../../../Keyboard";
+import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
+import {NotificationState} from "../../../stores/notifications/NotificationState";
interface IButtonProps {
space?: Room;
className?: string;
selected?: boolean;
tooltip?: string;
- notificationState?: SpaceNotificationState;
+ notificationState?: NotificationState;
isNarrow?: boolean;
onClick(): void;
}
@@ -212,8 +212,8 @@ const SpacePanel = () => {
className="mx_SpaceButton_home"
onClick={() => SpaceStore.instance.setActiveSpace(null)}
selected={!activeSpace}
- tooltip={_t("Home")}
- notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
+ tooltip={_t("All rooms")}
+ notificationState={RoomNotificationStateStore.instance.globalState}
isNarrow={isPanelCollapsed}
/>
{ invites.map(s =>
+
+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 =
+ {onHoldBackground}
+ {holdTransferContent}
+ {callControls}
+
+ );
+ } else {
+ const classes = classNames({
+ mx_CallView_content: true,
+ mx_CallView_voice: true,
+ mx_CallView_voice_hold: isOnHold,
+ });
+
+ contentView =(
+