You've already forked matrix-react-sdk
							
							
				mirror of
				https://github.com/matrix-org/matrix-react-sdk.git
				synced 2025-11-03 00:33:22 +03: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:
		@@ -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,120 +16,58 @@ 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
 | 
					 | 
				
			||||||
            // 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
 | 
					            // get the widget data
 | 
				
			||||||
            const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
 | 
					            const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
 | 
				
			||||||
                return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
 | 
					                return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
 | 
				
			||||||
@@ -149,9 +87,9 @@ export default class PersistentApp extends React.Component<IProps, IState> {
 | 
				
			|||||||
                waitForIframeLoad={app.waitForIframeLoad}
 | 
					                waitForIframeLoad={app.waitForIframeLoad}
 | 
				
			||||||
                miniMode={true}
 | 
					                miniMode={true}
 | 
				
			||||||
                showMenubar={false}
 | 
					                showMenubar={false}
 | 
				
			||||||
 | 
					                pointerEvents={this.props.pointerEvents}
 | 
				
			||||||
            />;
 | 
					            />;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        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.",
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user