mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 05:04:57 +08:00
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
@ -304,7 +304,6 @@
|
|||||||
@import "./views/typography/_Heading.scss";
|
@import "./views/typography/_Heading.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voip/CallView/_CallViewButtons.scss";
|
@import "./views/voip/CallView/_CallViewButtons.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
|
||||||
@import "./views/voip/_CallPreview.scss";
|
@import "./views/voip/_CallPreview.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_CallViewForRoom.scss";
|
@import "./views/voip/_CallViewForRoom.scss";
|
||||||
@ -313,4 +312,5 @@
|
|||||||
@import "./views/voip/_DialPad.scss";
|
@import "./views/voip/_DialPad.scss";
|
||||||
@import "./views/voip/_DialPadContextMenu.scss";
|
@import "./views/voip/_DialPadContextMenu.scss";
|
||||||
@import "./views/voip/_DialPadModal.scss";
|
@import "./views/voip/_DialPadModal.scss";
|
||||||
|
@import "./views/voip/_PiPContainer.scss";
|
||||||
@import "./views/voip/_VideoFeed.scss";
|
@import "./views/voip/_VideoFeed.scss";
|
||||||
|
@ -20,14 +20,15 @@ limitations under the License.
|
|||||||
background-color: $dark-panel-bg-color;
|
background-color: $dark-panel-bg-color;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
padding-right: 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;
|
pointer-events: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_large {
|
.mx_CallView_large {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
margin: $container-gap-width;
|
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;
|
margin-bottom: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -46,7 +47,7 @@ limitations under the License.
|
|||||||
width: 320px;
|
width: 320px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
background-color: $system;
|
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;
|
border-radius: 8px;
|
||||||
|
|
||||||
.mx_CallView_video_hold,
|
.mx_CallView_video_hold,
|
||||||
@ -170,7 +171,7 @@ limitations under the License.
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
filter: blur(20px);
|
filter: blur(20px);
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -194,10 +195,10 @@ limitations under the License.
|
|||||||
display: block;
|
display: block;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
content: '';
|
content: "";
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-image: url('$(res)/img/voip/paused.svg');
|
background-image: url("$(res)/img/voip/paused.svg");
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CallContainer {
|
.mx_PiPContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
bottom: 72px;
|
bottom: 72px;
|
||||||
@ -25,8 +25,4 @@ limitations under the License.
|
|||||||
// sure the cursor hits the iframe for Jitsi which will be at a
|
// sure the cursor hits the iframe for Jitsi which will be at a
|
||||||
// different level.
|
// different level.
|
||||||
pointer-events: none;
|
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 { hideToast as hideServerLimitToast, showToast as showServerLimitToast } from "../../toasts/ServerLimitToast";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import LeftPanel from "./LeftPanel";
|
import LeftPanel from "./LeftPanel";
|
||||||
import CallContainer from '../views/voip/CallContainer';
|
import PipContainer from '../views/voip/PipContainer';
|
||||||
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||||
@ -674,7 +674,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CallContainer />
|
<PipContainer />
|
||||||
<NonUrgentToastContainer />
|
<NonUrgentToastContainer />
|
||||||
<HostSignupContainer />
|
<HostSignupContainer />
|
||||||
{ audioFeedArraysForCalls }
|
{ 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
|
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||||
// AppTile's border is in the wrong place
|
// 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">
|
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 }
|
{ appTileBody }
|
||||||
</PersistedElement>
|
</PersistedElement>
|
||||||
</div>;
|
</div>;
|
||||||
@ -545,15 +550,15 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||||||
if (!this.props.hideMaximiseButton) {
|
if (!this.props.hideMaximiseButton) {
|
||||||
const widgetIsMaximised = WidgetLayoutStore.instance.
|
const widgetIsMaximised = WidgetLayoutStore.instance.
|
||||||
isInContainer(this.props.room, this.props.app, Container.Center);
|
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
|
maxMinButton = <AccessibleButton
|
||||||
className={
|
className={className}
|
||||||
"mx_AppTileMenuBar_iconButton"
|
|
||||||
+ (widgetIsMaximised
|
|
||||||
? " mx_AppTileMenuBar_iconButton_minWidget"
|
|
||||||
: " mx_AppTileMenuBar_iconButton_maxWidget")
|
|
||||||
}
|
|
||||||
title={
|
title={
|
||||||
widgetIsMaximised ? _t('Close'): _t('Maximise widget')
|
widgetIsMaximised ? _t('Close') : _t('Maximise widget')
|
||||||
}
|
}
|
||||||
onClick={this.onMaxMinWidgetClick}
|
onClick={this.onMaxMinWidgetClick}
|
||||||
/>;
|
/>;
|
||||||
|
@ -184,7 +184,7 @@ export default class PersistedElement extends React.Component<IProps> {
|
|||||||
width: parentRect.width + 'px',
|
width: parentRect.width + 'px',
|
||||||
height: parentRect.height + 'px',
|
height: parentRect.height + 'px',
|
||||||
});
|
});
|
||||||
}, 100, { trailing: true, leading: true });
|
}, 16, { trailing: true, leading: true });
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return <div ref={this.collectChildContainer} />;
|
return <div ref={this.collectChildContainer} />;
|
||||||
|
@ -16,141 +16,79 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { EventSubscription } from 'fbemitter';
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
|
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import AppTile from "./AppTile";
|
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 {
|
interface IProps {
|
||||||
// none
|
persistentWidgetId: string;
|
||||||
|
pointerEvents?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
persistentWidgetId: string;
|
|
||||||
rightPanelPhase?: RightPanelPhases;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.PersistentApp")
|
@replaceableComponent("views.elements.PersistentApp")
|
||||||
export default class PersistentApp extends React.Component<IProps, IState> {
|
export default class PersistentApp extends React.Component<IProps, IState> {
|
||||||
private roomStoreToken: EventSubscription;
|
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: RoomViewStore.getRoomId(),
|
||||||
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
|
|
||||||
rightPanelPhase: RightPanelStore.instance.currentCard.phase,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
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);
|
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
if (this.roomStoreToken) {
|
MatrixClientPeg.get().off("Room.myMembership", this.onMyMembership);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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> => {
|
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") {
|
if (membership !== "join") {
|
||||||
// we're not in the room anymore - delete
|
// we're not in the room anymore - delete
|
||||||
if (room .roomId === persistentWidgetInRoomId) {
|
if (room.roomId === persistentWidgetInRoomId) {
|
||||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId);
|
ActiveWidgetStore.instance.destroyPersistentWidget(this.props.persistentWidgetId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const wId = this.state.persistentWidgetId;
|
const wId = this.props.persistentWidgetId;
|
||||||
if (wId) {
|
if (wId) {
|
||||||
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(wId);
|
const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(wId);
|
||||||
|
|
||||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||||
|
|
||||||
// Sanity check the room - the widget may have been destroyed between render cycles, and
|
// get the widget data
|
||||||
// thus no room is associated anymore.
|
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||||
if (!persistentWidgetInRoom) return null;
|
return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
|
||||||
|
});
|
||||||
const wls = WidgetLayoutStore.instance;
|
const app = WidgetUtils.makeAppConfig(
|
||||||
|
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
|
||||||
const userIsPartOfTheRoom = persistentWidgetInRoom.getMyMembership() == "join";
|
persistentWidgetInRoomId, appEvent.getId(),
|
||||||
const fromAnotherRoom = this.state.roomId !== persistentWidgetInRoomId;
|
);
|
||||||
|
return <AppTile
|
||||||
const notInRightPanel =
|
key={app.id}
|
||||||
!(this.state.rightPanelPhase == RightPanelPhases.Widget &&
|
app={app}
|
||||||
wId == RightPanelStore.instance.currentCard.state?.widgetId);
|
fullWidth={true}
|
||||||
const notInCenterContainer =
|
room={persistentWidgetInRoom}
|
||||||
!wls.getContainerWidgets(persistentWidgetInRoom, Container.Center).some((app) => app.id == wId);
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
const notInTopContainer =
|
creatorUserId={app.creatorUserId}
|
||||||
!wls.getContainerWidgets(persistentWidgetInRoom, Container.Top).some(app => app.id == wId);
|
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||||
if (
|
waitForIframeLoad={app.waitForIframeLoad}
|
||||||
// the widget should only be shown as a persistent app (in a floating pip container) if it is not visible on screen
|
miniMode={true}
|
||||||
// either, because we are viewing a different room OR because it is in none of the possible containers of the room view.
|
showMenubar={false}
|
||||||
(fromAnotherRoom && userIsPartOfTheRoom) ||
|
pointerEvents={this.props.pointerEvents}
|
||||||
(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}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||||||
// Close the sticker picker when the window resizes
|
// Close the sticker picker when the window resizes
|
||||||
window.addEventListener('resize', this.onResize);
|
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
|
// Track updates to widget state in account data
|
||||||
MatrixClientPeg.get().on('accountData', this.updateWidget);
|
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) {
|
switch (payload.action) {
|
||||||
case "user_widget_updated":
|
case "user_widget_updated":
|
||||||
this.forceUpdate();
|
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 {
|
interface CallViewHeaderProps {
|
||||||
pipMode: boolean;
|
pipMode: boolean;
|
||||||
type: CallType;
|
type?: CallType;
|
||||||
callRooms?: Room[];
|
callRooms?: Room[];
|
||||||
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
@ -93,9 +93,9 @@ const CallViewHeader: React.FC<CallViewHeaderProps> = ({
|
|||||||
onPipMouseDown,
|
onPipMouseDown,
|
||||||
}) => {
|
}) => {
|
||||||
const [callRoom, onHoldCallRoom] = callRooms;
|
const [callRoom, onHoldCallRoom] = callRooms;
|
||||||
const callTypeText = _t(callTypeTranslationByType[type]);
|
const callTypeText = type ? _t(callTypeTranslationByType[type]) : _t("Widget");
|
||||||
const callRoomName = callRoom.name;
|
const callRoomName = callRoom?.name;
|
||||||
const { roomId } = callRoom;
|
const roomId = callRoom?.roomId;
|
||||||
|
|
||||||
if (!pipMode) {
|
if (!pipMode) {
|
||||||
return <div className="mx_CallViewHeader">
|
return <div className="mx_CallViewHeader">
|
||||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import CallPreview from './CallPreview';
|
import PipView from './PipView';
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@ -28,11 +28,11 @@ interface IState {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.voip.CallContainer")
|
@replaceableComponent("views.voip.PiPContainer")
|
||||||
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
export default class PiPContainer extends React.PureComponent<IProps, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
return <div className="mx_CallContainer">
|
return <div className="mx_PiPContainer">
|
||||||
<CallPreview />
|
<PipView />
|
||||||
</div>;
|
</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",
|
"Fill Screen": "Fill Screen",
|
||||||
"Return to call": "Return to call",
|
"Return to call": "Return to call",
|
||||||
"%(name)s on hold": "%(name)s on hold",
|
"%(name)s on hold": "%(name)s on hold",
|
||||||
|
"Widget": "Widget",
|
||||||
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
||||||
"Verified!": "Verified!",
|
"Verified!": "Verified!",
|
||||||
"You've successfully verified this user.": "You've successfully verified this user.",
|
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||||
|
Loading…
Reference in New Issue
Block a user