Merge pull request #11576 from matrix-org/t3chguy/cr/72
Make video & voice call buttons pin conference widget if unpinned
This commit is contained in:
commit
f9f2e79fd9
11 changed files with 332 additions and 227 deletions
|
@ -23,7 +23,6 @@ import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/ico
|
||||||
import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg";
|
import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg";
|
||||||
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
|
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
|
||||||
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
|
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
|
||||||
import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { useRoomName } from "../../../hooks/useRoomName";
|
import { useRoomName } from "../../../hooks/useRoomName";
|
||||||
|
@ -35,13 +34,12 @@ import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMember
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { Flex } from "../../utils/Flex";
|
import { Flex } from "../../utils/Flex";
|
||||||
import { Box } from "../../utils/Box";
|
import { Box } from "../../utils/Box";
|
||||||
import { useRoomCallStatus } from "../../../hooks/room/useRoomCallStatus";
|
import { useRoomCall } from "../../../hooks/room/useRoomCall";
|
||||||
import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications";
|
import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications";
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
|
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||||
import { placeCall } from "../../../utils/room/placeCall";
|
|
||||||
import { useEncryptionStatus } from "../../../hooks/useEncryptionStatus";
|
import { useEncryptionStatus } from "../../../hooks/useEncryptionStatus";
|
||||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
import FacePile from "../elements/FacePile";
|
import FacePile from "../elements/FacePile";
|
||||||
|
@ -74,7 +72,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
const members = useRoomMembers(room, 2500);
|
const members = useRoomMembers(room, 2500);
|
||||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
|
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
|
||||||
|
|
||||||
const { voiceCallDisabledReason, voiceCallType, videoCallDisabledReason, videoCallType } = useRoomCallStatus(room);
|
const { voiceCallDisabledReason, voiceCallClick, videoCallDisabledReason, videoCallClick } = useRoomCall(room);
|
||||||
|
|
||||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||||
/**
|
/**
|
||||||
|
@ -170,11 +168,8 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
<Tooltip label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}>
|
<Tooltip label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={!!voiceCallDisabledReason}
|
disabled={!!voiceCallDisabledReason}
|
||||||
title={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}
|
aria-label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}
|
||||||
onClick={(evt) => {
|
onClick={voiceCallClick}
|
||||||
evt.stopPropagation();
|
|
||||||
placeCall(room, CallType.Voice, voiceCallType);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<VoiceCallIcon />
|
<VoiceCallIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -183,11 +178,8 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
<Tooltip label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}>
|
<Tooltip label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={!!videoCallDisabledReason}
|
disabled={!!videoCallDisabledReason}
|
||||||
title={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}
|
aria-label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}
|
||||||
onClick={(evt) => {
|
onClick={videoCallClick}
|
||||||
evt.stopPropagation();
|
|
||||||
placeCall(room, CallType.Video, videoCallType);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<VideoCallIcon />
|
<VideoCallIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -199,7 +191,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.ThreadPanel);
|
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.ThreadPanel);
|
||||||
}}
|
}}
|
||||||
title={_t("common|threads")}
|
aria-label={_t("common|threads")}
|
||||||
>
|
>
|
||||||
<ThreadsIcon />
|
<ThreadsIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -212,7 +204,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel);
|
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.NotificationPanel);
|
||||||
}}
|
}}
|
||||||
title={_t("Notifications")}
|
aria-label={_t("Notifications")}
|
||||||
>
|
>
|
||||||
<NotificationsIcon />
|
<NotificationsIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
219
src/hooks/room/useRoomCall.ts
Normal file
219
src/hooks/room/useRoomCall.ts
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 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 { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
|
||||||
|
import { useFeatureEnabled } from "../useSettings";
|
||||||
|
import SdkConfig from "../../SdkConfig";
|
||||||
|
import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
|
||||||
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
|
||||||
|
import { useWidgets } from "../../components/views/right_panel/RoomSummaryCard";
|
||||||
|
import { WidgetType } from "../../widgets/WidgetType";
|
||||||
|
import { useCall } from "../useCall";
|
||||||
|
import { useRoomMemberCount } from "../useRoomMembers";
|
||||||
|
import { ElementCall } from "../../models/Call";
|
||||||
|
import { placeCall } from "../../utils/room/placeCall";
|
||||||
|
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
|
||||||
|
import { useRoomState } from "../useRoomState";
|
||||||
|
import { _t } from "../../languageHandler";
|
||||||
|
import { isManagedHybridWidget } from "../../widgets/ManagedHybrid";
|
||||||
|
import { IApp } from "../../stores/WidgetStore";
|
||||||
|
|
||||||
|
export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi";
|
||||||
|
|
||||||
|
const enum State {
|
||||||
|
NoCall,
|
||||||
|
NoOneHere,
|
||||||
|
NoPermission,
|
||||||
|
Unpinned,
|
||||||
|
Ongoing,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility hook for resolving state and click handlers for Voice & Video call buttons in the room header
|
||||||
|
* @param room the room to track
|
||||||
|
* @returns the call button attributes for the given room
|
||||||
|
*/
|
||||||
|
export const useRoomCall = (
|
||||||
|
room: Room,
|
||||||
|
): {
|
||||||
|
voiceCallDisabledReason: string | null;
|
||||||
|
voiceCallClick(evt: React.MouseEvent): void;
|
||||||
|
videoCallDisabledReason: string | null;
|
||||||
|
videoCallClick(evt: React.MouseEvent): void;
|
||||||
|
} => {
|
||||||
|
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||||
|
const useElementCallExclusively = useMemo(() => {
|
||||||
|
return SdkConfig.get("element_call").use_exclusively;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hasLegacyCall = useEventEmitterState(
|
||||||
|
LegacyCallHandler.instance,
|
||||||
|
LegacyCallHandlerEvent.CallsChanged,
|
||||||
|
() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const widgets = useWidgets(room);
|
||||||
|
const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
|
||||||
|
const hasJitsiWidget = !!jitsiWidget;
|
||||||
|
const managedHybridWidget = useMemo(() => widgets.find(isManagedHybridWidget), [widgets]);
|
||||||
|
const hasManagedHybridWidget = !!managedHybridWidget;
|
||||||
|
|
||||||
|
const groupCall = useCall(room.roomId);
|
||||||
|
const hasGroupCall = groupCall !== null;
|
||||||
|
|
||||||
|
const memberCount = useRoomMemberCount(room);
|
||||||
|
|
||||||
|
const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [
|
||||||
|
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
||||||
|
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const callType = useMemo((): PlatformCallType => {
|
||||||
|
if (groupCallsEnabled) {
|
||||||
|
if (hasGroupCall) {
|
||||||
|
return "jitsi_or_element_call";
|
||||||
|
}
|
||||||
|
if (mayCreateElementCalls && hasJitsiWidget) {
|
||||||
|
return "jitsi_or_element_call";
|
||||||
|
}
|
||||||
|
if (useElementCallExclusively) {
|
||||||
|
return "element_call";
|
||||||
|
}
|
||||||
|
if (memberCount <= 2) {
|
||||||
|
return "legacy_or_jitsi";
|
||||||
|
}
|
||||||
|
if (mayCreateElementCalls) {
|
||||||
|
return "element_call";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "legacy_or_jitsi";
|
||||||
|
}, [
|
||||||
|
groupCallsEnabled,
|
||||||
|
hasGroupCall,
|
||||||
|
mayCreateElementCalls,
|
||||||
|
hasJitsiWidget,
|
||||||
|
useElementCallExclusively,
|
||||||
|
memberCount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let widget: IApp | undefined;
|
||||||
|
if (callType === "legacy_or_jitsi") {
|
||||||
|
widget = jitsiWidget ?? managedHybridWidget;
|
||||||
|
} else if (callType === "element_call") {
|
||||||
|
widget = groupCall?.widget;
|
||||||
|
} else {
|
||||||
|
widget = groupCall?.widget ?? jitsiWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [canPinWidget, setCanPinWidget] = useState(false);
|
||||||
|
const [widgetPinned, setWidgetPinned] = useState(false);
|
||||||
|
const promptPinWidget = canPinWidget && !widgetPinned;
|
||||||
|
|
||||||
|
const updateWidgetState = useCallback((): void => {
|
||||||
|
setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, Container.Top));
|
||||||
|
setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Top));
|
||||||
|
}, [room, widget]);
|
||||||
|
|
||||||
|
useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState);
|
||||||
|
useEffect(() => {
|
||||||
|
updateWidgetState();
|
||||||
|
}, [room, jitsiWidget, groupCall, updateWidgetState]);
|
||||||
|
|
||||||
|
const state = useMemo((): State => {
|
||||||
|
if (hasGroupCall || hasJitsiWidget || hasManagedHybridWidget) {
|
||||||
|
return promptPinWidget ? State.Unpinned : State.Ongoing;
|
||||||
|
}
|
||||||
|
if (hasLegacyCall) {
|
||||||
|
return State.Ongoing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberCount <= 1) {
|
||||||
|
return State.NoOneHere;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mayCreateElementCalls && !mayEditWidgets) {
|
||||||
|
return State.NoPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
return State.NoCall;
|
||||||
|
}, [
|
||||||
|
hasGroupCall,
|
||||||
|
hasJitsiWidget,
|
||||||
|
hasLegacyCall,
|
||||||
|
hasManagedHybridWidget,
|
||||||
|
mayCreateElementCalls,
|
||||||
|
mayEditWidgets,
|
||||||
|
memberCount,
|
||||||
|
promptPinWidget,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const voiceCallClick = useCallback(
|
||||||
|
(evt: React.MouseEvent): void => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
if (widget && promptPinWidget) {
|
||||||
|
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||||
|
} else {
|
||||||
|
placeCall(room, CallType.Voice, callType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[promptPinWidget, room, widget, callType],
|
||||||
|
);
|
||||||
|
const videoCallClick = useCallback(
|
||||||
|
(evt: React.MouseEvent): void => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
if (widget && promptPinWidget) {
|
||||||
|
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||||
|
} else {
|
||||||
|
placeCall(room, CallType.Video, callType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[widget, promptPinWidget, room, callType],
|
||||||
|
);
|
||||||
|
|
||||||
|
let voiceCallDisabledReason: string | null;
|
||||||
|
let videoCallDisabledReason: string | null;
|
||||||
|
switch (state) {
|
||||||
|
case State.NoPermission:
|
||||||
|
voiceCallDisabledReason = _t("You do not have permission to start voice calls");
|
||||||
|
videoCallDisabledReason = _t("You do not have permission to start video calls");
|
||||||
|
break;
|
||||||
|
case State.Ongoing:
|
||||||
|
voiceCallDisabledReason = _t("Ongoing call");
|
||||||
|
videoCallDisabledReason = _t("Ongoing call");
|
||||||
|
break;
|
||||||
|
case State.NoOneHere:
|
||||||
|
voiceCallDisabledReason = _t("There's no one here to call");
|
||||||
|
videoCallDisabledReason = _t("There's no one here to call");
|
||||||
|
break;
|
||||||
|
case State.Unpinned:
|
||||||
|
case State.NoCall:
|
||||||
|
voiceCallDisabledReason = null;
|
||||||
|
videoCallDisabledReason = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We've gone through all the steps
|
||||||
|
*/
|
||||||
|
return {
|
||||||
|
voiceCallDisabledReason,
|
||||||
|
voiceCallClick,
|
||||||
|
videoCallDisabledReason,
|
||||||
|
videoCallClick,
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,154 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2023 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 { Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import { useFeatureEnabled } from "../useSettings";
|
|
||||||
import SdkConfig from "../../SdkConfig";
|
|
||||||
import { useEventEmitterState, useTypedEventEmitterState } from "../useEventEmitter";
|
|
||||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
|
|
||||||
import { useWidgets } from "../../components/views/right_panel/RoomSummaryCard";
|
|
||||||
import { WidgetType } from "../../widgets/WidgetType";
|
|
||||||
import { useCall } from "../useCall";
|
|
||||||
import { _t } from "../../languageHandler";
|
|
||||||
import { useRoomMemberCount } from "../useRoomMembers";
|
|
||||||
import { ElementCall } from "../../models/Call";
|
|
||||||
|
|
||||||
export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi";
|
|
||||||
|
|
||||||
const DEFAULT_DISABLED_REASON = null;
|
|
||||||
const DEFAULT_CALL_TYPE = "jitsi_or_element_call";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reports the call capabilities for the current room
|
|
||||||
* @param room the room to track
|
|
||||||
* @returns the call status for a room
|
|
||||||
*/
|
|
||||||
export const useRoomCallStatus = (
|
|
||||||
room: Room,
|
|
||||||
): {
|
|
||||||
voiceCallDisabledReason: string | null;
|
|
||||||
voiceCallType: PlatformCallType;
|
|
||||||
videoCallDisabledReason: string | null;
|
|
||||||
videoCallType: PlatformCallType;
|
|
||||||
} => {
|
|
||||||
const [voiceCallDisabledReason, setVoiceCallDisabledReason] = useState<string | null>(DEFAULT_DISABLED_REASON);
|
|
||||||
const [videoCallDisabledReason, setVideoCallDisabledReason] = useState<string | null>(DEFAULT_DISABLED_REASON);
|
|
||||||
const [voiceCallType, setVoiceCallType] = useState<PlatformCallType>(DEFAULT_CALL_TYPE);
|
|
||||||
const [videoCallType, setVideoCallType] = useState<PlatformCallType>(DEFAULT_CALL_TYPE);
|
|
||||||
|
|
||||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
|
||||||
const useElementCallExclusively = useMemo(() => {
|
|
||||||
return SdkConfig.get("element_call").use_exclusively;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const hasLegacyCall = useEventEmitterState(
|
|
||||||
LegacyCallHandler.instance,
|
|
||||||
LegacyCallHandlerEvent.CallsChanged,
|
|
||||||
() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null,
|
|
||||||
);
|
|
||||||
|
|
||||||
const widgets = useWidgets(room);
|
|
||||||
const hasJitsiWidget = useMemo(() => widgets.some((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
|
|
||||||
|
|
||||||
const hasGroupCall = useCall(room.roomId) !== null;
|
|
||||||
|
|
||||||
const memberCount = useRoomMemberCount(room);
|
|
||||||
|
|
||||||
const [mayEditWidgets, mayCreateElementCalls] = useTypedEventEmitterState(
|
|
||||||
room,
|
|
||||||
RoomStateEvent.Update,
|
|
||||||
useCallback(
|
|
||||||
() => [
|
|
||||||
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
|
||||||
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
|
|
||||||
],
|
|
||||||
[room],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// First reset all state to their default value
|
|
||||||
setVoiceCallDisabledReason(DEFAULT_DISABLED_REASON);
|
|
||||||
setVideoCallDisabledReason(DEFAULT_DISABLED_REASON);
|
|
||||||
setVoiceCallType(DEFAULT_CALL_TYPE);
|
|
||||||
setVideoCallType(DEFAULT_CALL_TYPE);
|
|
||||||
|
|
||||||
// And then run the logic to figure out their correct state
|
|
||||||
if (groupCallsEnabled) {
|
|
||||||
if (useElementCallExclusively) {
|
|
||||||
if (hasGroupCall) {
|
|
||||||
setVideoCallDisabledReason(_t("Ongoing call"));
|
|
||||||
} else if (mayCreateElementCalls) {
|
|
||||||
setVideoCallType("element_call");
|
|
||||||
} else {
|
|
||||||
setVideoCallDisabledReason(_t("You do not have permission to start video calls"));
|
|
||||||
}
|
|
||||||
} else if (hasLegacyCall || hasJitsiWidget || hasGroupCall) {
|
|
||||||
setVoiceCallDisabledReason(_t("Ongoing call"));
|
|
||||||
setVideoCallDisabledReason(_t("Ongoing call"));
|
|
||||||
} else if (memberCount <= 1) {
|
|
||||||
setVoiceCallDisabledReason(_t("There's no one here to call"));
|
|
||||||
setVideoCallDisabledReason(_t("There's no one here to call"));
|
|
||||||
} else if (memberCount === 2) {
|
|
||||||
setVoiceCallType("legacy_or_jitsi");
|
|
||||||
setVideoCallType("legacy_or_jitsi");
|
|
||||||
} else if (mayEditWidgets) {
|
|
||||||
setVoiceCallType("legacy_or_jitsi");
|
|
||||||
setVideoCallType(mayCreateElementCalls ? "jitsi_or_element_call" : "legacy_or_jitsi");
|
|
||||||
} else {
|
|
||||||
setVoiceCallDisabledReason(_t("You do not have permission to start voice calls"));
|
|
||||||
if (mayCreateElementCalls) {
|
|
||||||
setVideoCallType("element_call");
|
|
||||||
} else {
|
|
||||||
setVideoCallDisabledReason(_t("You do not have permission to start video calls"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (hasLegacyCall || hasJitsiWidget) {
|
|
||||||
setVoiceCallDisabledReason(_t("Ongoing call"));
|
|
||||||
setVideoCallDisabledReason(_t("Ongoing call"));
|
|
||||||
} else if (memberCount <= 1) {
|
|
||||||
setVoiceCallDisabledReason(_t("There's no one here to call"));
|
|
||||||
setVideoCallDisabledReason(_t("There's no one here to call"));
|
|
||||||
} else if (memberCount === 2 || mayEditWidgets) {
|
|
||||||
setVoiceCallType("legacy_or_jitsi");
|
|
||||||
setVideoCallType("legacy_or_jitsi");
|
|
||||||
} else {
|
|
||||||
setVoiceCallDisabledReason(_t("You do not have permission to start voice calls"));
|
|
||||||
setVideoCallDisabledReason(_t("You do not have permission to start video calls"));
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
memberCount,
|
|
||||||
groupCallsEnabled,
|
|
||||||
hasGroupCall,
|
|
||||||
hasJitsiWidget,
|
|
||||||
hasLegacyCall,
|
|
||||||
mayCreateElementCalls,
|
|
||||||
mayEditWidgets,
|
|
||||||
useElementCallExclusively,
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We've gone through all the steps
|
|
||||||
*/
|
|
||||||
return {
|
|
||||||
voiceCallDisabledReason,
|
|
||||||
voiceCallType,
|
|
||||||
videoCallDisabledReason,
|
|
||||||
videoCallType,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Room, RoomState, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
import { Room, RoomState, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import { useTypedEventEmitter } from "./useEventEmitter";
|
import { useTypedEventEmitter } from "./useEventEmitter";
|
||||||
|
@ -28,19 +28,27 @@ export const useRoomState = <T extends any = RoomState>(
|
||||||
room?: Room,
|
room?: Room,
|
||||||
mapper: Mapper<T> = defaultMapper as Mapper<T>,
|
mapper: Mapper<T> = defaultMapper as Mapper<T>,
|
||||||
): T => {
|
): T => {
|
||||||
|
// Create a ref that stores mapper
|
||||||
|
const savedMapper = useRef(mapper);
|
||||||
|
|
||||||
|
// Update ref.current value if mapper changes.
|
||||||
|
useEffect(() => {
|
||||||
|
savedMapper.current = mapper;
|
||||||
|
}, [mapper]);
|
||||||
|
|
||||||
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : (undefined as T));
|
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : (undefined as T));
|
||||||
|
|
||||||
const update = useCallback(() => {
|
const update = useCallback(() => {
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
setValue(mapper(room.currentState));
|
setValue(savedMapper.current(room.currentState));
|
||||||
}, [room, mapper]);
|
}, [room]);
|
||||||
|
|
||||||
useTypedEventEmitter(room?.currentState, RoomStateEvent.Update, update);
|
useTypedEventEmitter(room?.currentState, RoomStateEvent.Update, update);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
update();
|
update();
|
||||||
return () => {
|
return () => {
|
||||||
setValue(room ? mapper(room.currentState) : (undefined as T));
|
setValue(room ? savedMapper.current(room.currentState) : (undefined as T));
|
||||||
};
|
};
|
||||||
}, [room, mapper, update]);
|
}, [room, update]);
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1413,10 +1413,10 @@
|
||||||
"explore_rooms": "Explore Public Rooms",
|
"explore_rooms": "Explore Public Rooms",
|
||||||
"create_room": "Create a Group Chat"
|
"create_room": "Create a Group Chat"
|
||||||
},
|
},
|
||||||
"Ongoing call": "Ongoing call",
|
|
||||||
"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",
|
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
|
||||||
|
"You do not have permission to start video calls": "You do not have permission to start video calls",
|
||||||
|
"Ongoing call": "Ongoing call",
|
||||||
|
"There's no one here to call": "There's no one here to call",
|
||||||
"chat_effects": {
|
"chat_effects": {
|
||||||
"confetti_description": "Sends the given message with confetti",
|
"confetti_description": "Sends the given message with confetti",
|
||||||
"confetti_message": "sends confetti",
|
"confetti_message": "sends confetti",
|
||||||
|
|
|
@ -29,10 +29,12 @@ import { UPDATE_EVENT } from "./AsyncStore";
|
||||||
interface IState {}
|
interface IState {}
|
||||||
|
|
||||||
export interface IApp extends IWidget {
|
export interface IApp extends IWidget {
|
||||||
roomId: string;
|
"roomId": string;
|
||||||
eventId?: string; // not present on virtual widgets
|
"eventId"?: string; // not present on virtual widgets
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
avatar_url?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
|
"avatar_url"?: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765
|
||||||
|
// Whether the widget was created from `widget_build_url` and thus is a call widget of some kind
|
||||||
|
"io.element.managed_hybrid"?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAppWidget(widget: IWidget | IApp): widget is IApp {
|
export function isAppWidget(widget: IWidget | IApp): widget is IApp {
|
||||||
|
|
|
@ -332,7 +332,7 @@ export default class WidgetUtils {
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
widgetId: string,
|
widgetId: string,
|
||||||
content: IWidget,
|
content: IWidget & Record<string, any>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const addingWidget = !!content.url;
|
const addingWidget = !!content.url;
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
import { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import LegacyCallHandler from "../../LegacyCallHandler";
|
import LegacyCallHandler from "../../LegacyCallHandler";
|
||||||
import { PlatformCallType } from "../../hooks/room/useRoomCallStatus";
|
import { PlatformCallType } from "../../hooks/room/useRoomCall";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { getCallBehaviourWellKnown } from "../utils/WellKnownUtils";
|
||||||
import WidgetUtils from "../utils/WidgetUtils";
|
import WidgetUtils from "../utils/WidgetUtils";
|
||||||
import { IStoredLayout, WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
import { IStoredLayout, WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||||
import WidgetEchoStore from "../stores/WidgetEchoStore";
|
import WidgetEchoStore from "../stores/WidgetEchoStore";
|
||||||
import WidgetStore from "../stores/WidgetStore";
|
import WidgetStore, { IApp } from "../stores/WidgetStore";
|
||||||
import SdkConfig from "../SdkConfig";
|
import SdkConfig from "../SdkConfig";
|
||||||
import DMRoomMap from "../utils/DMRoomMap";
|
import DMRoomMap from "../utils/DMRoomMap";
|
||||||
|
|
||||||
|
@ -97,7 +97,10 @@ export async function addManagedHybridWidget(roomId: string): Promise<void> {
|
||||||
|
|
||||||
// Add the widget
|
// Add the widget
|
||||||
try {
|
try {
|
||||||
await WidgetUtils.setRoomWidgetContent(cli, roomId, widgetId, widgetContent);
|
await WidgetUtils.setRoomWidgetContent(cli, roomId, widgetId, {
|
||||||
|
...widgetContent,
|
||||||
|
"io.element.managed_hybrid": true,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Unable to add managed hybrid widget in room ${roomId}`, e);
|
logger.error(`Unable to add managed hybrid widget in room ${roomId}`, e);
|
||||||
return;
|
return;
|
||||||
|
@ -116,3 +119,7 @@ export async function addManagedHybridWidget(roomId: string): Promise<void> {
|
||||||
WidgetLayoutStore.instance.setContainerHeight(room, layout.container, layout.height);
|
WidgetLayoutStore.instance.setContainerHeight(room, layout.container, layout.height);
|
||||||
WidgetLayoutStore.instance.copyLayoutToRoom(room);
|
WidgetLayoutStore.instance.copyLayoutToRoom(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isManagedHybridWidget(widget: IApp): boolean {
|
||||||
|
return !!widget["io.element.managed_hybrid"];
|
||||||
|
}
|
||||||
|
|
|
@ -15,12 +15,19 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { EventType, JoinRule, MatrixClient, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
import { EventType, JoinRule, MatrixClient, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { getAllByTitle, getByLabelText, getByText, getByTitle, render, screen, waitFor } from "@testing-library/react";
|
import {
|
||||||
|
fireEvent,
|
||||||
|
getAllByLabelText,
|
||||||
|
getByLabelText,
|
||||||
|
getByText,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitFor,
|
||||||
|
} from "@testing-library/react";
|
||||||
|
|
||||||
import { mkEvent, stubClient, withClientContextRenderOptions } from "../../../test-utils";
|
import { filterConsole, mkEvent, stubClient, withClientContextRenderOptions } from "../../../test-utils";
|
||||||
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
|
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
|
||||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
@ -33,10 +40,13 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||||
import { CallStore } from "../../../../src/stores/CallStore";
|
import { CallStore } from "../../../../src/stores/CallStore";
|
||||||
import { Call, ElementCall } from "../../../../src/models/Call";
|
import { Call, ElementCall } from "../../../../src/models/Call";
|
||||||
import * as ShieldUtils from "../../../../src/utils/ShieldUtils";
|
import * as ShieldUtils from "../../../../src/utils/ShieldUtils";
|
||||||
|
import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
|
|
||||||
jest.mock("../../../../src/utils/ShieldUtils");
|
jest.mock("../../../../src/utils/ShieldUtils");
|
||||||
|
|
||||||
describe("RoomHeader", () => {
|
describe("RoomHeader", () => {
|
||||||
|
filterConsole("[getType] Room !1:example.org does not have an m.room.create event");
|
||||||
|
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
|
||||||
const ROOM_ID = "!1:example.org";
|
const ROOM_ID = "!1:example.org";
|
||||||
|
@ -94,7 +104,7 @@ describe("RoomHeader", () => {
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
|
|
||||||
await userEvent.click(getByText(container, ROOM_ID));
|
fireEvent.click(getByText(container, ROOM_ID));
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -184,7 +194,7 @@ describe("RoomHeader", () => {
|
||||||
const facePile = getByLabelText(container, "4 members");
|
const facePile = getByLabelText(container, "4 members");
|
||||||
expect(facePile).toHaveTextContent("4");
|
expect(facePile).toHaveTextContent("4");
|
||||||
|
|
||||||
await userEvent.click(facePile);
|
fireEvent.click(facePile);
|
||||||
|
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
|
||||||
});
|
});
|
||||||
|
@ -195,7 +205,7 @@ describe("RoomHeader", () => {
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
|
|
||||||
await userEvent.click(getByTitle(container, "Threads"));
|
fireEvent.click(getByLabelText(container, "Threads"));
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -209,7 +219,7 @@ describe("RoomHeader", () => {
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
|
|
||||||
await userEvent.click(getByTitle(container, "Notifications"));
|
fireEvent.click(getByLabelText(container, "Notifications"));
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -220,7 +230,7 @@ describe("RoomHeader", () => {
|
||||||
<RoomHeader room={room} />,
|
<RoomHeader room={room} />,
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
for (const button of getAllByTitle(container, "There's no one here to call")) {
|
for (const button of getAllByLabelText(container, "There's no one here to call")) {
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -231,17 +241,17 @@ describe("RoomHeader", () => {
|
||||||
<RoomHeader room={room} />,
|
<RoomHeader room={room} />,
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
const voiceButton = getByTitle(container, "Voice call");
|
const voiceButton = getByLabelText(container, "Voice call");
|
||||||
const videoButton = getByTitle(container, "Video call");
|
const videoButton = getByLabelText(container, "Video call");
|
||||||
expect(voiceButton).not.toBeDisabled();
|
expect(voiceButton).not.toBeDisabled();
|
||||||
expect(videoButton).not.toBeDisabled();
|
expect(videoButton).not.toBeDisabled();
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
||||||
|
|
||||||
await userEvent.click(voiceButton);
|
fireEvent.click(voiceButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
await userEvent.click(videoButton);
|
fireEvent.click(videoButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -255,7 +265,7 @@ describe("RoomHeader", () => {
|
||||||
<RoomHeader room={room} />,
|
<RoomHeader room={room} />,
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
for (const button of getAllByTitle(container, "Ongoing call")) {
|
for (const button of getAllByLabelText(container, "Ongoing call")) {
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -268,8 +278,8 @@ describe("RoomHeader", () => {
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByTitle(container, "Voice call")).not.toBeDisabled();
|
expect(getByLabelText(container, "Voice call")).not.toBeDisabled();
|
||||||
expect(getByTitle(container, "Video call")).not.toBeDisabled();
|
expect(getByLabelText(container, "Video call")).not.toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disable calls in large rooms by default", () => {
|
it("disable calls in large rooms by default", () => {
|
||||||
|
@ -279,8 +289,12 @@ describe("RoomHeader", () => {
|
||||||
<RoomHeader room={room} />,
|
<RoomHeader room={room} />,
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
expect(getByTitle(container, "You do not have permission to start voice calls")).toBeDisabled();
|
expect(
|
||||||
expect(getByTitle(container, "You do not have permission to start video calls")).toBeDisabled();
|
getByLabelText(container, "You do not have permission to start voice calls", { selector: "button" }),
|
||||||
|
).toBeDisabled();
|
||||||
|
expect(
|
||||||
|
getByLabelText(container, "You do not have permission to start video calls", { selector: "button" }),
|
||||||
|
).toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -290,6 +304,7 @@ describe("RoomHeader", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders only the video call element", async () => {
|
it("renders only the video call element", async () => {
|
||||||
|
mockRoomMembers(room, 3);
|
||||||
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
||||||
// allow element calls
|
// allow element calls
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||||
|
@ -301,28 +316,48 @@ describe("RoomHeader", () => {
|
||||||
|
|
||||||
expect(screen.queryByTitle("Voice call")).toBeNull();
|
expect(screen.queryByTitle("Voice call")).toBeNull();
|
||||||
|
|
||||||
const videoCallButton = getByTitle(container, "Video call");
|
const videoCallButton = getByLabelText(container, "Video call");
|
||||||
expect(videoCallButton).not.toBeDisabled();
|
expect(videoCallButton).not.toBeDisabled();
|
||||||
|
|
||||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
||||||
|
|
||||||
await userEvent.click(getByTitle(container, "Video call"));
|
fireEvent.click(getByLabelText(container, "Video call"));
|
||||||
|
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can call if there's an ongoing call", () => {
|
it("can't call if there's an ongoing (pinned) call", () => {
|
||||||
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
||||||
// allow element calls
|
// allow element calls
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
||||||
|
|
||||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({} as Call);
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget: {} } as Call);
|
||||||
|
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<RoomHeader room={room} />,
|
<RoomHeader room={room} />,
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
expect(getByTitle(container, "Ongoing call")).toBeDisabled();
|
expect(getByLabelText(container, "Ongoing call")).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clicking on ongoing (unpinned) call re-pins it", () => {
|
||||||
|
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
||||||
|
// allow element calls
|
||||||
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||||
|
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
||||||
|
const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||||
|
|
||||||
|
const widget = {};
|
||||||
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget } as Call);
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
expect(getByLabelText(container, "Video call")).not.toBeDisabled();
|
||||||
|
fireEvent.click(getByLabelText(container, "Video call"));
|
||||||
|
expect(spy).toHaveBeenCalledWith(room, widget, Container.Top);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables calling if there's a jitsi call", () => {
|
it("disables calling if there's a jitsi call", () => {
|
||||||
|
@ -335,7 +370,7 @@ describe("RoomHeader", () => {
|
||||||
<RoomHeader room={room} />,
|
<RoomHeader room={room} />,
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
for (const button of getAllByTitle(container, "Ongoing call")) {
|
for (const button of getAllByLabelText(container, "Ongoing call")) {
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -346,7 +381,7 @@ describe("RoomHeader", () => {
|
||||||
<RoomHeader room={room} />,
|
<RoomHeader room={room} />,
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
for (const button of getAllByTitle(container, "There's no one here to call")) {
|
for (const button of getAllByLabelText(container, "There's no one here to call")) {
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -358,16 +393,16 @@ describe("RoomHeader", () => {
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
|
|
||||||
const voiceButton = getByTitle(container, "Voice call");
|
const voiceButton = getByLabelText(container, "Voice call");
|
||||||
const videoButton = getByTitle(container, "Video call");
|
const videoButton = getByLabelText(container, "Video call");
|
||||||
expect(voiceButton).not.toBeDisabled();
|
expect(voiceButton).not.toBeDisabled();
|
||||||
expect(videoButton).not.toBeDisabled();
|
expect(videoButton).not.toBeDisabled();
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
||||||
await userEvent.click(voiceButton);
|
fireEvent.click(voiceButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
await userEvent.click(videoButton);
|
fireEvent.click(videoButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -384,16 +419,16 @@ describe("RoomHeader", () => {
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
|
|
||||||
const voiceButton = getByTitle(container, "Voice call");
|
const voiceButton = getByLabelText(container, "Voice call");
|
||||||
const videoButton = getByTitle(container, "Video call");
|
const videoButton = getByLabelText(container, "Video call");
|
||||||
expect(voiceButton).not.toBeDisabled();
|
expect(voiceButton).not.toBeDisabled();
|
||||||
expect(videoButton).not.toBeDisabled();
|
expect(videoButton).not.toBeDisabled();
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
||||||
await userEvent.click(voiceButton);
|
fireEvent.click(voiceButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
await userEvent.click(videoButton);
|
fireEvent.click(videoButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -411,17 +446,13 @@ describe("RoomHeader", () => {
|
||||||
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
);
|
);
|
||||||
|
|
||||||
const voiceButton = getByTitle(container, "Voice call");
|
const voiceButton = getByLabelText(container, "Voice call");
|
||||||
const videoButton = getByTitle(container, "Video call");
|
const videoButton = getByLabelText(container, "Video call");
|
||||||
expect(voiceButton).not.toBeDisabled();
|
expect(voiceButton).not.toBeDisabled();
|
||||||
expect(videoButton).not.toBeDisabled();
|
expect(videoButton).not.toBeDisabled();
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
|
||||||
await userEvent.click(voiceButton);
|
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
|
||||||
|
|
||||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
||||||
await userEvent.click(videoButton);
|
fireEvent.click(videoButton);
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,28 +35,28 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = `
|
||||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
aria-label="There's no one here to call"
|
||||||
class="_icon-button_1segd_17"
|
class="_icon-button_1segd_17"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
disabled=""
|
disabled=""
|
||||||
style="--cpd-icon-button-size: 32px;"
|
style="--cpd-icon-button-size: 32px;"
|
||||||
title="There's no one here to call"
|
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
aria-label="There's no one here to call"
|
||||||
class="_icon-button_1segd_17"
|
class="_icon-button_1segd_17"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
disabled=""
|
disabled=""
|
||||||
style="--cpd-icon-button-size: 32px;"
|
style="--cpd-icon-button-size: 32px;"
|
||||||
title="There's no one here to call"
|
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
aria-label="Threads"
|
||||||
class="_icon-button_1segd_17"
|
class="_icon-button_1segd_17"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
style="--cpd-icon-button-size: 32px;"
|
style="--cpd-icon-button-size: 32px;"
|
||||||
title="Threads"
|
|
||||||
>
|
>
|
||||||
<div />
|
<div />
|
||||||
</button>
|
</button>
|
||||||
|
|
Loading…
Reference in a new issue