New group call experience: Room header and PiP designs (#9351)
* Update our cancel icon The cancel icon we're using in the app has drifted out of sync with the ones used in our designs. We also had two identical-looking icons, so this consolidates them into one. I've simultaneously updated our chevron icons, since in the case of the 'jump to unread' timeline button, it became clear that the weight of the new close icon did not match the thinner chevron. * Don't squish bottom/top-aligned tooltips near the edge of the screen * Close the timeline panel when returning to the fullscreen timeline view * Add layout switching capabilities to ElementCall * Bring the room header in line with the group call designs * Bring the PiP header in line with the group call designs * Fix lints * Clarify tooltip CSS calculations * Test PipView * Expand RoomHeader test coverage * Test PipView more
This commit is contained in:
@@ -121,6 +121,7 @@ import { LargeLoader } from './LargeLoader';
|
||||
import { VoiceBroadcastInfoEventType } from '../../voice-broadcast';
|
||||
import { isVideoRoom } from '../../utils/video-rooms';
|
||||
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
||||
import { Call } from "../../models/Call";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
@@ -178,6 +179,7 @@ export interface IRoomState {
|
||||
searchHighlights?: string[];
|
||||
searchInProgress?: boolean;
|
||||
callState?: CallState;
|
||||
activeCall: Call | null;
|
||||
canPeek: boolean;
|
||||
canSelfRedact: boolean;
|
||||
showApps: boolean;
|
||||
@@ -303,6 +305,8 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
||||
excludedRightPanelPhaseButtons={[]}
|
||||
showButtons={false}
|
||||
enableRoomOptionsMenu={false}
|
||||
viewingCall={false}
|
||||
activeCall={null}
|
||||
/>
|
||||
<main className="mx_RoomView_body" ref={props.roomView}>
|
||||
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
|
||||
@@ -353,6 +357,8 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
|
||||
excludedRightPanelPhaseButtons={[]}
|
||||
showButtons={false}
|
||||
enableRoomOptionsMenu={false}
|
||||
viewingCall={false}
|
||||
activeCall={null}
|
||||
/>
|
||||
<div className="mx_RoomView_body">
|
||||
<LargeLoader text={text} />
|
||||
@@ -391,6 +397,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
numUnreadMessages: 0,
|
||||
searchResults: null,
|
||||
callState: null,
|
||||
activeCall: null,
|
||||
canPeek: false,
|
||||
canSelfRedact: false,
|
||||
showApps: false,
|
||||
@@ -497,13 +504,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) {
|
||||
// Show chat in right panel when a widget is maximised
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline });
|
||||
} else if (
|
||||
RightPanelStore.instance.isOpen &&
|
||||
RightPanelStore.instance.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
|
||||
) {
|
||||
// hide chat in right panel when the widget is minimized
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary });
|
||||
RightPanelStore.instance.togglePanel(this.state.roomId);
|
||||
}
|
||||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
@@ -571,8 +571,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
mainSplitContentType: room === null ? undefined : this.getMainSplitContentType(room),
|
||||
initialEventId: null, // default to clearing this, will get set later in the method if needed
|
||||
showRightPanel: RightPanelStore.instance.isOpenForRoom(roomId),
|
||||
activeCall: CallStore.instance.getActiveCall(roomId),
|
||||
};
|
||||
|
||||
if (
|
||||
this.state.mainSplitContentType !== MainSplitContentType.Timeline
|
||||
&& newState.mainSplitContentType === MainSplitContentType.Timeline
|
||||
&& RightPanelStore.instance.isOpen
|
||||
&& RightPanelStore.instance.currentCard.phase === RightPanelPhases.Timeline
|
||||
&& RightPanelStore.instance.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
|
||||
) {
|
||||
// We're returning to the main timeline, so hide the right panel timeline
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary });
|
||||
RightPanelStore.instance.togglePanel(this.state.roomId ?? null);
|
||||
newState.showRightPanel = false;
|
||||
}
|
||||
|
||||
const initialEventId = RoomViewStore.instance.getInitialEventId();
|
||||
if (initialEventId) {
|
||||
let initialEvent = room?.findEventById(initialEventId);
|
||||
@@ -701,7 +715,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
};
|
||||
|
||||
private onActiveCalls = () => {
|
||||
if (this.state.roomId !== undefined && !CallStore.instance.hasActiveCall(this.state.roomId)) {
|
||||
if (this.state.roomId === undefined) return;
|
||||
const activeCall = CallStore.instance.getActiveCall(this.state.roomId);
|
||||
|
||||
if (activeCall === null) {
|
||||
// We disconnected from the call, so stop viewing it
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
@@ -710,6 +727,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
metricsTrigger: undefined,
|
||||
}, true); // Synchronous so that CallView disappears immediately
|
||||
}
|
||||
|
||||
this.setState({ activeCall });
|
||||
};
|
||||
|
||||
private getRoomId = () => {
|
||||
@@ -2404,6 +2423,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
let onForgetClick = this.onForgetClick;
|
||||
let onSearchClick = this.onSearchClick;
|
||||
let onInviteClick = null;
|
||||
let viewingCall = false;
|
||||
|
||||
// Simplify the header for other main split types
|
||||
switch (this.state.mainSplitContentType) {
|
||||
@@ -2422,12 +2442,19 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
RightPanelPhases.PinnedMessages,
|
||||
RightPanelPhases.NotificationPanel,
|
||||
];
|
||||
if (!isVideoRoom(this.state.room)) {
|
||||
excludedRightPanelPhaseButtons.push(RightPanelPhases.RoomSummary);
|
||||
if (this.state.activeCall === null) {
|
||||
excludedRightPanelPhaseButtons.push(RightPanelPhases.Timeline);
|
||||
}
|
||||
}
|
||||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
if (this.state.room.canInvite(this.context.credentials.userId)) {
|
||||
onInviteClick = this.onInviteClick;
|
||||
}
|
||||
viewingCall = true;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -2451,6 +2478,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
||||
showButtons={!this.viewsLocalRoom}
|
||||
enableRoomOptionsMenu={!this.viewsLocalRoom}
|
||||
viewingCall={viewingCall}
|
||||
activeCall={this.state.activeCall}
|
||||
/>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
<div className={mainSplitContentClasses} ref={this.roomViewBody} data-layout={this.state.layout}>
|
||||
|
||||
@@ -493,7 +493,6 @@ export class EmailIdentityAuthEntry extends
|
||||
? _t("Resent!")
|
||||
: _t("Resend")}
|
||||
alignment={Alignment.Right}
|
||||
tooltipClassName="mx_Tooltip_noMargin"
|
||||
onHideTooltip={this.state.requested
|
||||
? () => this.setState({ requested: false })
|
||||
: undefined}
|
||||
|
||||
@@ -149,18 +149,24 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
||||
break;
|
||||
case Alignment.Top:
|
||||
style.top = baseTop - spacing;
|
||||
style.left = horizontalCenter;
|
||||
style.transform = "translate(-50%, -100%)";
|
||||
// Attempt to center the tooltip on the element while clamping
|
||||
// its horizontal translation to keep it on screen
|
||||
// eslint-disable-next-line max-len
|
||||
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))), -100%)`;
|
||||
break;
|
||||
case Alignment.Bottom:
|
||||
style.top = baseTop + parentBox.height + spacing;
|
||||
style.left = horizontalCenter;
|
||||
style.transform = "translate(-50%)";
|
||||
// Attempt to center the tooltip on the element while clamping
|
||||
// its horizontal translation to keep it on screen
|
||||
// eslint-disable-next-line max-len
|
||||
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
|
||||
break;
|
||||
case Alignment.InnerBottom:
|
||||
style.top = baseTop + parentBox.height - 50;
|
||||
style.left = horizontalCenter;
|
||||
style.transform = "translate(-50%)";
|
||||
// Attempt to center the tooltip on the element while clamping
|
||||
// its horizontal translation to keep it on screen
|
||||
// eslint-disable-next-line max-len
|
||||
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
|
||||
break;
|
||||
case Alignment.TopRight:
|
||||
style.top = baseTop - spacing;
|
||||
|
||||
@@ -23,6 +23,7 @@ import classNames from 'classnames';
|
||||
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
interface IProps {
|
||||
// Whether this button is highlighted
|
||||
@@ -54,6 +55,7 @@ export default class HeaderButton extends React.Component<IProps> {
|
||||
aria-selected={isHighlighted}
|
||||
role="tab"
|
||||
title={title}
|
||||
alignment={Alignment.Bottom}
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
/>;
|
||||
|
||||
@@ -282,7 +282,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||
<HeaderButton
|
||||
key="roomSummaryButton"
|
||||
name="roomSummaryButton"
|
||||
title={_t('Room Info')}
|
||||
title={_t('Room info')}
|
||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||
onClick={this.onRoomSummaryClicked}
|
||||
/>,
|
||||
|
||||
@@ -20,7 +20,7 @@ import classNames from 'classnames';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
|
||||
export enum E2EState {
|
||||
@@ -49,10 +49,20 @@ interface IProps {
|
||||
size?: number;
|
||||
onClick?: () => void;
|
||||
hideTooltip?: boolean;
|
||||
tooltipAlignment?: Alignment;
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
||||
const E2EIcon: React.FC<IProps> = ({
|
||||
isUser,
|
||||
status,
|
||||
className,
|
||||
size,
|
||||
onClick,
|
||||
hideTooltip,
|
||||
tooltipAlignment,
|
||||
bordered,
|
||||
}) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const classes = classNames({
|
||||
@@ -80,7 +90,7 @@ const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, h
|
||||
|
||||
let tip;
|
||||
if (hover && !hideTooltip) {
|
||||
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
|
||||
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />;
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserTab";
|
||||
@@ -32,7 +31,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
@@ -57,14 +56,17 @@ import SdkConfig from "../../../SdkConfig";
|
||||
import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { useWidgets } from "../right_panel/RoomSummaryCard";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { useCall } from "../../../hooks/useCall";
|
||||
import { useCall, useLayout } from "../../../hooks/useCall";
|
||||
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
|
||||
import { ElementCall } from "../../../models/Call";
|
||||
import { Call, ElementCall, Layout } from "../../../models/Call";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { CallDurationFromEvent } from "../voip/CallDuration";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
class DisabledWithReason {
|
||||
constructor(public readonly reason: string) { }
|
||||
@@ -107,6 +109,7 @@ const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavi
|
||||
onClick={onClick}
|
||||
title={_t("Voice call")}
|
||||
tooltip={tooltip ?? _t("Voice call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>;
|
||||
};
|
||||
@@ -207,6 +210,7 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
|
||||
onClick={onClick}
|
||||
title={_t("Video call")}
|
||||
tooltip={tooltip ?? _t("Video call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>
|
||||
{ menu }
|
||||
@@ -318,6 +322,72 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
|
||||
}
|
||||
};
|
||||
|
||||
interface CallLayoutSelectorProps {
|
||||
call: ElementCall;
|
||||
}
|
||||
|
||||
const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
|
||||
const layout = useLayout(call);
|
||||
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
const onClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
openMenu();
|
||||
}, [openMenu]);
|
||||
|
||||
const onFreedomClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
call.setLayout(Layout.Tile);
|
||||
}, [closeMenu, call]);
|
||||
|
||||
const onSpotlightClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
call.setLayout(Layout.Spotlight);
|
||||
}, [closeMenu, call]);
|
||||
|
||||
let menu: JSX.Element | null = null;
|
||||
if (menuOpen) {
|
||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||
menu = <IconizedContextMenu
|
||||
className="mx_RoomHeader_layoutMenu"
|
||||
{...aboveLeftOf(buttonRect)}
|
||||
onFinished={closeMenu}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_freedomIcon"
|
||||
label={_t("Freedom")}
|
||||
active={layout === Layout.Tile}
|
||||
onClick={onFreedomClick}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_spotlightIcon"
|
||||
label={_t("Spotlight")}
|
||||
active={layout === Layout.Spotlight}
|
||||
onClick={onSpotlightClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return <>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className={classNames("mx_RoomHeader_button", {
|
||||
"mx_RoomHeader_layoutButton--freedom": layout === Layout.Tile,
|
||||
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={_t("Layout type")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="layout"
|
||||
/>
|
||||
{ menu }
|
||||
</>;
|
||||
};
|
||||
|
||||
export interface ISearchInfo {
|
||||
searchTerm: string;
|
||||
searchScope: SearchScope;
|
||||
@@ -338,6 +408,8 @@ export interface IProps {
|
||||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||
showButtons?: boolean;
|
||||
enableRoomOptionsMenu?: boolean;
|
||||
viewingCall: boolean;
|
||||
activeCall: Call | null;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -356,6 +428,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
private readonly client = this.props.room.client;
|
||||
|
||||
constructor(props: IProps, context: IState) {
|
||||
super(props, context);
|
||||
@@ -367,14 +440,12 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli?.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
@@ -401,7 +472,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
this.forceUpdate();
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
private onContextMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
private onContextMenuOpenClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
@@ -412,56 +483,98 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
this.setState({ contextMenuPosition: undefined });
|
||||
};
|
||||
|
||||
private renderButtons(): JSX.Element[] {
|
||||
const buttons: JSX.Element[] = [];
|
||||
private onHideCallClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.props.room.roomId,
|
||||
view_call: false,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.props.inRoom && !this.context.tombstone) {
|
||||
buttons.push(<CallButtons key="calls" room={this.props.room} />);
|
||||
private renderButtons(isVideoRoom: boolean): React.ReactNode {
|
||||
const startButtons: JSX.Element[] = [];
|
||||
|
||||
if (!this.props.viewingCall && this.props.inRoom && !this.context.tombstone) {
|
||||
startButtons.push(<CallButtons key="calls" room={this.props.room} />);
|
||||
}
|
||||
|
||||
if (this.props.onForgetClick) {
|
||||
const forgetButton = <AccessibleTooltipButton
|
||||
if (this.props.viewingCall && this.props.activeCall instanceof ElementCall) {
|
||||
startButtons.push(<CallLayoutSelector call={this.props.activeCall} />);
|
||||
}
|
||||
|
||||
if (!this.props.viewingCall && this.props.onForgetClick) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
||||
onClick={this.props.onForgetClick}
|
||||
title={_t("Forget room")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="forget"
|
||||
/>;
|
||||
buttons.push(forgetButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.onAppsClick) {
|
||||
const appsButton = <AccessibleTooltipButton
|
||||
if (!this.props.viewingCall && this.props.onAppsClick) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
||||
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
||||
})}
|
||||
onClick={this.props.onAppsClick}
|
||||
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="apps"
|
||||
/>;
|
||||
buttons.push(appsButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
const searchButton = <AccessibleTooltipButton
|
||||
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
||||
onClick={this.props.onSearchClick}
|
||||
title={_t("Search")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="search"
|
||||
/>;
|
||||
buttons.push(searchButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.onInviteClick && this.props.inRoom) {
|
||||
const inviteButton = <AccessibleTooltipButton
|
||||
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_inviteButton"
|
||||
onClick={this.props.onInviteClick}
|
||||
title={_t("Invite")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="invite"
|
||||
/>;
|
||||
buttons.push(inviteButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
const endButtons: JSX.Element[] = [];
|
||||
|
||||
if (this.props.viewingCall && !isVideoRoom) {
|
||||
if (this.props.activeCall === null) {
|
||||
endButtons.push(<AccessibleButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_closeButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("Close call")}
|
||||
key="close"
|
||||
/>);
|
||||
} else {
|
||||
endButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_minimiseButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("View chat timeline")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="minimise"
|
||||
/>);
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
{ startButtons }
|
||||
<RoomHeaderButtons
|
||||
room={this.props.room}
|
||||
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
|
||||
/>
|
||||
{ endButtons }
|
||||
</>;
|
||||
}
|
||||
|
||||
private renderName(oobName: string) {
|
||||
@@ -480,7 +593,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
if (members.length === 1 && members[0].userId === this.client.credentials.userId) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
@@ -505,6 +618,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
onClick={this.onContextMenuOpenClick}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
title={_t("Room options")}
|
||||
alignment={Alignment.Bottom}
|
||||
>
|
||||
{ roomName }
|
||||
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
|
||||
@@ -519,6 +633,57 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
|
||||
|
||||
let roomAvatar: JSX.Element | null = null;
|
||||
if (this.props.room) {
|
||||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
const icon = this.props.viewingCall
|
||||
? <div className="mx_RoomHeader_icon mx_RoomHeader_icon_video" />
|
||||
: this.props.e2eStatus
|
||||
? <E2EIcon
|
||||
className="mx_RoomHeader_icon"
|
||||
status={this.props.e2eStatus}
|
||||
tooltipAlignment={Alignment.Bottom}
|
||||
/>
|
||||
// If we're expecting an E2EE status to come in, but it hasn't
|
||||
// yet been loaded, insert a blank div to reserve space
|
||||
: this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled()
|
||||
? <div className="mx_RoomHeader_icon" />
|
||||
: null;
|
||||
|
||||
const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null;
|
||||
|
||||
if (this.props.viewingCall && !isVideoRoom) {
|
||||
return (
|
||||
<header className="mx_RoomHeader light-panel">
|
||||
<div
|
||||
className="mx_RoomHeader_wrapper"
|
||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
{ icon }
|
||||
<div className="mx_RoomHeader_name mx_RoomHeader_name--textonly mx_RoomHeader_name--small">
|
||||
{ _t("Video call") }
|
||||
</div>
|
||||
{ this.props.activeCall instanceof ElementCall && (
|
||||
<CallDurationFromEvent mxEvent={this.props.activeCall.groupCall} />
|
||||
) }
|
||||
{ /* Empty topic element to fill out space */ }
|
||||
<div className="mx_RoomHeader_topic" />
|
||||
{ buttons }
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
let searchStatus: JSX.Element | null = null;
|
||||
|
||||
// don't display the search count until the search completes and
|
||||
@@ -543,29 +708,6 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
className="mx_RoomHeader_topic"
|
||||
/>;
|
||||
|
||||
let roomAvatar: JSX.Element | null = null;
|
||||
if (this.props.room) {
|
||||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
let buttons: JSX.Element | null = null;
|
||||
if (this.props.showButtons) {
|
||||
buttons = <React.Fragment>
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ this.renderButtons() }
|
||||
</div>
|
||||
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
|
||||
|
||||
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
|
||||
const viewLabs = () => defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
@@ -581,7 +723,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
||||
{ icon }
|
||||
{ name }
|
||||
{ searchStatus }
|
||||
{ topicElement }
|
||||
|
||||
@@ -89,7 +89,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
call: CallStore.instance.get(this.props.room.roomId),
|
||||
call: CallStore.instance.getCall(this.props.room.roomId),
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: "",
|
||||
};
|
||||
@@ -159,7 +159,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
||||
|
||||
// Recalculate the call for this room, since it could've changed between
|
||||
// construction and mounting
|
||||
this.setState({ call: CallStore.instance.get(this.props.room.roomId) });
|
||||
this.setState({ call: CallStore.instance.getCall(this.props.room.roomId) });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
||||
@@ -32,7 +32,7 @@ const LegacyCallViewHeaderControls: React.FC<LegacyCallControlsProps> = ({ onExp
|
||||
{ onMaximize && <AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_fullscreen"
|
||||
onClick={onMaximize}
|
||||
title={_t("Fill Screen")}
|
||||
title={_t("Fill screen")}
|
||||
/> }
|
||||
{ onPin && <AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_pin"
|
||||
|
||||
@@ -201,7 +201,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<aside
|
||||
className={this.props.className}
|
||||
style={style}
|
||||
ref={this.callViewWrapper}
|
||||
@@ -211,7 +211,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
||||
onStartMoving: this.onStartMoving,
|
||||
onResize: this.onResize,
|
||||
}) }
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import LegacyCallView from "./LegacyCallView";
|
||||
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
@@ -35,6 +34,7 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/Activ
|
||||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import { CallStore } from "../../../stores/CallStore";
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
@@ -116,7 +116,6 @@ function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall
|
||||
*/
|
||||
|
||||
export default class PipView extends React.Component<IProps, IState> {
|
||||
private settingsWatcherRef: string;
|
||||
private movePersistedElement = createRef<() => void>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
@@ -157,7 +156,6 @@ export default class PipView extends React.Component<IProps, IState> {
|
||||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||
const room = MatrixClientPeg.get().getRoom(this.state.viewedRoomId);
|
||||
if (room) {
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
|
||||
@@ -278,6 +276,14 @@ export default class PipView extends React.Component<IProps, IState> {
|
||||
});
|
||||
};
|
||||
|
||||
private onViewCall = (): void =>
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.persistentRoomId,
|
||||
view_call: true,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
|
||||
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
|
||||
public updateShowWidgetInPip(
|
||||
persistentWidgetId = this.state.persistentWidgetId,
|
||||
@@ -323,18 +329,19 @@ export default class PipView extends React.Component<IProps, IState> {
|
||||
mx_LegacyCallView_large: !pipMode,
|
||||
});
|
||||
const roomId = this.state.persistentRoomId;
|
||||
const roomForWidget = MatrixClientPeg.get().getRoom(roomId);
|
||||
const roomForWidget = MatrixClientPeg.get().getRoom(roomId)!;
|
||||
const viewingCallRoom = this.state.viewedRoomId === roomId;
|
||||
const isCall = CallStore.instance.getActiveCall(roomId) !== null;
|
||||
|
||||
pipContent = ({ onStartMoving, _onResize }) =>
|
||||
pipContent = ({ onStartMoving }) =>
|
||||
<div className={pipViewClasses}>
|
||||
<LegacyCallViewHeader
|
||||
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
||||
pipMode={pipMode}
|
||||
callRooms={[roomForWidget]}
|
||||
onExpand={!viewingCallRoom && this.onExpand}
|
||||
onPin={viewingCallRoom && this.onPin}
|
||||
onMaximize={viewingCallRoom && this.onMaximize}
|
||||
onExpand={!isCall && !viewingCallRoom ? this.onExpand : undefined}
|
||||
onPin={!isCall && viewingCallRoom ? this.onPin : undefined}
|
||||
onMaximize={isCall ? this.onViewCall : viewingCallRoom ? this.onMaximize : undefined}
|
||||
/>
|
||||
<PersistentApp
|
||||
persistentWidgetId={this.state.persistentWidgetId}
|
||||
|
||||
@@ -65,6 +65,7 @@ const RoomContext = createContext<IRoomState>({
|
||||
threadId: undefined,
|
||||
liveTimeline: undefined,
|
||||
narrow: false,
|
||||
activeCall: null,
|
||||
});
|
||||
RoomContext.displayName = "RoomContext";
|
||||
export default RoomContext;
|
||||
|
||||
@@ -17,14 +17,14 @@ limitations under the License.
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { Call, ConnectionState } from "../models/Call";
|
||||
import type { Call, ConnectionState, ElementCall, Layout } from "../models/Call";
|
||||
import { useTypedEventEmitterState } from "./useEventEmitter";
|
||||
import { CallEvent } from "../models/Call";
|
||||
import { CallStore, CallStoreEvent } from "../stores/CallStore";
|
||||
import { useEventEmitter } from "./useEventEmitter";
|
||||
|
||||
export const useCall = (roomId: string): Call | null => {
|
||||
const [call, setCall] = useState(() => CallStore.instance.get(roomId));
|
||||
const [call, setCall] = useState(() => CallStore.instance.getCall(roomId));
|
||||
useEventEmitter(CallStore.instance, CallStoreEvent.Call, (call: Call | null, forRoomId: string) => {
|
||||
if (forRoomId === roomId) setCall(call);
|
||||
});
|
||||
@@ -44,3 +44,10 @@ export const useParticipants = (call: Call): Set<RoomMember> =>
|
||||
CallEvent.Participants,
|
||||
useCallback(state => state ?? call.participants, [call]),
|
||||
);
|
||||
|
||||
export const useLayout = (call: ElementCall): Layout =>
|
||||
useTypedEventEmitterState(
|
||||
call,
|
||||
CallEvent.Layout,
|
||||
useCallback(state => state ?? call.layout, [call]),
|
||||
);
|
||||
|
||||
@@ -1082,7 +1082,7 @@
|
||||
"Show sidebar": "Show sidebar",
|
||||
"More": "More",
|
||||
"Hangup": "Hangup",
|
||||
"Fill Screen": "Fill Screen",
|
||||
"Fill screen": "Fill screen",
|
||||
"Pin": "Pin",
|
||||
"Return to call": "Return to call",
|
||||
"%(name)s on hold": "%(name)s on hold",
|
||||
@@ -1894,11 +1894,16 @@
|
||||
"You do not have permission to start video calls": "You do not have permission to start video calls",
|
||||
"There's no one here to call": "There's no one here to call",
|
||||
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
|
||||
"Freedom": "Freedom",
|
||||
"Spotlight": "Spotlight",
|
||||
"Layout type": "Layout type",
|
||||
"Forget room": "Forget room",
|
||||
"Hide Widgets": "Hide Widgets",
|
||||
"Show Widgets": "Show Widgets",
|
||||
"Search": "Search",
|
||||
"Invite": "Invite",
|
||||
"Close call": "Close call",
|
||||
"View chat timeline": "View chat timeline",
|
||||
"Room options": "Room options",
|
||||
"(~%(count)s results)|other": "(~%(count)s results)",
|
||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||
@@ -2094,7 +2099,7 @@
|
||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||
"Pinned messages": "Pinned messages",
|
||||
"Chat": "Chat",
|
||||
"Room Info": "Room Info",
|
||||
"Room info": "Room info",
|
||||
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||
"Maximise": "Maximise",
|
||||
"Unpin this widget to view it in this panel": "Unpin this widget to view it in this panel",
|
||||
|
||||
+45
-1
@@ -71,15 +71,22 @@ export enum ConnectionState {
|
||||
export const isConnected = (state: ConnectionState): boolean =>
|
||||
state === ConnectionState.Connected || state === ConnectionState.Disconnecting;
|
||||
|
||||
export enum Layout {
|
||||
Tile = "tile",
|
||||
Spotlight = "spotlight",
|
||||
}
|
||||
|
||||
export enum CallEvent {
|
||||
ConnectionState = "connection_state",
|
||||
Participants = "participants",
|
||||
Layout = "layout",
|
||||
Destroy = "destroy",
|
||||
}
|
||||
|
||||
interface CallEventHandlerMap {
|
||||
[CallEvent.ConnectionState]: (state: ConnectionState, prevState: ConnectionState) => void;
|
||||
[CallEvent.Participants]: (participants: Set<RoomMember>, prevParticipants: Set<RoomMember>) => void;
|
||||
[CallEvent.Layout]: (layout: Layout) => void;
|
||||
[CallEvent.Destroy]: () => void;
|
||||
}
|
||||
|
||||
@@ -110,7 +117,7 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
return this.widget.roomId;
|
||||
}
|
||||
|
||||
private _connectionState: ConnectionState = ConnectionState.Disconnected;
|
||||
private _connectionState = ConnectionState.Disconnected;
|
||||
public get connectionState(): ConnectionState {
|
||||
return this._connectionState;
|
||||
}
|
||||
@@ -604,6 +611,15 @@ export class ElementCall extends Call {
|
||||
private participantsExpirationTimer: number | null = null;
|
||||
private terminationTimer: number | null = null;
|
||||
|
||||
private _layout = Layout.Tile;
|
||||
public get layout(): Layout {
|
||||
return this._layout;
|
||||
}
|
||||
protected set layout(value: Layout) {
|
||||
this._layout = value;
|
||||
this.emit(CallEvent.Layout, value);
|
||||
}
|
||||
|
||||
private constructor(public readonly groupCall: MatrixEvent, client: MatrixClient) {
|
||||
// Splice together the Element Call URL for this call
|
||||
const url = new URL(SdkConfig.get("element_call").url);
|
||||
@@ -779,6 +795,8 @@ export class ElementCall extends Call {
|
||||
}
|
||||
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
}
|
||||
|
||||
protected async performDisconnection(): Promise<void> {
|
||||
@@ -791,6 +809,8 @@ export class ElementCall extends Call {
|
||||
|
||||
public setDisconnected() {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
super.setDisconnected();
|
||||
}
|
||||
|
||||
@@ -812,6 +832,18 @@ export class ElementCall extends Call {
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the call's layout.
|
||||
* @param layout The layout to switch to.
|
||||
*/
|
||||
public async setLayout(layout: Layout): Promise<void> {
|
||||
const action = layout === Layout.Tile
|
||||
? ElementWidgetActions.TileLayout
|
||||
: ElementWidgetActions.SpotlightLayout;
|
||||
|
||||
await this.messaging!.transport.send(action, {});
|
||||
}
|
||||
|
||||
private get mayTerminate(): boolean {
|
||||
return this.groupCall.getContent()["m.intent"] !== "m.room"
|
||||
&& this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client);
|
||||
@@ -869,4 +901,16 @@ export class ElementCall extends Call {
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setDisconnected();
|
||||
};
|
||||
|
||||
private onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.layout = Layout.Tile;
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
|
||||
private onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.layout = Layout.Spotlight;
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export class CallStore extends AsyncStoreWithClient<{}> {
|
||||
await Promise.all([
|
||||
...uncleanlyDisconnectedRoomIds.map(async uncleanlyDisconnectedRoomId => {
|
||||
logger.log(`Cleaning up call state for room ${uncleanlyDisconnectedRoomId}`);
|
||||
await this.get(uncleanlyDisconnectedRoomId)?.clean();
|
||||
await this.getCall(uncleanlyDisconnectedRoomId)?.clean();
|
||||
}),
|
||||
SettingsStore.setValue("activeCallRoomIds", null, SettingLevel.DEVICE, []),
|
||||
]);
|
||||
@@ -152,18 +152,18 @@ export class CallStore extends AsyncStoreWithClient<{}> {
|
||||
* @param {string} roomId The room's ID.
|
||||
* @returns {Call | null} The call.
|
||||
*/
|
||||
public get(roomId: string): Call | null {
|
||||
public getCall(roomId: string): Call | null {
|
||||
return this.calls.get(roomId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given room has an active call.
|
||||
* Gets the active call associated with the given room, if any.
|
||||
* @param roomId The room's ID.
|
||||
* @returns Whether the given room has an active call.
|
||||
* @returns The active call.
|
||||
*/
|
||||
public hasActiveCall(roomId: string): boolean {
|
||||
const call = this.get(roomId);
|
||||
return call !== null && this.activeCalls.has(call);
|
||||
public getActiveCall(roomId: string): Call | null {
|
||||
const call = this.getCall(roomId);
|
||||
return call !== null && this.activeCalls.has(call) ? call : null;
|
||||
}
|
||||
|
||||
private onRoom = (room: Room) => this.updateRoom(room);
|
||||
|
||||
@@ -365,7 +365,7 @@ export class RoomViewStore extends EventEmitter {
|
||||
viewingCall: payload.view_call ?? (
|
||||
payload.room_id === this.state.roomId
|
||||
? this.state.viewingCall
|
||||
: CallStore.instance.hasActiveCall(payload.room_id)
|
||||
: CallStore.instance.getActiveCall(payload.room_id) !== null
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user