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>
This commit is contained in:
Timo 2024-01-31 16:18:52 +01:00 committed by GitHub
parent 31449d6f80
commit 73b16239a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 286 additions and 164 deletions

View file

@ -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,
};
};

View file

@ -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(() => {