mirror of https://github.com/vector-im/riot-web
Rebuild hook around room header call management
And tweak behaviour around ongoing yet unpinned calls to add a shortcut to quickly pinpull/28788/head^2
parent
5f0501a18a
commit
5a2595a093
|
@ -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 ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.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 { useRoomName } from "../../../hooks/useRoomName";
|
||||
|
@ -36,13 +35,12 @@ import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMember
|
|||
import { _t } from "../../../languageHandler";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { Box } from "../../utils/Box";
|
||||
import { useRoomCallStatus } from "../../../hooks/room/useRoomCallStatus";
|
||||
import { useRoomCall } from "../../../hooks/room/useRoomCall";
|
||||
import { useRoomThreadNotifications } from "../../../hooks/room/useRoomThreadNotifications";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { placeCall } from "../../../utils/room/placeCall";
|
||||
import { useEncryptionStatus } from "../../../hooks/useEncryptionStatus";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import FacePile from "../elements/FacePile";
|
||||
|
@ -84,7 +82,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
const members = useRoomMembers(room, 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");
|
||||
/**
|
||||
|
@ -179,10 +177,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
<IconButton
|
||||
disabled={!!voiceCallDisabledReason}
|
||||
aria-label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
placeCall(room, CallType.Voice, voiceCallType);
|
||||
}}
|
||||
onClick={voiceCallClick}
|
||||
>
|
||||
<VoiceCallIcon />
|
||||
</IconButton>
|
||||
|
@ -192,10 +187,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
<IconButton
|
||||
disabled={!!videoCallDisabledReason}
|
||||
aria-label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
placeCall(room, CallType.Video, videoCallType);
|
||||
}}
|
||||
onClick={videoCallClick}
|
||||
>
|
||||
<VideoCallIcon />
|
||||
</IconButton>
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
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";
|
||||
|
||||
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 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 || mayCreateElementCalls) {
|
||||
// Looks like for Audio this was previously legacy_or_jitsi
|
||||
return "element_call";
|
||||
}
|
||||
if (mayEditWidgets) {
|
||||
return "jitsi_or_element_call";
|
||||
}
|
||||
}
|
||||
return "legacy_or_jitsi";
|
||||
}, [
|
||||
groupCallsEnabled,
|
||||
hasGroupCall,
|
||||
mayCreateElementCalls,
|
||||
hasJitsiWidget,
|
||||
useElementCallExclusively,
|
||||
mayEditWidgets,
|
||||
]);
|
||||
const widget = callType === "element_call" ? 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) {
|
||||
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,
|
||||
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 voice 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.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Room, RoomState, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useTypedEventEmitter } from "./useEventEmitter";
|
||||
|
@ -28,19 +28,27 @@ export const useRoomState = <T extends any = RoomState>(
|
|||
room?: Room,
|
||||
mapper: Mapper<T> = defaultMapper as Mapper<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 update = useCallback(() => {
|
||||
if (!room) return;
|
||||
setValue(mapper(room.currentState));
|
||||
}, [room, mapper]);
|
||||
setValue(savedMapper.current(room.currentState));
|
||||
}, [room]);
|
||||
|
||||
useTypedEventEmitter(room?.currentState, RoomStateEvent.Update, update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
|||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import LegacyCallHandler from "../../LegacyCallHandler";
|
||||
import { PlatformCallType } from "../../hooks/room/useRoomCallStatus";
|
||||
import { PlatformCallType } from "../../hooks/room/useRoomCall";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../dispatcher/actions";
|
||||
|
|
Loading…
Reference in New Issue