Add Element call related functionality to new room header (#12091)
* New room header - add chat button during call - close lobby button in lobby - join button if session exists - allow to toggle call <-> timeline during call with call button Compound style for join button in call notify toast. Signed-off-by: Timo K <toger5@hotmail.de> * dont show start call, join button in video rooms. Signed-off-by: Timo K <toger5@hotmail.de> * Make active call check based on participant count Not based on available call object Signed-off-by: Timo K <toger5@hotmail.de> * fix room header tests Signed-off-by: Timo K <toger5@hotmail.de> * fix room header tests Signed-off-by: Timo K <toger5@hotmail.de> * remove chat button test for displaying. Chat button display logic is now part of the RoomHeader. Signed-off-by: Timo K <toger5@hotmail.de> * remove duplicate notification Tread icon Signed-off-by: Timo K <toger5@hotmail.de> * remove obsolete jest snapshot Signed-off-by: Timo K <toger5@hotmail.de> * Update src/components/views/rooms/RoomHeader.tsx Co-authored-by: Robin <robin@robin.town> * update isECWidget logic Signed-off-by: Timo K <toger5@hotmail.de> * remove dead code Signed-off-by: Timo K <toger5@hotmail.de> * refactor call options Add menu to choose if there are multiple options Signed-off-by: Timo K <toger5@hotmail.de> * join ec when clicking join button (dont start jitsi) Use icon buttons don't show call icon when join button is visible Signed-off-by: Timo K <toger5@hotmail.de> * refactor isViewingCall Signed-off-by: Timo K <toger5@hotmail.de> * fix room header tests Signed-off-by: Timo K <toger5@hotmail.de> * fix header snapshot Signed-off-by: Timo K <toger5@hotmail.de> * sonar proposals Signed-off-by: Timo K <toger5@hotmail.de> * fix event shiftKey may be undefined Signed-off-by: Timo K <toger5@hotmail.de> * more lobby time before timeout only await sticky promise on becoming sticky. Signed-off-by: Timo K <toger5@hotmail.de> * don't allow starting new calls if there is an ongoing other call. Signed-off-by: Timo K <toger5@hotmail.de> * review Signed-off-by: Timo K <toger5@hotmail.de> * fix translation typo Signed-off-by: Timo K <toger5@hotmail.de> --------- Signed-off-by: Timo K <toger5@hotmail.de> Co-authored-by: Robin <robin@robin.town>pull/28788/head^2
parent
31449d6f80
commit
73b16239a5
|
@ -821,7 +821,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
private onActiveCalls = (): void => {
|
||||
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>(
|
||||
|
|
|
@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Body as BodyText, IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
|
||||
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
|
||||
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
|
||||
import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
|
||||
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg";
|
||||
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg";
|
||||
import { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg";
|
||||
|
@ -35,7 +36,7 @@ import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMember
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { Box } from "../../utils/Box";
|
||||
import { useRoomCall } from "../../../hooks/room/useRoomCall";
|
||||
import { getPlatformCallTypeLabel, useRoomCall } from "../../../hooks/room/useRoomCall";
|
||||
import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications";
|
||||
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
|
@ -51,6 +52,7 @@ import { Linkify, topicToHtml } from "../../../HtmlUtils";
|
|||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton";
|
||||
import { RoomKnocksBar } from "./RoomKnocksBar";
|
||||
import { isVideoRoom } from "../../../utils/video-rooms";
|
||||
import { notificationLevelToIndicator } from "../../../utils/notifications";
|
||||
|
||||
export default function RoomHeader({
|
||||
|
@ -69,7 +71,17 @@ export default function RoomHeader({
|
|||
const members = useRoomMembers(room, 2500);
|
||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
|
||||
|
||||
const { voiceCallDisabledReason, voiceCallClick, videoCallDisabledReason, videoCallClick } = useRoomCall(room);
|
||||
const {
|
||||
voiceCallDisabledReason,
|
||||
voiceCallClick,
|
||||
videoCallDisabledReason,
|
||||
videoCallClick,
|
||||
toggleCallMaximized: toggleCall,
|
||||
isViewingCall,
|
||||
isConnectedToCall,
|
||||
hasActiveCallSession,
|
||||
callOptions,
|
||||
} = useRoomCall(room);
|
||||
|
||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||
/**
|
||||
|
@ -104,6 +116,97 @@ export default function RoomHeader({
|
|||
|
||||
const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join");
|
||||
|
||||
const videoClick = useCallback((ev) => videoCallClick(ev, callOptions[0]), [callOptions, videoCallClick]);
|
||||
|
||||
const toggleCallButton = (
|
||||
<Tooltip label={isViewingCall ? _t("voip|minimise_call") : _t("voip|maximise_call")}>
|
||||
<IconButton onClick={toggleCall}>
|
||||
<VideoCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
const joinCallButton = (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={videoClick}
|
||||
Icon={VideoCallIcon}
|
||||
className="mx_RoomHeader_join_button"
|
||||
color="primary"
|
||||
>
|
||||
{_t("action|join")}
|
||||
</Button>
|
||||
);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const callIconWithTooltip = (
|
||||
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
|
||||
<VideoCallIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
const startVideoCallButton = (
|
||||
<>
|
||||
{/* Can be either a menu or just a button depending on the number of call options.*/}
|
||||
{callOptions.length > 1 ? (
|
||||
<Menu
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
title={_t("voip|video_call_using")}
|
||||
trigger={
|
||||
<IconButton
|
||||
disabled={!!videoCallDisabledReason}
|
||||
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
|
||||
>
|
||||
{callIconWithTooltip}
|
||||
</IconButton>
|
||||
}
|
||||
side="left"
|
||||
align="start"
|
||||
>
|
||||
{callOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option}
|
||||
label={getPlatformCallTypeLabel(option)}
|
||||
onClick={(ev) => videoCallClick(ev, option)}
|
||||
Icon={VideoCallIcon}
|
||||
onSelect={() => {} /* Dummy handler since we want the click event.*/}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
) : (
|
||||
<IconButton
|
||||
disabled={!!videoCallDisabledReason}
|
||||
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
|
||||
onClick={videoClick}
|
||||
>
|
||||
{callIconWithTooltip}
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const voiceCallButton = (
|
||||
<Tooltip label={voiceCallDisabledReason ?? _t("voip|voice_call")}>
|
||||
<IconButton
|
||||
disabled={!!voiceCallDisabledReason}
|
||||
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
|
||||
onClick={(ev) => voiceCallClick(ev, callOptions[0])}
|
||||
>
|
||||
<VoiceCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
const closeLobbyButton = (
|
||||
<Tooltip label={_t("voip|close_lobby")}>
|
||||
<IconButton onClick={toggleCall}>
|
||||
<CloseCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
let videoCallButton = startVideoCallButton;
|
||||
if (isConnectedToCall) {
|
||||
videoCallButton = toggleCallButton;
|
||||
} else if (isViewingCall) {
|
||||
videoCallButton = closeLobbyButton;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex as="header" align="center" gap="var(--cpd-space-3x)" className="mx_RoomHeader light-panel">
|
||||
|
@ -190,29 +293,17 @@ export default function RoomHeader({
|
|||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<Tooltip label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}>
|
||||
<IconButton
|
||||
disabled={!!videoCallDisabledReason}
|
||||
aria-label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}
|
||||
onClick={videoCallClick}
|
||||
>
|
||||
<VideoCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{!useElementCallExclusively && (
|
||||
<Tooltip label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}>
|
||||
<IconButton
|
||||
disabled={!!voiceCallDisabledReason}
|
||||
aria-label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}
|
||||
onClick={voiceCallClick}
|
||||
>
|
||||
<VoiceCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Renders nothing when room is not a video room */}
|
||||
<VideoRoomChatButton room={room} />
|
||||
{((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && <VideoRoomChatButton room={room} />}
|
||||
|
||||
{hasActiveCallSession && !isConnectedToCall ? (
|
||||
joinCallButton
|
||||
) : (
|
||||
<>
|
||||
{!isVideoRoom(room) && videoCallButton}
|
||||
{!useElementCallExclusively && !isVideoRoom(room) && voiceCallButton}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip label={_t("common|threads")}>
|
||||
<IconButton
|
||||
|
|
|
@ -19,7 +19,6 @@ import { Icon as ChatIcon } from "@vector-im/compound-design-tokens/icons/chat-s
|
|||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import { isVideoRoom as calcIsVideoRoom } from "../../../../utils/video-rooms";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { useEventEmitterState } from "../../../../hooks/useEventEmitter";
|
||||
import { NotificationStateEvents } from "../../../../stores/notifications/NotificationState";
|
||||
|
@ -31,25 +30,18 @@ import { ButtonEvent } from "../../elements/AccessibleButton";
|
|||
/**
|
||||
* Display a button to toggle timeline for video rooms
|
||||
* @param room
|
||||
* @returns for a video room: a button to toggle timeline in the right panel
|
||||
* otherwise null
|
||||
* @returns A button to toggle timeline in the right panel.
|
||||
*/
|
||||
export const VideoRoomChatButton: React.FC<{ room: Room }> = ({ room }) => {
|
||||
const sdkContext = useContext(SDKContext);
|
||||
|
||||
const isVideoRoom = calcIsVideoRoom(room);
|
||||
|
||||
const notificationState = isVideoRoom ? sdkContext.roomNotificationStateStore.getRoomState(room) : undefined;
|
||||
const notificationState = sdkContext.roomNotificationStateStore.getRoomState(room);
|
||||
const notificationColor = useEventEmitterState(
|
||||
notificationState,
|
||||
NotificationStateEvents.Update,
|
||||
() => notificationState?.level,
|
||||
);
|
||||
|
||||
if (!isVideoRoom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayUnreadIndicator =
|
||||
!!notificationColor &&
|
||||
[NotificationLevel.Activity, NotificationLevel.Notification, NotificationLevel.Highlight].includes(
|
||||
|
|
|
@ -24,18 +24,37 @@ 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 { useCall, useConnectionState, useParticipantCount } from "../useCall";
|
||||
import { useRoomMemberCount } from "../useRoomMembers";
|
||||
import { ElementCall } from "../../models/Call";
|
||||
import { Call, ConnectionState, 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";
|
||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
||||
|
||||
export type PlatformCallType = "element_call" | "jitsi_or_element_call" | "legacy_or_jitsi";
|
||||
|
||||
export enum PlatformCallType {
|
||||
ElementCall,
|
||||
JitsiCall,
|
||||
LegacyCall,
|
||||
}
|
||||
export const getPlatformCallTypeLabel = (platformCallType: PlatformCallType): string => {
|
||||
switch (platformCallType) {
|
||||
case PlatformCallType.ElementCall:
|
||||
return _t("voip|element_call");
|
||||
case PlatformCallType.JitsiCall:
|
||||
return _t("voip|jitsi_call");
|
||||
case PlatformCallType.LegacyCall:
|
||||
return _t("voip|legacy_call");
|
||||
}
|
||||
};
|
||||
const enum State {
|
||||
NoCall,
|
||||
NoOneHere,
|
||||
|
@ -53,9 +72,14 @@ export const useRoomCall = (
|
|||
room: Room,
|
||||
): {
|
||||
voiceCallDisabledReason: string | null;
|
||||
voiceCallClick(evt: React.MouseEvent): void;
|
||||
voiceCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void;
|
||||
videoCallDisabledReason: string | null;
|
||||
videoCallClick(evt: React.MouseEvent): void;
|
||||
videoCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void;
|
||||
toggleCallMaximized: () => void;
|
||||
isViewingCall: boolean;
|
||||
isConnectedToCall: boolean;
|
||||
hasActiveCallSession: boolean;
|
||||
callOptions: PlatformCallType[];
|
||||
} => {
|
||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||
const useElementCallExclusively = useMemo(() => {
|
||||
|
@ -75,69 +99,83 @@ export const useRoomCall = (
|
|||
const hasManagedHybridWidget = !!managedHybridWidget;
|
||||
|
||||
const groupCall = useCall(room.roomId);
|
||||
const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected;
|
||||
const hasGroupCall = groupCall !== null;
|
||||
const hasActiveCallSession = useParticipantCount(groupCall) > 0;
|
||||
const isViewingCall = useEventEmitterState(SdkContextClass.instance.roomViewStore, UPDATE_EVENT, () =>
|
||||
SdkContextClass.instance.roomViewStore.isViewingCall(),
|
||||
);
|
||||
|
||||
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),
|
||||
room.currentState.mayClientSendStateEvent(ElementCall.MEMBER_EVENT_TYPE.name, room.client),
|
||||
]);
|
||||
|
||||
const callType = useMemo((): PlatformCallType => {
|
||||
// The options provided to the RoomHeader.
|
||||
// If there are multiple options, the user will be prompted to choose.
|
||||
const callOptions = useMemo((): PlatformCallType[] => {
|
||||
const options = [];
|
||||
if (memberCount <= 2) {
|
||||
options.push(PlatformCallType.LegacyCall);
|
||||
} else if (mayEditWidgets || hasJitsiWidget) {
|
||||
options.push(PlatformCallType.JitsiCall);
|
||||
}
|
||||
if (groupCallsEnabled) {
|
||||
if (hasGroupCall) {
|
||||
return "jitsi_or_element_call";
|
||||
if (hasGroupCall || mayCreateElementCalls) {
|
||||
options.push(PlatformCallType.ElementCall);
|
||||
}
|
||||
if (mayCreateElementCalls && hasJitsiWidget) {
|
||||
return "jitsi_or_element_call";
|
||||
if (useElementCallExclusively && !hasJitsiWidget) {
|
||||
return [PlatformCallType.ElementCall];
|
||||
}
|
||||
if (useElementCallExclusively) {
|
||||
return "element_call";
|
||||
}
|
||||
if (memberCount <= 2) {
|
||||
return "legacy_or_jitsi";
|
||||
}
|
||||
if (mayCreateElementCalls) {
|
||||
return "element_call";
|
||||
if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
|
||||
// only allow joining joining the ongoing Element call if there is one.
|
||||
return [PlatformCallType.ElementCall];
|
||||
}
|
||||
}
|
||||
return "legacy_or_jitsi";
|
||||
return options;
|
||||
}, [
|
||||
memberCount,
|
||||
mayEditWidgets,
|
||||
hasJitsiWidget,
|
||||
groupCallsEnabled,
|
||||
hasGroupCall,
|
||||
mayCreateElementCalls,
|
||||
hasJitsiWidget,
|
||||
useElementCallExclusively,
|
||||
memberCount,
|
||||
groupCall?.widget.type,
|
||||
]);
|
||||
|
||||
let widget: IApp | undefined;
|
||||
if (callType === "legacy_or_jitsi") {
|
||||
if (callOptions.includes(PlatformCallType.JitsiCall) || callOptions.includes(PlatformCallType.LegacyCall)) {
|
||||
widget = jitsiWidget ?? managedHybridWidget;
|
||||
} else if (callType === "element_call") {
|
||||
}
|
||||
if (callOptions.includes(PlatformCallType.ElementCall)) {
|
||||
widget = groupCall?.widget;
|
||||
} else {
|
||||
widget = groupCall?.widget ?? jitsiWidget;
|
||||
}
|
||||
|
||||
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 [activeCalls, setActiveCalls] = useState<Call[]>(Array.from(CallStore.instance.activeCalls));
|
||||
useEventEmitter(CallStore.instance, CallStoreEvent.ActiveCalls, () => {
|
||||
setActiveCalls(Array.from(CallStore.instance.activeCalls));
|
||||
});
|
||||
const [canPinWidget, setCanPinWidget] = useState(false);
|
||||
const [widgetPinned, setWidgetPinned] = useState(false);
|
||||
// We only want to prompt to pin the widget if it's not element call based.
|
||||
const isECWidget = WidgetType.CALL.matches(widget?.type ?? "");
|
||||
const promptPinWidget = !isECWidget && 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 (activeCalls.find((call) => call.roomId != room.roomId)) {
|
||||
return State.Ongoing;
|
||||
}
|
||||
if (hasGroupCall || hasJitsiWidget || hasManagedHybridWidget) {
|
||||
return promptPinWidget ? State.Unpinned : State.Ongoing;
|
||||
}
|
||||
|
@ -152,9 +190,9 @@ export const useRoomCall = (
|
|||
if (!mayCreateElementCalls && !mayEditWidgets) {
|
||||
return State.NoPermission;
|
||||
}
|
||||
|
||||
return State.NoCall;
|
||||
}, [
|
||||
activeCalls,
|
||||
hasGroupCall,
|
||||
hasJitsiWidget,
|
||||
hasLegacyCall,
|
||||
|
@ -163,29 +201,30 @@ export const useRoomCall = (
|
|||
mayEditWidgets,
|
||||
memberCount,
|
||||
promptPinWidget,
|
||||
room.roomId,
|
||||
]);
|
||||
|
||||
const voiceCallClick = useCallback(
|
||||
(evt: React.MouseEvent): void => {
|
||||
evt.stopPropagation();
|
||||
(evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
|
||||
evt?.stopPropagation();
|
||||
if (widget && promptPinWidget) {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||
} else {
|
||||
placeCall(room, CallType.Voice, callType, evt.shiftKey);
|
||||
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey ?? false);
|
||||
}
|
||||
},
|
||||
[promptPinWidget, room, widget, callType],
|
||||
[promptPinWidget, room, widget],
|
||||
);
|
||||
const videoCallClick = useCallback(
|
||||
(evt: React.MouseEvent): void => {
|
||||
evt.stopPropagation();
|
||||
(evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => {
|
||||
evt?.stopPropagation();
|
||||
if (widget && promptPinWidget) {
|
||||
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
|
||||
} else {
|
||||
placeCall(room, CallType.Video, callType, evt.shiftKey);
|
||||
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey ?? false);
|
||||
}
|
||||
},
|
||||
[widget, promptPinWidget, room, callType],
|
||||
[widget, promptPinWidget, room],
|
||||
);
|
||||
|
||||
let voiceCallDisabledReason: string | null;
|
||||
|
@ -208,6 +247,14 @@ export const useRoomCall = (
|
|||
voiceCallDisabledReason = null;
|
||||
videoCallDisabledReason = null;
|
||||
}
|
||||
const toggleCallMaximized = useCallback(() => {
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: undefined,
|
||||
view_call: !isViewingCall,
|
||||
});
|
||||
}, [isViewingCall, room.roomId]);
|
||||
|
||||
/**
|
||||
* We've gone through all the steps
|
||||
|
@ -217,5 +264,10 @@ export const useRoomCall = (
|
|||
voiceCallClick,
|
||||
videoCallDisabledReason,
|
||||
videoCallClick,
|
||||
toggleCallMaximized: toggleCallMaximized,
|
||||
isViewingCall: isViewingCall,
|
||||
isConnectedToCall: isConnectedToCall,
|
||||
hasActiveCallSession: hasActiveCallSession,
|
||||
callOptions,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -36,21 +36,22 @@ export const useCallForWidget = (widgetId: string, roomId: string): Call | null
|
|||
return call?.widget.id === widgetId ? call : null;
|
||||
};
|
||||
|
||||
export const useConnectionState = (call: Call): ConnectionState =>
|
||||
export const useConnectionState = (call: Call | null): ConnectionState =>
|
||||
useTypedEventEmitterState(
|
||||
call,
|
||||
call ?? undefined,
|
||||
CallEvent.ConnectionState,
|
||||
useCallback((state) => state ?? call.connectionState, [call]),
|
||||
useCallback((state) => state ?? call?.connectionState ?? ConnectionState.Disconnected, [call]),
|
||||
);
|
||||
|
||||
export const useParticipants = (call: Call): Map<RoomMember, Set<string>> =>
|
||||
useTypedEventEmitterState(
|
||||
call,
|
||||
export const useParticipants = (call: Call | null): Map<RoomMember, Set<string>> => {
|
||||
return useTypedEventEmitterState(
|
||||
call ?? undefined,
|
||||
CallEvent.Participants,
|
||||
useCallback((state) => state ?? call.participants, [call]),
|
||||
useCallback((state) => state ?? call?.participants ?? [], [call]),
|
||||
);
|
||||
};
|
||||
|
||||
export const useParticipantCount = (call: Call): number => {
|
||||
export const useParticipantCount = (call: Call | null): number => {
|
||||
const participants = useParticipants(call);
|
||||
|
||||
return useMemo(() => {
|
||||
|
|
|
@ -3807,6 +3807,7 @@
|
|||
"camera_enabled": "Your camera is still enabled",
|
||||
"cannot_call_yourself_description": "You cannot place a call with yourself.",
|
||||
"change_input_device": "Change input device",
|
||||
"close_lobby": "Close lobby",
|
||||
"connecting": "Connecting",
|
||||
"connection_lost": "Connectivity to the server has been lost",
|
||||
"connection_lost_description": "You cannot place calls without a connection to the server.",
|
||||
|
@ -3820,6 +3821,7 @@
|
|||
"disabled_no_perms_start_video_call": "You do not have permission to start video calls",
|
||||
"disabled_no_perms_start_voice_call": "You do not have permission to start voice calls",
|
||||
"disabled_ongoing_call": "Ongoing call",
|
||||
"element_call": "Element Call",
|
||||
"enable_camera": "Turn on camera",
|
||||
"enable_microphone": "Unmute microphone",
|
||||
"expand": "Return to call",
|
||||
|
@ -3828,9 +3830,13 @@
|
|||
"hangup": "Hangup",
|
||||
"hide_sidebar_button": "Hide sidebar",
|
||||
"input_devices": "Input devices",
|
||||
"jitsi_call": "Jitsi Conference",
|
||||
"join_button_tooltip_call_full": "Sorry — this call is currently full",
|
||||
"join_button_tooltip_connecting": "Connecting",
|
||||
"legacy_call": "Legacy Call",
|
||||
"maximise": "Fill screen",
|
||||
"maximise_call": "Maximise call",
|
||||
"minimise_call": "Minimise call",
|
||||
"misconfigured_server": "Call failed due to misconfigured server",
|
||||
"misconfigured_server_description": "Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.",
|
||||
"misconfigured_server_fallback": "Alternatively, you can try to use the public server at <server/>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.",
|
||||
|
@ -3878,6 +3884,7 @@
|
|||
"user_is_presenting": "%(sharerName)s is presenting",
|
||||
"video_call": "Video call",
|
||||
"video_call_started": "Video call started",
|
||||
"video_call_using": "Video call using:",
|
||||
"voice_call": "Voice call",
|
||||
"you_are_presenting": "You are presenting"
|
||||
},
|
||||
|
|
|
@ -65,18 +65,26 @@ const waitForEvent = async (
|
|||
emitter: EventEmitter,
|
||||
event: string,
|
||||
pred: (...args: any[]) => boolean = () => true,
|
||||
customTimeout?: number | false,
|
||||
): Promise<void> => {
|
||||
let listener: (...args: any[]) => void;
|
||||
const wait = new Promise<void>((resolve) => {
|
||||
listener = (...args) => {
|
||||
if (pred(...args)) resolve();
|
||||
if (pred(...args)) {
|
||||
resolve();
|
||||
if (customTimeout === false) {
|
||||
emitter.off(event, listener!);
|
||||
}
|
||||
}
|
||||
};
|
||||
emitter.on(event, listener);
|
||||
});
|
||||
|
||||
const timedOut = (await timeout(wait, false, TIMEOUT_MS)) === false;
|
||||
emitter.off(event, listener!);
|
||||
if (timedOut) throw new Error("Timed out");
|
||||
if (customTimeout !== false) {
|
||||
const timedOut = (await timeout(wait, false, customTimeout ?? TIMEOUT_MS)) === false;
|
||||
emitter.off(event, listener!);
|
||||
if (timedOut) throw new Error("Timed out");
|
||||
}
|
||||
};
|
||||
|
||||
export enum ConnectionState {
|
||||
|
@ -899,6 +907,7 @@ export class ElementCall extends Call {
|
|||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
(_, newMemberships: CallMembership[]) =>
|
||||
newMemberships.some((m) => m.sender === this.client.getUserId()),
|
||||
false, // allow user to wait as long as they want (no timeout)
|
||||
);
|
||||
} else {
|
||||
await waitForEvent(
|
||||
|
@ -906,6 +915,7 @@ export class ElementCall extends Call {
|
|||
MatrixRTCSessionManagerEvents.SessionStarted,
|
||||
(roomId: string, session: MatrixRTCSession) =>
|
||||
this.session.callId === session.callId && roomId === this.roomId,
|
||||
false, // allow user to wait as long as they want (no timeout)
|
||||
);
|
||||
}
|
||||
this.sendCallNotify();
|
||||
|
|
|
@ -345,8 +345,11 @@ export class StopGapWidget extends EventEmitter {
|
|||
if (this.messaging?.hasCapability(MatrixCapabilities.AlwaysOnScreen)) {
|
||||
ev.preventDefault();
|
||||
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
|
||||
|
||||
if (this.stickyPromise) await this.stickyPromise();
|
||||
if (ev.detail.data.value) {
|
||||
// If the widget wants to become sticky we wait for the stickyPromise to resolve
|
||||
if (this.stickyPromise) await this.stickyPromise();
|
||||
}
|
||||
// Stop being persistent can be done instantly
|
||||
ActiveWidgetStore.instance.setWidgetPersistence(
|
||||
this.mockWidget.id,
|
||||
this.roomId ?? null,
|
||||
|
|
|
@ -20,6 +20,8 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|||
import { MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSessionManager";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
|
||||
|
||||
import { _t } from "../languageHandler";
|
||||
import RoomAvatar from "../components/views/avatars/RoomAvatar";
|
||||
|
@ -28,7 +30,6 @@ import defaultDispatcher from "../dispatcher/dispatcher";
|
|||
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton";
|
||||
import {
|
||||
LiveContentSummary,
|
||||
LiveContentSummaryWithCall,
|
||||
|
@ -41,6 +42,7 @@ import { ActionPayload } from "../dispatcher/payloads";
|
|||
import { Call } from "../models/Call";
|
||||
import { AudioID } from "../LegacyCallHandler";
|
||||
import { useTypedEventEmitter } from "../hooks/useEventEmitter";
|
||||
import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton";
|
||||
|
||||
export const getIncomingCallToastKey = (callId: string, roomId: string): string => `call_${callId}_${roomId}`;
|
||||
const MAX_RING_TIME_MS = 10 * 1000;
|
||||
|
@ -54,15 +56,15 @@ function JoinCallButtonWithCall({ onClick, call }: JoinCallButtonWithCallProps):
|
|||
const disabledTooltip = useJoinCallButtonDisabledTooltip(call);
|
||||
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
<Button
|
||||
className="mx_IncomingCallToast_joinButton"
|
||||
onClick={onClick}
|
||||
disabled={disabledTooltip !== null}
|
||||
tooltip={disabledTooltip ?? undefined}
|
||||
kind="primary"
|
||||
size="sm"
|
||||
>
|
||||
{_t("action|join")}
|
||||
</AccessibleTooltipButton>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -179,13 +181,15 @@ export function IncomingCallToast({ notifyEvent }: Props): JSX.Element {
|
|||
{call ? (
|
||||
<JoinCallButtonWithCall onClick={onJoinClick} call={call} />
|
||||
) : (
|
||||
<AccessibleTooltipButton
|
||||
<Button
|
||||
className="mx_IncomingCallToast_joinButton"
|
||||
onClick={onJoinClick}
|
||||
kind="primary"
|
||||
size="sm"
|
||||
Icon={VideoCallIcon}
|
||||
>
|
||||
{_t("action|join")}
|
||||
</AccessibleTooltipButton>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<AccessibleTooltipButton
|
||||
|
|
|
@ -35,27 +35,15 @@ export const placeCall = async (
|
|||
platformCallType: PlatformCallType,
|
||||
skipLobby: boolean,
|
||||
): Promise<void> => {
|
||||
switch (platformCallType) {
|
||||
case "legacy_or_jitsi":
|
||||
await LegacyCallHandler.instance.placeCall(room.roomId, callType);
|
||||
break;
|
||||
// TODO: Remove the jitsi_or_element_call case and
|
||||
// use the commented code below
|
||||
case "element_call":
|
||||
case "jitsi_or_element_call":
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
view_call: true,
|
||||
metricsTrigger: undefined,
|
||||
skipLobby,
|
||||
});
|
||||
break;
|
||||
|
||||
// case "jitsi_or_element_call":
|
||||
// TODO: Open dropdown menu to choice between
|
||||
// EC and Jitsi. Waiting on Compound's dropdown
|
||||
// component
|
||||
// break;
|
||||
if (platformCallType == PlatformCallType.LegacyCall || platformCallType == PlatformCallType.JitsiCall) {
|
||||
await LegacyCallHandler.instance.placeCall(room.roomId, callType);
|
||||
} else if (platformCallType == PlatformCallType.ElementCall) {
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
view_call: true,
|
||||
skipLobby,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -323,7 +323,7 @@ describe("RoomHeader", () => {
|
|||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(true);
|
||||
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget: {} } as Call);
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget: {}, on: () => {} } as unknown as Call);
|
||||
|
||||
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||
expect(getByLabelText(container, "Ongoing call")).toHaveAttribute("aria-disabled", "true");
|
||||
|
@ -336,8 +336,11 @@ describe("RoomHeader", () => {
|
|||
jest.spyOn(WidgetLayoutStore.instance, "isInContainer").mockReturnValue(false);
|
||||
const spy = jest.spyOn(WidgetLayoutStore.instance, "moveToContainer");
|
||||
|
||||
const widget = { eventId: "some_id_so_it_is_interpreted_as_non_virtual_widget" };
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({ widget } as Call);
|
||||
const widget = {};
|
||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({
|
||||
widget,
|
||||
on: () => {},
|
||||
} as unknown as Call);
|
||||
|
||||
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||
expect(getByLabelText(container, "Video call")).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
@ -367,6 +370,10 @@ describe("RoomHeader", () => {
|
|||
|
||||
it("calls using legacy or jitsi", async () => {
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === "im.vector.modular.widgets") return true;
|
||||
return false;
|
||||
});
|
||||
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
const voiceButton = getByLabelText(container, "Voice call");
|
||||
|
@ -409,8 +416,7 @@ describe("RoomHeader", () => {
|
|||
mockRoomMembers(room, 3);
|
||||
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === "im.vector.modular.widgets") return true;
|
||||
if (key === ElementCall.CALL_EVENT_TYPE.name) return true;
|
||||
if (key === ElementCall.MEMBER_EVENT_TYPE.name) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
|
|
|
@ -80,20 +80,6 @@ describe("<VideoRoomChatButton />", () => {
|
|||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("does not render button when room is not a video room", () => {
|
||||
const room = makeRoom(false);
|
||||
getComponent(room);
|
||||
|
||||
expect(screen.queryByLabelText("Chat")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders button when room is a video room", () => {
|
||||
const room = makeRoom();
|
||||
getComponent(room);
|
||||
|
||||
expect(screen.getByLabelText("Chat")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("toggles timeline in right panel on click", () => {
|
||||
const room = makeRoom();
|
||||
getComponent(room);
|
||||
|
|
|
@ -1,23 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<VideoRoomChatButton /> renders button when room is a video room 1`] = `
|
||||
<button
|
||||
aria-label="Chat"
|
||||
class="_icon-button_16nk7_17"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_jtb4d_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`<VideoRoomChatButton /> renders button with an unread marker when room is unread 1`] = `
|
||||
<button
|
||||
aria-label="Chat"
|
||||
|
|
|
@ -47,7 +47,6 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = `
|
|||
aria-disabled="true"
|
||||
aria-label="There's no one here to call"
|
||||
class="_icon-button_16nk7_17"
|
||||
data-state="closed"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
tabindex="0"
|
||||
|
@ -56,7 +55,9 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = `
|
|||
class="_indicator-icon_jtb4d_26"
|
||||
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
||||
>
|
||||
<div />
|
||||
<div
|
||||
data-state="closed"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
|
|
Loading…
Reference in New Issue