Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/core/api/BbbPluginSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions src/core/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions src/data-consumption/domain/meeting/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { Meeting } from './from-core/types';
export { MeetingData } from './meeting-data/types';
16 changes: 16 additions & 0 deletions src/data-consumption/domain/meeting/meeting-data/hooks.ts
Original file line number Diff line number Diff line change
@@ -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<MeetingData>,
) => useProjectedValue<MeetingData>(
createDataConsumptionHook<
MeetingData
>(
DataConsumptionHooks.MEETING_DATA,
),
projectionFunction,
);
154 changes: 154 additions & 0 deletions src/data-consumption/domain/meeting/meeting-data/types.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>;
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<string>;
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<groups>;
}

export type UseMeetingDataFunction = (
projectionFunction?: (q: MeetingData) => DeepPartial<MeetingData>,
) => GraphqlResponseWrapper<MeetingData>;
1 change: 1 addition & 0 deletions src/data-consumption/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
82 changes: 82 additions & 0 deletions src/data-consumption/factory/hooks.ts
Original file line number Diff line number Diff line change
@@ -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<TQueryResult>(
queryResult: GraphqlResponseWrapper<TQueryResult[]>,
project?: (q: TQueryResult) => DeepPartial<TQueryResult>,
): GraphqlResponseWrapper<TQueryResult[]> | GraphqlResponseWrapper<DeepPartial<TQueryResult>[]>;
// Function overload for single value type
export function useProjectedValue<TQueryResult>(
queryResult: GraphqlResponseWrapper<TQueryResult>,
project?: (q: TQueryResult) => DeepPartial<TQueryResult>,
): GraphqlResponseWrapper<TQueryResult> | GraphqlResponseWrapper<DeepPartial<TQueryResult>>;
// Implementation
export function useProjectedValue<TQueryResult>(
queryResult: GraphqlResponseWrapper<TQueryResult[] | TQueryResult>,
project?: (q: TQueryResult) => DeepPartial<TQueryResult>,
): GraphqlResponseWrapper<TQueryResult[] | TQueryResult>
| GraphqlResponseWrapper<DeepPartial<TQueryResult>[] | DeepPartial<TQueryResult>> {
if (!project) return queryResult;

const isArray = Array.isArray(queryResult.data);

// Store the previous projected data to compare against
const previousProjectedDataRef = useRef<
DeepPartial<TQueryResult>[] | DeepPartial<TQueryResult> | 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<TQueryResult>[] | DeepPartial<TQueryResult>>
>({
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;
}
7 changes: 7 additions & 0 deletions src/data-consumption/factory/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type DeepPartial<T> = {
[K in keyof T]?: T[K] extends (infer U)[]
? DeepPartial<U>[]
: T[K] extends object
? DeepPartial<T[K]>
: T[K];
};
11 changes: 11 additions & 0 deletions src/data-consumption/factory/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 2 additions & 0 deletions src/data-consumption/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';