Add Voice and Video call in room header (#11444)
* Add Voice and Video call in room header * Add thread icon in room header * Add room notification icon to room header * Fix linting * Add tests for buttons in room header * Add JSDoc * micro optimisations * Fix call disabled when hanging up * Fix disabled state change on members count update * Exclude functional members from members count optionally * i18n
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
type CallType = "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: CallType;
|
||||
videoCallDisabledReason: string | null;
|
||||
videoCallType: CallType;
|
||||
} => {
|
||||
const [voiceCallDisabledReason, setVoiceCallDisabledReason] = useState<string | null>(DEFAULT_DISABLED_REASON);
|
||||
const [videoCallDisabledReason, setVideoCallDisabledReason] = useState<string | null>(DEFAULT_DISABLED_REASON);
|
||||
const [voiceCallType, setVoiceCallType] = useState<CallType>(DEFAULT_CALL_TYPE);
|
||||
const [videoCallType, setVideoCallType] = useState<CallType>(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, { includeFunctional: false });
|
||||
|
||||
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,
|
||||
]);
|
||||
|
||||
console.table({
|
||||
voiceCallDisabledReason,
|
||||
voiceCallType,
|
||||
videoCallDisabledReason,
|
||||
videoCallType,
|
||||
});
|
||||
|
||||
/**
|
||||
* We've gone through all the steps
|
||||
*/
|
||||
return {
|
||||
voiceCallDisabledReason,
|
||||
voiceCallType,
|
||||
videoCallDisabledReason,
|
||||
videoCallType,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
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 { NotificationCountType, Room, RoomEvent, ThreadEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { NotificationColor } from "../../stores/notifications/NotificationColor";
|
||||
import { doesRoomOrThreadHaveUnreadMessages } from "../../Unread";
|
||||
import { useEventEmitter } from "../useEventEmitter";
|
||||
|
||||
/**
|
||||
* Tracks the thread unread state for an entire room
|
||||
* @param room the room to track
|
||||
* @returns the type of notification for this room
|
||||
*/
|
||||
export const useRoomThreadNotifications = (room: Room): NotificationColor => {
|
||||
const [notificationColor, setNotificationColor] = useState(NotificationColor.None);
|
||||
|
||||
const updateNotification = useCallback(() => {
|
||||
switch (room?.threadsAggregateNotificationType) {
|
||||
case NotificationCountType.Highlight:
|
||||
setNotificationColor(NotificationColor.Red);
|
||||
break;
|
||||
case NotificationCountType.Total:
|
||||
setNotificationColor(NotificationColor.Grey);
|
||||
break;
|
||||
}
|
||||
// We don't have any notified messages, but we might have unread messages. Let's
|
||||
// find out.
|
||||
for (const thread of room!.getThreads()) {
|
||||
// If the current thread has unread messages, we're done.
|
||||
if (doesRoomOrThreadHaveUnreadMessages(thread)) {
|
||||
setNotificationColor(NotificationColor.Bold);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(room, RoomEvent.UnreadNotifications, updateNotification);
|
||||
useEventEmitter(room, RoomEvent.Receipt, updateNotification);
|
||||
useEventEmitter(room, RoomEvent.Timeline, updateNotification);
|
||||
useEventEmitter(room, RoomEvent.Redaction, updateNotification);
|
||||
useEventEmitter(room, RoomEvent.LocalEchoUpdated, updateNotification);
|
||||
useEventEmitter(room, RoomEvent.MyMembership, updateNotification);
|
||||
useEventEmitter(room, ThreadEvent.New, updateNotification);
|
||||
useEventEmitter(room, ThreadEvent.Update, updateNotification);
|
||||
|
||||
// Compute the notification once when mouting a room
|
||||
useEffect(() => {
|
||||
updateNotification();
|
||||
}, [updateNotification]);
|
||||
|
||||
return notificationColor;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
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 { useState } from "react";
|
||||
|
||||
import { SummarizedNotificationState } from "../stores/notifications/SummarizedNotificationState";
|
||||
import {
|
||||
RoomNotificationStateStore,
|
||||
UPDATE_STATUS_INDICATOR,
|
||||
} from "../stores/notifications/RoomNotificationStateStore";
|
||||
import { useEventEmitter } from "./useEventEmitter";
|
||||
|
||||
/**
|
||||
* Tracks the global notification state of the user's account
|
||||
* @returns A global notification state object
|
||||
*/
|
||||
export const useGlobalNotificationState = (): SummarizedNotificationState => {
|
||||
const [summarizedNotificationState, setSummarizedNotificationState] = useState(
|
||||
RoomNotificationStateStore.instance.globalState,
|
||||
);
|
||||
|
||||
useEventEmitter(
|
||||
RoomNotificationStateStore.instance,
|
||||
UPDATE_STATUS_INDICATOR,
|
||||
(notificationState: SummarizedNotificationState) => {
|
||||
setSummarizedNotificationState(notificationState);
|
||||
},
|
||||
);
|
||||
|
||||
return summarizedNotificationState;
|
||||
};
|
||||
@@ -19,6 +19,7 @@ import { Room, RoomEvent, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/m
|
||||
import { throttle } from "lodash";
|
||||
|
||||
import { useTypedEventEmitter } from "./useEventEmitter";
|
||||
import { getJoinedNonFunctionalMembers } from "../utils/room/getJoinedNonFunctionalMembers";
|
||||
|
||||
// Hook to simplify watching Matrix Room joined members
|
||||
export const useRoomMembers = (room: Room, throttleWait = 250): RoomMember[] => {
|
||||
@@ -37,15 +38,43 @@ export const useRoomMembers = (room: Room, throttleWait = 250): RoomMember[] =>
|
||||
return members;
|
||||
};
|
||||
|
||||
type RoomMemberCountOpts = {
|
||||
/**
|
||||
* Wait time between room member count update
|
||||
*/
|
||||
throttleWait?: number;
|
||||
/**
|
||||
* Whether to include functional members (bots, etc...) in the room count
|
||||
* @default true
|
||||
*/
|
||||
includeFunctional: boolean;
|
||||
};
|
||||
|
||||
// Hook to simplify watching Matrix Room joined member count
|
||||
export const useRoomMemberCount = (room: Room, throttleWait = 250): number => {
|
||||
export const useRoomMemberCount = (
|
||||
room: Room,
|
||||
opts: RoomMemberCountOpts = { throttleWait: 250, includeFunctional: true },
|
||||
): number => {
|
||||
const [count, setCount] = useState<number>(room.getJoinedMemberCount());
|
||||
|
||||
const { throttleWait, includeFunctional } = opts;
|
||||
|
||||
useTypedEventEmitter(
|
||||
room.currentState,
|
||||
RoomStateEvent.Members,
|
||||
throttle(
|
||||
() => {
|
||||
setCount(room.getJoinedMemberCount());
|
||||
// At the time where `RoomStateEvent.Members` is emitted the
|
||||
// summary API has not had a chance to update the `summaryJoinedMemberCount`
|
||||
// value, therefore handling the logic locally here.
|
||||
//
|
||||
// Tracked as part of https://github.com/vector-im/element-web/issues/26033
|
||||
const membersCount = includeFunctional
|
||||
? room.getMembers().reduce((count, m) => {
|
||||
return m.membership === "join" ? count + 1 : count;
|
||||
}, 0)
|
||||
: getJoinedNonFunctionalMembers(room).length;
|
||||
setCount(membersCount);
|
||||
},
|
||||
throttleWait,
|
||||
{ leading: true, trailing: true },
|
||||
|
||||
Reference in New Issue
Block a user