From b87085eef662d41af62946f6014757dcfc56f72e Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Tue, 9 Dec 2025 16:54:41 -0300 Subject: [PATCH] [use-meeting-data] added more fields to useMeeting and projection function --- src/core/api/BbbPluginSdk.ts | 3 + src/core/api/types.ts | 14 ++ src/data-consumption/domain/meeting/index.ts | 1 + .../domain/meeting/meeting-data/hooks.ts | 16 ++ .../domain/meeting/meeting-data/types.ts | 154 ++++++++++++++++++ src/data-consumption/enums.ts | 1 + src/data-consumption/factory/hooks.ts | 82 ++++++++++ src/data-consumption/factory/types.ts | 7 + src/data-consumption/factory/utils.ts | 11 ++ src/data-consumption/index.ts | 2 + 10 files changed, 291 insertions(+) create mode 100644 src/data-consumption/domain/meeting/meeting-data/hooks.ts create mode 100644 src/data-consumption/domain/meeting/meeting-data/types.ts create mode 100644 src/data-consumption/factory/hooks.ts create mode 100644 src/data-consumption/factory/types.ts create mode 100644 src/data-consumption/factory/utils.ts diff --git a/src/core/api/BbbPluginSdk.ts b/src/core/api/BbbPluginSdk.ts index 90d06ded..a7de443d 100644 --- a/src/core/api/BbbPluginSdk.ts +++ b/src/core/api/BbbPluginSdk.ts @@ -56,6 +56,8 @@ import { useCustomQuery } from '../../data-consumption/domain/shared/custom-quer import { UseCustomQueryFunction } from '../../data-consumption/domain/shared/custom-query/types'; import { useCustomMutation } from '../../data-creation/hook'; import { UseCustomMutationFunction } from '../../data-creation/types'; +import { UseMeetingDataFunction } from '../../data-consumption/domain/meeting/meeting-data/types'; +import { useMeetingData } from '../../data-consumption/domain/meeting/meeting-data/hooks'; declare const window: PluginBrowserWindow; @@ -98,6 +100,7 @@ export abstract class BbbPluginSdk { pluginApi.useLoadedUserList = (() => useLoadedUserList()) as UseLoadedUserListFunction; pluginApi.useCurrentUser = (() => useCurrentUser()) as UseCurrentUserFunction; pluginApi.useMeeting = (() => useMeeting()) as UseMeetingFunction; + pluginApi.useMeetingData = useMeetingData as UseMeetingDataFunction; pluginApi.useUsersBasicInfo = (() => useUsersBasicInfo()) as UseUsersBasicInfoFunction; pluginApi.useTalkingIndicator = (() => useTalkingIndicator()) as UseTalkingIndicatorFunction; pluginApi.getJoinUrl = (params) => getJoinUrl(params); diff --git a/src/core/api/types.ts b/src/core/api/types.ts index 48a66821..65172232 100644 --- a/src/core/api/types.ts +++ b/src/core/api/types.ts @@ -37,6 +37,7 @@ import { UseShouldUnmountPluginFunction } from '../auxiliary/plugin-unmount/type import { GetUiDataFunction } from '../../ui-data/getters/types'; import { UseCustomQueryFunction } from '../../data-consumption/domain/shared/custom-query/types'; import { UseCustomMutationFunction } from '../../data-creation/types'; +import { UseMeetingDataFunction } from '../../data-consumption/domain/meeting/meeting-data/types'; // Setter Functions for the API export type SetPresentationToolbarItems = (presentationToolbarItem: @@ -147,8 +148,21 @@ export interface PluginApi { * * @returns `GraphqlResponseWrapper` with the CurrentMeeting type. * + * @deprecated use {@link useMeetingData} + * */ useMeeting?: UseMeetingFunction; + /** + * Returns an object containing the data on the current meeting, i.e. the meeting on which the + * plugin is running. + * + * @param projectionFunction - function to select only specific fields from the + * Meeting type (Optional - if not provided, returns all fields). + * + * @returns `GraphqlResponseWrapper` with the CurrentMeeting type. + * + */ + useMeetingData?: UseMeetingDataFunction; /** * Returns an object containing the brief data on every user in te meeting. * diff --git a/src/data-consumption/domain/meeting/index.ts b/src/data-consumption/domain/meeting/index.ts index 0b68de78..c9321833 100644 --- a/src/data-consumption/domain/meeting/index.ts +++ b/src/data-consumption/domain/meeting/index.ts @@ -1 +1,2 @@ export { Meeting } from './from-core/types'; +export { MeetingData } from './meeting-data/types'; diff --git a/src/data-consumption/domain/meeting/meeting-data/hooks.ts b/src/data-consumption/domain/meeting/meeting-data/hooks.ts new file mode 100644 index 00000000..fa2ee282 --- /dev/null +++ b/src/data-consumption/domain/meeting/meeting-data/hooks.ts @@ -0,0 +1,16 @@ +import { DeepPartial } from '../../../../data-consumption/factory/types'; +import { useProjectedValue } from '../../../../data-consumption/factory/hooks'; +import { DataConsumptionHooks } from '../../../enums'; +import { createDataConsumptionHook } from '../../../factory/hookCreator'; +import { MeetingData } from './types'; + +export const useMeetingData = ( + projectionFunction?: (q: MeetingData) => DeepPartial, +) => useProjectedValue( + createDataConsumptionHook< + MeetingData + >( + DataConsumptionHooks.MEETING_DATA, + ), + projectionFunction, +); diff --git a/src/data-consumption/domain/meeting/meeting-data/types.ts b/src/data-consumption/domain/meeting/meeting-data/types.ts new file mode 100644 index 00000000..151896e0 --- /dev/null +++ b/src/data-consumption/domain/meeting/meeting-data/types.ts @@ -0,0 +1,154 @@ +import { DeepPartial } from '../../../../data-consumption/factory/types'; +import { GraphqlResponseWrapper } from '../../../../core'; + +export interface LockSettings { + disableCam: boolean; + disableMic: boolean; + disableNotes: boolean; + disablePrivateChat: boolean; + disablePublicChat: boolean; + hasActiveLockSetting: boolean; + hideUserList: boolean; + hideViewersCursor: boolean; + hideViewersAnnotation: false, + meetingId: boolean; + webcamsOnlyForModerator: boolean; +} + +export interface groups { + groupId: string; + name: string; +} + +export interface WelcomeSettings { + welcomeMsg: string; + welcomeMsgForModerators: string; + meetingId: string; +} + +export interface MeetingRecording { + isRecording: boolean; + startedAt: Date; + previousRecordedTimeInSeconds: number; + startedBy: string; + stoppedAt: number; + stoppedBy: string; +} +export interface MeetingRecordingPolicies { + allowStartStopRecording: boolean; + autoStartRecording: boolean; + record: boolean; + keepEvents: boolean; + startedAt: number; + startedBy: string; + stoppedAt: number; + stoppedBy: string; +} + +export interface UsersPolicies { + allowModsToEjectCameras: boolean; + allowModsToUnmuteUsers: boolean; + authenticatedGuest: boolean; + allowPromoteGuestToModerator: boolean; + guestPolicy: string; + maxUserConcurrentAccesses: number; + maxUsers: number; + meetingId: string; + meetingLayout: string; + userCameraCap: number; + webcamsOnlyForModerator: boolean; + guestLobbyMessage: string | null; +} + +export interface VoiceSettings { + dialNumber: string; + meetingId: string; + muteOnStart: boolean; + telVoice: string; + voiceConf: string; +} + +export interface BreakoutPolicies { + breakoutRooms: Array; + captureNotes: string; + captureNotesFilename: string; + captureSlides: string; + captureSlidesFilename: string; + freeJoin: boolean; + parentId: string; + privateChatEnabled: boolean; + record: boolean; + sequence: number; +} + +export interface BreakoutRoomsCommonProperties { + durationInSeconds: number; + freeJoin: boolean; + sendInvitationToModerators: boolean; + startedAt: Date; +} + +export interface ExternalVideo { + externalVideoId: string; + playerCurrentTime: number; + playerPlaybackRate: number; + playerPlaying: boolean; + externalVideoUrl: string; + startedSharingAt: number; + stoppedSharingAt: number; + updatedAt: string; +} + +export interface Layout { + currentLayoutType: string; +} + +export interface ComponentsFlags { + hasCaption: boolean; + hasBreakoutRoom: boolean; + hasExternalVideo: boolean; + hasPoll: boolean; + hasScreenshare: boolean; + hasTimer: boolean; + showRemainingTime: boolean; + hasCameraAsContent: boolean; + hasScreenshareAsContent: boolean; + hasCurrentPresentation: boolean; + hasSharedNotes: boolean; + isSharedNotesPinned: boolean; +} + +export interface MeetingData { + createdTime: number; + disabledFeatures: Array; + durationInSeconds: number; + extId: string; + isBreakout: boolean; + learningDashboardAccessToken: string; + maxPinnedCameras: number; + meetingCameraCap: number; + cameraBridge: string; + screenShareBridge: string; + audioBridge: string; + meetingId: string; + name: string; + notifyRecordingIsOn: boolean; + presentationUploadExternalDescription: string; + presentationUploadExternalUrl: string; + usersPolicies: UsersPolicies; + lockSettings: LockSettings; + voiceSettings: VoiceSettings; + breakoutPolicies: BreakoutPolicies; + breakoutRoomsCommonProperties: BreakoutRoomsCommonProperties; + externalVideo: ExternalVideo; + layout: Layout; + componentsFlags: ComponentsFlags; + endWhenNoModerator: boolean; + endWhenNoModeratorDelayInMinutes: number; + loginUrl: string | null; + groups: Array; +} + +export type UseMeetingDataFunction = ( + projectionFunction?: (q: MeetingData) => DeepPartial, +) => GraphqlResponseWrapper; diff --git a/src/data-consumption/enums.ts b/src/data-consumption/enums.ts index b8ed57f2..238ce3c1 100644 --- a/src/data-consumption/enums.ts +++ b/src/data-consumption/enums.ts @@ -5,6 +5,7 @@ export enum DataConsumptionHooks { USERS_BASIC_INFO = 'Hooks::UseUsersBasicInfo', LOADED_CHAT_MESSAGES = 'Hooks::UseLoadedChatMessages', MEETING = 'Hooks::UseMeeting', + MEETING_DATA = 'Hooks::UseMeetingData', TALKING_INDICATOR = 'Hooks::UseTalkingIndicator', CUSTOM_SUBSCRIPTION = 'Hooks::CustomSubscription', CUSTOM_QUERY = 'Hooks::CustomQuery', diff --git a/src/data-consumption/factory/hooks.ts b/src/data-consumption/factory/hooks.ts new file mode 100644 index 00000000..d88bd48c --- /dev/null +++ b/src/data-consumption/factory/hooks.ts @@ -0,0 +1,82 @@ +import { + useRef, + useMemo, + useEffect, + useState, +} from 'react'; +import { sortedStringify } from '../utils'; +import { GraphqlResponseWrapper } from '../../core'; +import { hasErrorChanged } from './utils'; +import { DeepPartial } from './types'; + +// Function overload for array type +export function useProjectedValue( + queryResult: GraphqlResponseWrapper, + project?: (q: TQueryResult) => DeepPartial, +): GraphqlResponseWrapper | GraphqlResponseWrapper[]>; +// Function overload for single value type +export function useProjectedValue( + queryResult: GraphqlResponseWrapper, + project?: (q: TQueryResult) => DeepPartial, +): GraphqlResponseWrapper | GraphqlResponseWrapper>; +// Implementation +export function useProjectedValue( + queryResult: GraphqlResponseWrapper, + project?: (q: TQueryResult) => DeepPartial, +): GraphqlResponseWrapper + | GraphqlResponseWrapper[] | DeepPartial> { + if (!project) return queryResult; + + const isArray = Array.isArray(queryResult.data); + + // Store the previous projected data to compare against + const previousProjectedDataRef = useRef< + DeepPartial[] | DeepPartial | undefined + >(undefined); + + // Compute the new projected value from the data field + const currentProjectedData = useMemo(() => { + if (!queryResult.data) return undefined; + if (isArray) { + return (queryResult.data as TQueryResult[]).map((item) => project(item)); + } + return project(queryResult.data as TQueryResult); + }, [queryResult.data, project, isArray]); + + // Initialize state with the wrapper structure + const [projectionResult, setProjectionResult] = useState< + GraphqlResponseWrapper[] | DeepPartial> + >({ + loading: queryResult.loading, + data: currentProjectedData, + error: queryResult.error, + }); + + useEffect(() => { + // Perform deep equality check using sortedStringify + const currentSerialized = sortedStringify(currentProjectedData); + const previousSerialized = sortedStringify(previousProjectedDataRef.current); + + // Check if loading or error states changed + const loadingChanged = queryResult.loading !== projectionResult.loading; + const errorChanged = hasErrorChanged(projectionResult.error, queryResult.error); + + // Only update if the projected data, loading, or error state has changed + if (currentSerialized !== previousSerialized || loadingChanged || errorChanged) { + previousProjectedDataRef.current = currentProjectedData; + setProjectionResult({ + loading: queryResult.loading, + data: currentProjectedData, + error: queryResult.error, + }); + } + }, [ + currentProjectedData, + queryResult.loading, + queryResult.error, + projectionResult.loading, + projectionResult.error, + ]); + + return projectionResult; +} diff --git a/src/data-consumption/factory/types.ts b/src/data-consumption/factory/types.ts new file mode 100644 index 00000000..1a58cf55 --- /dev/null +++ b/src/data-consumption/factory/types.ts @@ -0,0 +1,7 @@ +export type DeepPartial = { + [K in keyof T]?: T[K] extends (infer U)[] + ? DeepPartial[] + : T[K] extends object + ? DeepPartial + : T[K]; +}; diff --git a/src/data-consumption/factory/utils.ts b/src/data-consumption/factory/utils.ts new file mode 100644 index 00000000..0eacd4d0 --- /dev/null +++ b/src/data-consumption/factory/utils.ts @@ -0,0 +1,11 @@ +import { ApolloError } from '@apollo/client'; +import { sortedStringify } from '../utils'; + +export const hasErrorChanged = ( + previousResultError?: ApolloError, + currentResultError?: ApolloError, +) => { + const currentSerialized = sortedStringify(currentResultError); + const previousSerialized = sortedStringify(previousResultError); + return currentSerialized !== previousSerialized; +}; diff --git a/src/data-consumption/index.ts b/src/data-consumption/index.ts index 0a28ea66..410f4af5 100644 --- a/src/data-consumption/index.ts +++ b/src/data-consumption/index.ts @@ -4,3 +4,5 @@ export * from './domain/meeting'; export * from './domain/users'; export * from './domain/user-voice'; export * from './domain/shared'; + +export * from './factory/types';