Render Jitsi (and other sticky widgets) in PiP container, so it can be dragged and the "jump to room functionality" is provided (#7450)
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
This commit is contained in:
parent
8b01b68fa3
commit
ef95644e23
13 changed files with 396 additions and 342 deletions
|
@ -304,7 +304,6 @@
|
|||
@import "./views/typography/_Heading.scss";
|
||||
@import "./views/verification/_VerificationShowSas.scss";
|
||||
@import "./views/voip/CallView/_CallViewButtons.scss";
|
||||
@import "./views/voip/_CallContainer.scss";
|
||||
@import "./views/voip/_CallPreview.scss";
|
||||
@import "./views/voip/_CallView.scss";
|
||||
@import "./views/voip/_CallViewForRoom.scss";
|
||||
|
@ -313,4 +312,5 @@
|
|||
@import "./views/voip/_DialPad.scss";
|
||||
@import "./views/voip/_DialPadContextMenu.scss";
|
||||
@import "./views/voip/_DialPadModal.scss";
|
||||
@import "./views/voip/_PiPContainer.scss";
|
||||
@import "./views/voip/_VideoFeed.scss";
|
||||
|
|
|
@ -20,14 +20,15 @@ limitations under the License.
|
|||
background-color: $dark-panel-bg-color;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
// XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
|
||||
// XXX: PiPContainer sets pointer-events: none - should probably be set back in a better place
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.mx_CallView_large {
|
||||
padding-bottom: 10px;
|
||||
margin: $container-gap-width;
|
||||
margin-right: calc($container-gap-width / 2); // The left side gap is fully handled by this margin. To prohibit bleeding on webkit browser.
|
||||
// The left side gap is fully handled by this margin. To prohibit bleeding on webkit browser.
|
||||
margin-right: calc($container-gap-width / 2);
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -46,7 +47,7 @@ limitations under the License.
|
|||
width: 320px;
|
||||
padding-bottom: 8px;
|
||||
background-color: $system;
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
|
||||
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 8px;
|
||||
|
||||
.mx_CallView_video_hold,
|
||||
|
@ -170,7 +171,7 @@ limitations under the License.
|
|||
background-position: center;
|
||||
filter: blur(20px);
|
||||
&::after {
|
||||
content: '';
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
@ -194,10 +195,10 @@ limitations under the License.
|
|||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
content: '';
|
||||
content: "";
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-image: url('$(res)/img/voip/paused.svg');
|
||||
background-image: url("$(res)/img/voip/paused.svg");
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallContainer {
|
||||
.mx_PiPContainer {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 72px;
|
||||
|
@ -25,8 +25,4 @@ limitations under the License.
|
|||
// sure the cursor hits the iframe for Jitsi which will be at a
|
||||
// different level.
|
||||
pointer-events: none;
|
||||
|
||||
.mx_AppTile_persistedWrapper div {
|
||||
min-width: 350px;
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ import { DefaultTagID } from "../../stores/room-list/models";
|
|||
import { hideToast as hideServerLimitToast, showToast as showServerLimitToast } from "../../toasts/ServerLimitToast";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import LeftPanel from "./LeftPanel";
|
||||
import CallContainer from '../views/voip/CallContainer';
|
||||
import PipContainer from '../views/voip/PipContainer';
|
||||
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||
|
@ -674,7 +674,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CallContainer />
|
||||
<PipContainer />
|
||||
<NonUrgentToastContainer />
|
||||
<HostSignupContainer />
|
||||
{ audioFeedArraysForCalls }
|
||||
|
|
|
@ -508,8 +508,13 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
|
||||
// For persistent apps in PiP we want the zIndex to be higher then for other persistent apps (100)
|
||||
// otherwise there are issues that the PiP view is drawn UNDER another widget (Persistent app) when dragged around.
|
||||
const zIndexAboveOtherPersistentElements = 101;
|
||||
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement zIndex={this.props.miniMode ? 10 : 9}persistKey={this.persistKey}>
|
||||
<PersistedElement zIndex={this.props.miniMode ? zIndexAboveOtherPersistentElements : 9} persistKey={this.persistKey}>
|
||||
{ appTileBody }
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
|
@ -545,15 +550,15 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
if (!this.props.hideMaximiseButton) {
|
||||
const widgetIsMaximised = WidgetLayoutStore.instance.
|
||||
isInContainer(this.props.room, this.props.app, Container.Center);
|
||||
const className = classNames({
|
||||
"mx_AppTileMenuBar_iconButton": true,
|
||||
"mx_AppTileMenuBar_iconButton_minWidget": widgetIsMaximised,
|
||||
"mx_AppTileMenuBar_iconButton_maxWidget": !widgetIsMaximised,
|
||||
});
|
||||
maxMinButton = <AccessibleButton
|
||||
className={
|
||||
"mx_AppTileMenuBar_iconButton"
|
||||
+ (widgetIsMaximised
|
||||
? " mx_AppTileMenuBar_iconButton_minWidget"
|
||||
: " mx_AppTileMenuBar_iconButton_maxWidget")
|
||||
}
|
||||
className={className}
|
||||
title={
|
||||
widgetIsMaximised ? _t('Close'): _t('Maximise widget')
|
||||
widgetIsMaximised ? _t('Close') : _t('Maximise widget')
|
||||
}
|
||||
onClick={this.onMaxMinWidgetClick}
|
||||
/>;
|
||||
|
|
|
@ -184,7 +184,7 @@ export default class PersistedElement extends React.Component<IProps> {
|
|||
width: parentRect.width + 'px',
|
||||
height: parentRect.height + 'px',
|
||||
});
|
||||
}, 100, { trailing: true, leading: true });
|
||||
}, 16, { trailing: true, leading: true });
|
||||
|
||||
public render(): JSX.Element {
|
||||
return <div ref={this.collectChildContainer} />;
|
||||
|
|
|
@ -16,141 +16,79 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EventSubscription } from 'fbemitter';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AppTile from "./AppTile";
|
||||
import { Container, WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
|
||||
interface IProps {
|
||||
// none
|
||||
persistentWidgetId: string;
|
||||
pointerEvents?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
roomId: string;
|
||||
persistentWidgetId: string;
|
||||
rightPanelPhase?: RightPanelPhases;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.PersistentApp")
|
||||
export default class PersistentApp extends React.Component<IProps, IState> {
|
||||
private roomStoreToken: EventSubscription;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
|
||||
rightPanelPhase: RightPanelStore.instance.currentCard.phase,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
|
||||
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
|
||||
}
|
||||
MatrixClientPeg.get().off("Room.myMembership", this.onMyMembership);
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = (): void => {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
});
|
||||
};
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({
|
||||
rightPanelPhase: RightPanelStore.instance.currentCard.phase,
|
||||
});
|
||||
};
|
||||
|
||||
private onActiveWidgetStoreUpdate = (): void => {
|
||||
this.setState({
|
||||
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
|
||||
});
|
||||
};
|
||||
|
||||
private onMyMembership = async (room: Room, membership: string): Promise<void> => {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.props.persistentWidgetId);
|
||||
if (membership !== "join") {
|
||||
// we're not in the room anymore - delete
|
||||
if (room .roomId === persistentWidgetInRoomId) {
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId);
|
||||
if (room.roomId === persistentWidgetInRoomId) {
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.persistentWidgetId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const wId = this.state.persistentWidgetId;
|
||||
const wId = this.props.persistentWidgetId;
|
||||
if (wId) {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(wId);
|
||||
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
|
||||
// Sanity check the room - the widget may have been destroyed between render cycles, and
|
||||
// thus no room is associated anymore.
|
||||
if (!persistentWidgetInRoom) return null;
|
||||
|
||||
const wls = WidgetLayoutStore.instance;
|
||||
|
||||
const userIsPartOfTheRoom = persistentWidgetInRoom.getMyMembership() == "join";
|
||||
const fromAnotherRoom = this.state.roomId !== persistentWidgetInRoomId;
|
||||
|
||||
const notInRightPanel =
|
||||
!(this.state.rightPanelPhase == RightPanelPhases.Widget &&
|
||||
wId == RightPanelStore.instance.currentCard.state?.widgetId);
|
||||
const notInCenterContainer =
|
||||
!wls.getContainerWidgets(persistentWidgetInRoom, Container.Center).some((app) => app.id == wId);
|
||||
const notInTopContainer =
|
||||
!wls.getContainerWidgets(persistentWidgetInRoom, Container.Top).some(app => app.id == wId);
|
||||
if (
|
||||
// the widget should only be shown as a persistent app (in a floating pip container) if it is not visible on screen
|
||||
// either, because we are viewing a different room OR because it is in none of the possible containers of the room view.
|
||||
(fromAnotherRoom && userIsPartOfTheRoom) ||
|
||||
(notInRightPanel && notInCenterContainer && notInTopContainer && userIsPartOfTheRoom)
|
||||
) {
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
|
||||
});
|
||||
const app = WidgetUtils.makeAppConfig(
|
||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
||||
persistentWidgetInRoomId, appEvent.getId(),
|
||||
);
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
app={app}
|
||||
fullWidth={true}
|
||||
room={persistentWidgetInRoom}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
miniMode={true}
|
||||
showMenubar={false}
|
||||
/>;
|
||||
}
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
|
||||
});
|
||||
const app = WidgetUtils.makeAppConfig(
|
||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
||||
persistentWidgetInRoomId, appEvent.getId(),
|
||||
);
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
app={app}
|
||||
fullWidth={true}
|
||||
room={persistentWidgetInRoom}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
miniMode={true}
|
||||
showMenubar={false}
|
||||
pointerEvents={this.props.pointerEvents}
|
||||
/>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
// Close the sticker picker when the window resizes
|
||||
window.addEventListener('resize', this.onResize);
|
||||
|
||||
this.dispatcherRef = dis.register(this.onWidgetAction);
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
// Track updates to widget state in account data
|
||||
MatrixClientPeg.get().on('accountData', this.updateWidget);
|
||||
|
@ -198,7 +198,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onWidgetAction = (payload: ActionPayload): void => {
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
switch (payload.action) {
|
||||
case "user_widget_updated":
|
||||
this.forceUpdate();
|
||||
|
|
|
@ -1,217 +0,0 @@
|
|||
/*
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { EventSubscription } from 'fbemitter';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import CallView from "./CallView";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
CallState.InviteSent,
|
||||
CallState.Connecting,
|
||||
CallState.CreateAnswer,
|
||||
CallState.CreateOffer,
|
||||
CallState.WaitLocalMedia,
|
||||
];
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
roomId: string;
|
||||
|
||||
// The main call that we are displaying (ie. not including the call in the room being viewed, if any)
|
||||
primaryCall: MatrixCall;
|
||||
|
||||
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||
// they belong to
|
||||
secondaryCall: MatrixCall;
|
||||
}
|
||||
|
||||
// Splits a list of calls into one 'primary' one and a list
|
||||
// (which should be a single element) of other calls.
|
||||
// The primary will be the one not on hold, or an arbitrary one
|
||||
// if they're all on hold)
|
||||
function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall[]] {
|
||||
const calls = CallHandler.instance.getAllActiveCallsForPip(roomId);
|
||||
|
||||
let primary: MatrixCall = null;
|
||||
let secondaries: MatrixCall[] = [];
|
||||
|
||||
for (const call of calls) {
|
||||
if (!SHOW_CALL_IN_STATES.includes(call.state)) continue;
|
||||
|
||||
if (!call.isRemoteOnHold() && primary === null) {
|
||||
primary = call;
|
||||
} else {
|
||||
secondaries.push(call);
|
||||
}
|
||||
}
|
||||
|
||||
if (primary === null && secondaries.length > 0) {
|
||||
primary = secondaries[0];
|
||||
secondaries = secondaries.slice(1);
|
||||
}
|
||||
|
||||
if (secondaries.length > 1) {
|
||||
// We should never be in more than two calls so this shouldn't happen
|
||||
logger.log("Found more than 1 secondary call! Other calls will not be shown.");
|
||||
}
|
||||
|
||||
return [primary, secondaries];
|
||||
}
|
||||
|
||||
/**
|
||||
* CallPreview shows a small version of CallView hovering over the UI in 'picture-in-picture'
|
||||
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing.
|
||||
*/
|
||||
@replaceableComponent("views.voip.CallPreview")
|
||||
export default class CallPreview extends React.Component<IProps, IState> {
|
||||
private roomStoreToken: EventSubscription;
|
||||
private dispatcherRef: string;
|
||||
private settingsWatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
|
||||
|
||||
this.state = {
|
||||
roomId,
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.updateCalls);
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.state.roomId);
|
||||
if (room) {
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
if (this.roomStoreToken) {
|
||||
this.roomStoreToken.remove();
|
||||
}
|
||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||
const room = MatrixClientPeg.get().getRoom(this.state.roomId);
|
||||
if (room) {
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
|
||||
}
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = () => {
|
||||
const newRoomId = RoomViewStore.getRoomId();
|
||||
const oldRoomId = this.state.roomId;
|
||||
if (newRoomId === oldRoomId) return;
|
||||
// The WidgetLayoutStore observer always tracks the currently viewed Room,
|
||||
// so we don't end up with multiple observers and know what observer to remove on unmount
|
||||
const oldRoom = MatrixClientPeg.get()?.getRoom(oldRoomId);
|
||||
if (oldRoom) {
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(oldRoom), this.updateCalls);
|
||||
}
|
||||
const newRoom = MatrixClientPeg.get()?.getRoom(newRoomId);
|
||||
if (newRoom) {
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(newRoom), this.updateCalls);
|
||||
}
|
||||
if (!newRoomId) return;
|
||||
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(newRoomId);
|
||||
this.setState({
|
||||
roomId: newRoomId,
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
};
|
||||
|
||||
private updateCalls = (): void => {
|
||||
if (!this.state.roomId) return;
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.state.roomId);
|
||||
|
||||
this.setState({
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
};
|
||||
|
||||
private onCallRemoteHold = () => {
|
||||
if (!this.state.roomId) return;
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.state.roomId);
|
||||
|
||||
this.setState({
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
};
|
||||
|
||||
private onDoubleClick = (): void => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.primaryCall.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
const pipMode = true;
|
||||
if (this.state.primaryCall) {
|
||||
return (
|
||||
<PictureInPictureDragger
|
||||
className="mx_CallPreview"
|
||||
draggable={pipMode}
|
||||
onDoubleClick={this.onDoubleClick}
|
||||
>
|
||||
{
|
||||
({ onStartMoving, onResize }) =>
|
||||
<CallView
|
||||
onMouseDownOnHeader={onStartMoving}
|
||||
call={this.state.primaryCall}
|
||||
secondaryCall={this.state.secondaryCall}
|
||||
pipMode={pipMode}
|
||||
onResize={onResize}
|
||||
/>
|
||||
}
|
||||
</PictureInPictureDragger>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
return <PersistentApp />;
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ const callTypeTranslationByType: Record<CallType, string> = {
|
|||
|
||||
interface CallViewHeaderProps {
|
||||
pipMode: boolean;
|
||||
type: CallType;
|
||||
type?: CallType;
|
||||
callRooms?: Room[];
|
||||
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
}
|
||||
|
@ -93,9 +93,9 @@ const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
|||
onPipMouseDown,
|
||||
}) => {
|
||||
const [callRoom, onHoldCallRoom] = callRooms;
|
||||
const callTypeText = _t(callTypeTranslationByType[type]);
|
||||
const callRoomName = callRoom.name;
|
||||
const { roomId } = callRoom;
|
||||
const callTypeText = type ? _t(callTypeTranslationByType[type]) : _t("Widget");
|
||||
const callRoomName = callRoom?.name;
|
||||
const roomId = callRoom?.roomId;
|
||||
|
||||
if (!pipMode) {
|
||||
return <div className="mx_CallViewHeader">
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import CallPreview from './CallPreview';
|
||||
import PipView from './PipView';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
|
@ -28,11 +28,11 @@ interface IState {
|
|||
|
||||
}
|
||||
|
||||
@replaceableComponent("views.voip.CallContainer")
|
||||
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
||||
@replaceableComponent("views.voip.PiPContainer")
|
||||
export default class PiPContainer extends React.PureComponent<IProps, IState> {
|
||||
public render() {
|
||||
return <div className="mx_CallContainer">
|
||||
<CallPreview />
|
||||
return <div className="mx_PiPContainer">
|
||||
<PipView />
|
||||
</div>;
|
||||
}
|
||||
}
|
330
src/components/views/voip/PipView.tsx
Normal file
330
src/components/views/voip/PipView.tsx
Normal file
|
@ -0,0 +1,330 @@
|
|||
/*
|
||||
Copyright 2017 - 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import { EventSubscription } from 'fbemitter';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import classNames from 'classnames';
|
||||
|
||||
import CallView from "./CallView";
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { Container, WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
|
||||
import CallViewHeader from './CallView/CallViewHeader';
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
CallState.InviteSent,
|
||||
CallState.Connecting,
|
||||
CallState.CreateAnswer,
|
||||
CallState.CreateOffer,
|
||||
CallState.WaitLocalMedia,
|
||||
];
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
viewedRoomId: string;
|
||||
|
||||
// The main call that we are displaying (ie. not including the call in the room being viewed, if any)
|
||||
primaryCall: MatrixCall;
|
||||
|
||||
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
|
||||
// they belong to
|
||||
secondaryCall: MatrixCall;
|
||||
|
||||
// widget candidate to be displayed in the pip view.
|
||||
persistentWidgetId: string;
|
||||
showWidgetInPip: boolean;
|
||||
rightPanelPhase: RightPanelPhases;
|
||||
|
||||
moving: boolean;
|
||||
}
|
||||
|
||||
// Splits a list of calls into one 'primary' one and a list
|
||||
// (which should be a single element) of other calls.
|
||||
// The primary will be the one not on hold, or an arbitrary one
|
||||
// if they're all on hold)
|
||||
function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall[]] {
|
||||
const calls = CallHandler.instance.getAllActiveCallsForPip(roomId);
|
||||
|
||||
let primary: MatrixCall = null;
|
||||
let secondaries: MatrixCall[] = [];
|
||||
|
||||
for (const call of calls) {
|
||||
if (!SHOW_CALL_IN_STATES.includes(call.state)) continue;
|
||||
|
||||
if (!call.isRemoteOnHold() && primary === null) {
|
||||
primary = call;
|
||||
} else {
|
||||
secondaries.push(call);
|
||||
}
|
||||
}
|
||||
|
||||
if (primary === null && secondaries.length > 0) {
|
||||
primary = secondaries[0];
|
||||
secondaries = secondaries.slice(1);
|
||||
}
|
||||
|
||||
if (secondaries.length > 1) {
|
||||
// We should never be in more than two calls so this shouldn't happen
|
||||
logger.log("Found more than 1 secondary call! Other calls will not be shown.");
|
||||
}
|
||||
|
||||
return [primary, secondaries];
|
||||
}
|
||||
|
||||
/**
|
||||
* PipView shows a small version of the CallView or a sticky widget hovering over the UI in 'picture-in-picture'
|
||||
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing
|
||||
* and all widgets that are active but not shown in any other possible container.
|
||||
*/
|
||||
|
||||
@replaceableComponent("views.voip.PipView")
|
||||
export default class PipView extends React.Component<IProps, IState> {
|
||||
private roomStoreToken: EventSubscription;
|
||||
private settingsWatcherRef: string;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const roomId = RoomViewStore.getRoomId();
|
||||
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
|
||||
|
||||
this.state = {
|
||||
moving: false,
|
||||
viewedRoomId: roomId,
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
|
||||
rightPanelPhase: RightPanelStore.instance.currentCard.phase,
|
||||
showWidgetInPip: false,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
CallHandler.instance.addListener(CallHandlerEvent.CallState, this.updateCalls);
|
||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.state.viewedRoomId);
|
||||
if (room) {
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
|
||||
}
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
|
||||
document.addEventListener("mouseup", this.onEndMoving.bind(this));
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||
CallHandler.instance.removeListener(CallHandlerEvent.CallState, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
this.roomStoreToken?.remove();
|
||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||
const room = MatrixClientPeg.get().getRoom(this.state.viewedRoomId);
|
||||
if (room) {
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
|
||||
}
|
||||
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
|
||||
document.removeEventListener("mouseup", this.onEndMoving.bind(this));
|
||||
}
|
||||
|
||||
private onStartMoving() {
|
||||
this.setState({ moving: true });
|
||||
}
|
||||
|
||||
private onEndMoving() {
|
||||
this.setState({ moving: false });
|
||||
}
|
||||
|
||||
private onRoomViewStoreUpdate = () => {
|
||||
const newRoomId = RoomViewStore.getRoomId();
|
||||
const oldRoomId = this.state.viewedRoomId;
|
||||
if (newRoomId === oldRoomId) return;
|
||||
// The WidgetLayoutStore observer always tracks the currently viewed Room,
|
||||
// so we don't end up with multiple observers and know what observer to remove on unmount
|
||||
const oldRoom = MatrixClientPeg.get()?.getRoom(oldRoomId);
|
||||
if (oldRoom) {
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(oldRoom), this.updateCalls);
|
||||
}
|
||||
const newRoom = MatrixClientPeg.get()?.getRoom(newRoomId);
|
||||
if (newRoom) {
|
||||
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(newRoom), this.updateCalls);
|
||||
}
|
||||
if (!newRoomId) return;
|
||||
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(newRoomId);
|
||||
this.setState({
|
||||
viewedRoomId: newRoomId,
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
this.updateShowWidgetInPip();
|
||||
};
|
||||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
this.setState({
|
||||
rightPanelPhase: RightPanelStore.instance.currentCard.phase,
|
||||
});
|
||||
this.updateShowWidgetInPip();
|
||||
};
|
||||
|
||||
private onActiveWidgetStoreUpdate = (): void => {
|
||||
this.setState({
|
||||
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
|
||||
});
|
||||
this.updateShowWidgetInPip();
|
||||
};
|
||||
|
||||
private updateCalls = (): void => {
|
||||
if (!this.state.viewedRoomId) return;
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.state.viewedRoomId);
|
||||
|
||||
this.setState({
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
this.updateShowWidgetInPip();
|
||||
};
|
||||
|
||||
private onCallRemoteHold = () => {
|
||||
if (!this.state.viewedRoomId) return;
|
||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.state.viewedRoomId);
|
||||
|
||||
this.setState({
|
||||
primaryCall: primaryCall,
|
||||
secondaryCall: secondaryCalls[0],
|
||||
});
|
||||
};
|
||||
|
||||
private onDoubleClick = (): void => {
|
||||
const callRoomId = this.state.primaryCall?.roomId;
|
||||
const widgetRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
|
||||
if (!!(callRoomId ?? widgetRoomId)) {
|
||||
dis.dispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: callRoomId ?? widgetRoomId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public updateShowWidgetInPip() {
|
||||
const wId = this.state.persistentWidgetId;
|
||||
|
||||
let userIsPartOfTheRoom = false;
|
||||
let fromAnotherRoom = false;
|
||||
let notInRightPanel = false;
|
||||
let notInCenterContainer = false;
|
||||
let notInTopContainer = false;
|
||||
if (wId) {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(wId);
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
|
||||
// Sanity check the room - the widget may have been destroyed between render cycles, and
|
||||
// thus no room is associated anymore.
|
||||
if (!persistentWidgetInRoom) return null;
|
||||
|
||||
const wls = WidgetLayoutStore.instance;
|
||||
|
||||
userIsPartOfTheRoom = persistentWidgetInRoom.getMyMembership() == "join";
|
||||
fromAnotherRoom = this.state.viewedRoomId !== persistentWidgetInRoomId;
|
||||
|
||||
notInRightPanel =
|
||||
!(RightPanelStore.instance.currentCard.phase == RightPanelPhases.Widget &&
|
||||
wId == RightPanelStore.instance.currentCard.state?.widgetId);
|
||||
notInCenterContainer =
|
||||
!wls.getContainerWidgets(persistentWidgetInRoom, Container.Center).some((app) => app.id == wId);
|
||||
notInTopContainer =
|
||||
!wls.getContainerWidgets(persistentWidgetInRoom, Container.Top).some(app => app.id == wId);
|
||||
}
|
||||
|
||||
// The widget should only be shown as a persistent app (in a floating pip container) if it is not visible on screen
|
||||
// either, because we are viewing a different room OR because it is in none of the possible containers of the room view.
|
||||
const showWidgetInPip =
|
||||
(fromAnotherRoom && userIsPartOfTheRoom) ||
|
||||
(notInRightPanel && notInCenterContainer && notInTopContainer && userIsPartOfTheRoom);
|
||||
|
||||
this.setState({ showWidgetInPip });
|
||||
}
|
||||
|
||||
public render() {
|
||||
const pipMode = true;
|
||||
let pipContent;
|
||||
|
||||
if (this.state.primaryCall) {
|
||||
pipContent = ({ onStartMoving, onResize }) =>
|
||||
<CallView
|
||||
onMouseDownOnHeader={onStartMoving}
|
||||
call={this.state.primaryCall}
|
||||
secondaryCall={this.state.secondaryCall}
|
||||
pipMode={pipMode}
|
||||
onResize={onResize}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (this.state.showWidgetInPip) {
|
||||
const pipViewClasses = classNames({
|
||||
mx_CallView: true,
|
||||
mx_CallView_pip: pipMode,
|
||||
mx_CallView_large: !pipMode,
|
||||
});
|
||||
const roomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
|
||||
const roomForWidget = MatrixClientPeg.get().getRoom(roomId);
|
||||
|
||||
pipContent = ({ onStartMoving, _onResize }) =>
|
||||
<div className={pipViewClasses}>
|
||||
<CallViewHeader
|
||||
type={undefined}
|
||||
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
||||
pipMode={pipMode}
|
||||
callRooms={[roomForWidget]}
|
||||
/>
|
||||
<PersistentApp
|
||||
persistentWidgetId={this.state.persistentWidgetId}
|
||||
pointerEvents={this.state.moving ? 'none' : undefined}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (!!pipContent) {
|
||||
return <PictureInPictureDragger
|
||||
className="mx_CallPreview"
|
||||
draggable={pipMode}
|
||||
onDoubleClick={this.onDoubleClick}
|
||||
>
|
||||
{ pipContent }
|
||||
</PictureInPictureDragger>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1011,6 +1011,7 @@
|
|||
"Fill Screen": "Fill Screen",
|
||||
"Return to call": "Return to call",
|
||||
"%(name)s on hold": "%(name)s on hold",
|
||||
"Widget": "Widget",
|
||||
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
||||
"Verified!": "Verified!",
|
||||
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||
|
|
Loading…
Reference in a new issue