From 533d679c6774687b81ecfb24c596081637c341ae Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 14 Aug 2025 11:10:34 -0300 Subject: [PATCH 1/7] [general-refactor] refactor settings fetching and use behavior --- src/commons/constants.ts | 3 + src/commons/types.ts | 5 ++ src/{utils => commons}/utils.ts | 4 +- src/components/modal/component.tsx | 15 ++-- .../modal/picked-user-view/component.tsx | 2 +- src/components/modal/types.ts | 5 +- src/components/pick-random-user/component.tsx | 32 +++------ src/components/pick-random-user/hooks.ts | 70 +++++++++++++++++++ 8 files changed, 97 insertions(+), 39 deletions(-) create mode 100644 src/commons/constants.ts create mode 100644 src/commons/types.ts rename src/{utils => commons}/utils.ts (88%) create mode 100644 src/components/pick-random-user/hooks.ts diff --git a/src/commons/constants.ts b/src/commons/constants.ts new file mode 100644 index 0000000..96072dd --- /dev/null +++ b/src/commons/constants.ts @@ -0,0 +1,3 @@ +const PICKED_USER_TIME_WINDOW = 10; // seconds + +export default PICKED_USER_TIME_WINDOW; diff --git a/src/commons/types.ts b/src/commons/types.ts new file mode 100644 index 0000000..fc54250 --- /dev/null +++ b/src/commons/types.ts @@ -0,0 +1,5 @@ +export interface PickRandomUserSettings { + pingSoundEnabled: boolean; + pingSoundUrl: string; + pickedUserTimeWindow: number; +} diff --git a/src/utils/utils.ts b/src/commons/utils.ts similarity index 88% rename from src/utils/utils.ts rename to src/commons/utils.ts index 04bbf7e..f4ed9cf 100644 --- a/src/utils/utils.ts +++ b/src/commons/utils.ts @@ -9,7 +9,7 @@ import { PickedUserSeenEntryDataChannel } from '../components/pick-random-user/t * @param pickedUserId userId from the picked user * @returns boolean indicating if the current user has seen the picked-user */ -const hasCurrentUserSeenPickedUser = ( +export const hasCurrentUserSeenPickedUser = ( pickedUserSeenEntries: GraphqlResponseWrapper< DataChannelEntryResponseType[]>, currentUserId: string, @@ -20,4 +20,4 @@ const hasCurrentUserSeenPickedUser = ( && view.payloadJson.seenByUserId === currentUserId && view.payloadJson.pickedUserId === pickedUserId); -export default hasCurrentUserSeenPickedUser; +export const isNumber = (obj: unknown): boolean => obj && typeof obj && !Number.isNaN(obj); diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 8d93a49..899a61a 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -6,7 +6,7 @@ import * as Styled from './styles'; import { PickUserModalProps, WindowClientSettings } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; -import hasCurrentUserSeenPickedUser from '../../utils/utils'; +import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; const intlMessages = defineMessages({ currentUserPicked: { @@ -35,8 +35,7 @@ function notifyRandomlyPickedUser(message: string) { export function PickUserModal(props: PickUserModalProps) { const { - pluginSettings, - isPluginSettingsLoading, + pickRandomUserSettings, intl, showModal, handleCloseModal, @@ -57,6 +56,7 @@ export function PickUserModal(props: PickUserModalProps) { pushPickedUserSeen, } = props; + const { pingSoundEnabled, pingSoundUrl } = pickRandomUserSettings; const [showPresenterView, setShowPresenterView] = useState( currentUser?.presenter && !pickedUserWithEntryId, ); @@ -65,22 +65,15 @@ export function PickUserModal(props: PickUserModalProps) { useEffect(() => { // Play audio when user is selected - const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( pickedUserSeenEntries, userId, pickedUserWithEntryId?.pickedUser?.userId, ); - const isPingSoundEnabled = !isPluginSettingsLoading && pluginSettings?.pingSoundEnabled; - if (isPingSoundEnabled && pickedUserWithEntryId + if (pingSoundEnabled && pickedUserWithEntryId && pickedUserWithEntryId?.pickedUser?.userId === userId // Current user must not have seen this entry and data should be done loading && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { - const { cdn, basename } = window.meetingClientSettings.public.app; - const host = cdn + basename; - const pingSoundUrl: string = pluginSettings?.pingSoundUrl - ? String(pluginSettings?.pingSoundUrl) - : `${host}/resources/sounds/doorbell.mp3`; const audio = new Audio(pingSoundUrl); audio.play(); notifyRandomlyPickedUser(intl.formatMessage(intlMessages.currentUserPicked)); diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index ce49867..87b8b2b 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { defineMessages } from 'react-intl'; import { PickedUserViewComponentProps } from './types'; import * as Styled from './styles'; -import hasCurrentUserSeenPickedUser from '../../../utils/utils'; +import { hasCurrentUserSeenPickedUser } from '../../../commons/utils'; const intlMessages = defineMessages({ currentUserPicked: { diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index a76b49d..6c1136c 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -1,12 +1,11 @@ -import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; import { CurrentUserData, DeleteEntryFunction, GraphqlResponseWrapper } from 'bigbluebutton-html-plugin-sdk'; import { IntlShape } from 'react-intl'; import { DataChannelEntryResponseType, PushEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; import { PickedUser, PickedUserWithEntryId, PickedUserSeenEntryDataChannel } from '../pick-random-user/types'; +import { PickRandomUserSettings } from '../../commons/types'; export interface PickUserModalProps { - pluginSettings: PluginSettingsData; - isPluginSettingsLoading: boolean; + pickRandomUserSettings: PickRandomUserSettings intl: IntlShape showModal: boolean; handleCloseModal: () => void; diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index ea641a6..60c120b 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -3,7 +3,11 @@ import { useState, useEffect } from 'react'; import { createIntl, createIntlCache } from 'react-intl'; import { BbbPluginSdk, PluginApi, RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; -import hasCurrentUserSeenPickedUser from '../../utils/utils'; +import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; +import { + useGetAllSettings, + useRequestPermissionForNotification, +} from './hooks'; import { ModalInformationFromPresenter, PickRandomUserPluginProps, @@ -24,8 +28,6 @@ const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === }, } : null; -const PICKED_USER_TIME_WINDOW = 10; // seconds - function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { BbbPluginSdk.initialize(uuid); const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); @@ -37,25 +39,12 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const [userFilterViewer, setUserFilterViewer] = useState(true); const [filterOutPresenter, setFilterOutPresenter] = useState(true); const [filterOutPickedUsers, setFilterOutPickedUsers] = useState(true); - const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); - const { data: pluginSettings, loading: isPluginSettingsLoading } = pluginApi.usePluginSettings(); + const settingsResponseData = pluginApi.usePluginSettings(); + const pickRandomUserSettings = useGetAllSettings(settingsResponseData); + const { pickedUserTimeWindow, pingSoundEnabled } = pickRandomUserSettings; - useEffect(() => { - if (!isPluginSettingsLoading - && pluginSettings - && pluginSettings.pingSoundEnabled) { - Notification.requestPermission(); - } - if (!isPluginSettingsLoading - && pluginSettings - && pluginSettings.pickedUserTimeWindow - && typeof pluginSettings.pickedUserTimeWindow === 'number' - && !Number.isNaN(pluginSettings.pickedUserTimeWindow) - ) { - setPickedUserTimeWindow(pluginSettings.pickedUserTimeWindow); - } - }, [isPluginSettingsLoading, pluginSettings]); + useRequestPermissionForNotification(pingSoundEnabled); const currentUserInfo = pluginApi.useCurrentUser(); const shouldUnmountPlugin = pluginApi.useShouldUnmountPlugin(); @@ -195,8 +184,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { <> void, + settingsData: GraphqlResponseWrapper, +) => { + const { data: pluginSettings, loading: isPluginSettingsLoading } = settingsData; + useEffect(() => { + if (!isPluginSettingsLoading && pluginSettings) { + callback(pluginSettings); + } + }, [isPluginSettingsLoading, pluginSettings]); +}; + +export const useRequestPermissionForNotification = ( + pingSoundEnabled: boolean, +) => { + useEffect(() => { + if (pingSoundEnabled) { + Notification.requestPermission(); + } + }, [pingSoundEnabled]); +}; + +export const getPingSoundEnabled = ( + settings: PluginSettingsData, +): boolean => !!settings.pingSoundEnabled; + +const getPickedUserTimeWindowFromSettings = (settings: PluginSettingsData) => { + const settingTimeWindow = settings.pickedUserTimeWindow as unknown; + if (isNumber(settingTimeWindow)) { + const timeWindow: number = settingTimeWindow as number; + return timeWindow; + } return PICKED_USER_TIME_WINDOW; +}; + +const getPingSoundUrl = (settings: PluginSettingsData): string => { + const { cdn, basename } = window.meetingClientSettings.public.app; + const host = cdn + basename; + const pingSoundUrl: string = settings.pingSoundUrl + ? String(settings.pingSoundUrl) + : `${host}/resources/sounds/doorbell.mp3`; + return pingSoundUrl; +}; + +export const useGetAllSettings = ( + settingsData: GraphqlResponseWrapper, +): PickRandomUserSettings => { + const [pingSoundEnabled, setPingSoundEnabled] = useState(false); + const [pingSoundUrl, setPingSoundUrl] = useState(''); + const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); + useSettingsLoaded((settings) => { + setPickedUserTimeWindow(getPickedUserTimeWindowFromSettings(settings)); + setPingSoundEnabled(getPingSoundEnabled(settings)); + setPingSoundUrl(getPingSoundUrl(settings)); + }, settingsData); + return { + pingSoundEnabled, + pingSoundUrl, + pickedUserTimeWindow, + }; +}; From 08361bb1d2822e634a9b68cac11e7c80e13956ad Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 09:33:56 -0300 Subject: [PATCH 2/7] [general-refactor] Filter Options have been moved to be controlled by a hook only --- manifest.json | 2 +- src/commons/constants.ts | 4 +- src/components/modal/component.tsx | 15 --- .../modal/presenter-view/component.tsx | 42 +++++--- src/components/modal/presenter-view/types.ts | 6 -- src/components/modal/types.ts | 7 -- src/components/pick-random-user/component.tsx | 95 +++++++++---------- src/components/pick-random-user/context.ts | 16 ++++ src/components/pick-random-user/hooks.ts | 94 +++++++++++++++++- src/components/pick-random-user/types.ts | 12 +-- 10 files changed, 188 insertions(+), 105 deletions(-) create mode 100644 src/components/pick-random-user/context.ts diff --git a/manifest.json b/manifest.json index 98e4d12..85b7167 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "localesBaseUrl": "locales", "dataChannels": [ { - "name": "modalInformationFromPresenter", + "name": "filterOptions", "pushPermission": ["presenter"], "replaceOrDeletePermission": ["creator", "moderator"] }, diff --git a/src/commons/constants.ts b/src/commons/constants.ts index 96072dd..dfa0a11 100644 --- a/src/commons/constants.ts +++ b/src/commons/constants.ts @@ -1,3 +1 @@ -const PICKED_USER_TIME_WINDOW = 10; // seconds - -export default PICKED_USER_TIME_WINDOW; +export const PICKED_USER_TIME_WINDOW = 10; // seconds diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 899a61a..e22787a 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -43,15 +43,8 @@ export function PickUserModal(props: PickUserModalProps) { pickedUserWithEntryId, handlePickRandomUser, currentUser, - filterOutPresenter, - setFilterOutPresenter, - userFilterViewer, - setUserFilterViewer, - filterOutPickedUsers, - setFilterOutPickedUsers, dataChannelPickedUsers, deletionFunction, - dispatcherPickedUser, pickedUserSeenEntries, pushPickedUserSeen, } = props; @@ -111,18 +104,11 @@ export function PickUserModal(props: PickUserModalProps) { ) : ( @@ -135,7 +121,6 @@ export function PickUserModal(props: PickUserModalProps) { currentUser, showModal, setShowPresenterView, - dispatcherPickedUser, }} /> ) diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index 3744190..ff19cc7 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -2,10 +2,12 @@ import * as React from 'react'; import { RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; import { defineMessages } from 'react-intl'; +import { useContext } from 'react'; import * as Styled from './styles'; import { PickedUser } from '../../pick-random-user/types'; import { PresenterViewComponentProps } from './types'; +import { FilterOptionsContext } from '../../pick-random-user/context'; const intlMessages = defineMessages({ optionsTitle: { @@ -113,12 +115,6 @@ const makeVerticalListOfNames = ( export function PresenterViewComponent(props: PresenterViewComponentProps) { const { intl, - filterOutPresenter, - setFilterOutPresenter, - userFilterViewer, - setUserFilterViewer, - filterOutPickedUsers, - setFilterOutPickedUsers, deletionFunction, handlePickRandomUser, dataChannelPickedUsers, @@ -126,9 +122,20 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { users, } = props; + const { + filterOptions, + setFilterOptions, + } = useContext(FilterOptionsContext); + + const { + skipModerators, + skipPresenter, + includePickedUsers, + } = filterOptions; + let userRoleLabel: string; const usersCountVariable = { 0: users?.length }; - if (userFilterViewer) { + if (skipModerators) { userRoleLabel = (users?.length !== 1) ? intl.formatMessage(intlMessages.viewerLabelPlural, usersCountVariable) : intl.formatMessage(intlMessages.viewerLabel, usersCountVariable); @@ -148,9 +155,12 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { { - setUserFilterViewer(!userFilterViewer); + setFilterOptions((filterOptionsPrevious) => ({ + ...filterOptionsPrevious, + skipModerators: !filterOptionsPrevious.skipModerators, + })); }} name="options" value="skipModerators" @@ -163,9 +173,12 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { { - setFilterOutPresenter(!filterOutPresenter); + setFilterOptions((filterOptionsPrevious) => ({ + ...filterOptionsPrevious, + skipPresenter: !filterOptionsPrevious.skipPresenter, + })); }} name="options" value="skipPresenter" @@ -178,9 +191,12 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { { - setFilterOutPickedUsers(!filterOutPickedUsers); + setFilterOptions((filterOptionsPrevious) => ({ + ...filterOptionsPrevious, + includePickedUsers: !filterOptionsPrevious.includePickedUsers, + })); }} name="options" value="includePickedUsers" diff --git a/src/components/modal/presenter-view/types.ts b/src/components/modal/presenter-view/types.ts index d00e750..26cabcc 100644 --- a/src/components/modal/presenter-view/types.ts +++ b/src/components/modal/presenter-view/types.ts @@ -5,12 +5,6 @@ import { PickedUser, PickedUserWithEntryId } from '../../pick-random-user/types' export interface PresenterViewComponentProps { intl: IntlShape; - filterOutPresenter: boolean; - setFilterOutPresenter: (filter: boolean) => void; - userFilterViewer: boolean; - setUserFilterViewer: (filter: boolean) => void; - filterOutPickedUsers: boolean; - setFilterOutPickedUsers: (filter: boolean) => void; deletionFunction: DeleteEntryFunction; handlePickRandomUser: () => void; dataChannelPickedUsers?: DataChannelEntryResponseType[]; diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index 6c1136c..b02fea4 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -13,15 +13,8 @@ export interface PickUserModalProps { pickedUserWithEntryId: PickedUserWithEntryId; handlePickRandomUser: () => void; currentUser: CurrentUserData; - filterOutPresenter: boolean, - setFilterOutPresenter: (filter: boolean) => void, - userFilterViewer: boolean; - setUserFilterViewer: (filter: boolean) => void; - filterOutPickedUsers: boolean, - setFilterOutPickedUsers: (filter: boolean) => void, dataChannelPickedUsers?: DataChannelEntryResponseType[]; deletionFunction: DeleteEntryFunction; - dispatcherPickedUser: PushEntryFunction; pickedUserSeenEntries: GraphqlResponseWrapper< DataChannelEntryResponseType[]>; pushPickedUserSeen: PushEntryFunction; diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 60c120b..60ad8bf 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -1,21 +1,22 @@ import * as React from 'react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { createIntl, createIntlCache } from 'react-intl'; import { BbbPluginSdk, PluginApi, RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; import { useGetAllSettings, + useGetFilterOptions, useRequestPermissionForNotification, } from './hooks'; import { - ModalInformationFromPresenter, PickRandomUserPluginProps, PickedUserSeenEntryDataChannel, PickedUser, PickedUserWithEntryId, UsersMoreInformationGraphqlResponse, } from './types'; +import { FilterOptionsContext } from './context'; import { USERS_MORE_INFORMATION } from './queries'; import { PickUserModal } from '../modal/component'; import { Role } from './enums'; @@ -36,9 +37,6 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const [ pickedUserWithEntryId, setPickedUserWithEntryId] = useState(); - const [userFilterViewer, setUserFilterViewer] = useState(true); - const [filterOutPresenter, setFilterOutPresenter] = useState(true); - const [filterOutPickedUsers, setFilterOutPickedUsers] = useState(true); const settingsResponseData = pluginApi.usePluginSettings(); const pickRandomUserSettings = useGetAllSettings(settingsResponseData); @@ -71,10 +69,13 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { pushEntry: pushPickedUser, deleteEntry: deletePickedUser, } = pluginApi.useDataChannel('pickRandomUser'); + + const [filterOptions, setFilterOptions] = useGetFilterOptions(pluginApi, currentUser?.presenter); const { - data: modalInformationFromPresenter, - pushEntry: dispatchModalInformationFromPresenter, - } = pluginApi.useDataChannel('modalInformationFromPresenter'); + skipModerators, + skipPresenter, + includePickedUsers, + } = filterOptions; const { data: pickedUserSeenEntries, @@ -87,23 +88,11 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { loading: false, }; - useEffect(() => { - const modalInformationList = modalInformationFromPresenter - .data; - const modalInformation = modalInformationList - ? modalInformationList[modalInformationList.length - 1]?.payloadJson : null; - if (modalInformation) { - setFilterOutPresenter(modalInformation.skipPresenter); - setUserFilterViewer(modalInformation.skipModerators); - setFilterOutPickedUsers(!modalInformation.includePickedUsers); - } - }, [modalInformationFromPresenter]); - const usersToBePicked: UsersMoreInformationGraphqlResponse = { user: allUsers?.user.filter((user) => { let roleFilter = true; - if (userFilterViewer) roleFilter = user.role === Role.VIEWER; - if (filterOutPickedUsers && pickedUserFromDataChannel.data) { + if (skipModerators) roleFilter = user.role === Role.VIEWER; + if (!includePickedUsers && pickedUserFromDataChannel.data) { return roleFilter && pickedUserFromDataChannel .data.findIndex( (u) => u?.payloadJson?.userId === user?.userId, @@ -111,7 +100,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { } return roleFilter; }).filter((user) => { - if (filterOutPresenter) return !user.presenter; + if (skipPresenter) return !user.presenter; return true; }), }; @@ -128,11 +117,6 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const handleCloseModal = (): void => { if (currentUser?.presenter) { - dispatchModalInformationFromPresenter({ - skipModerators: userFilterViewer, - skipPresenter: filterOutPresenter, - includePickedUsers: !filterOutPickedUsers, - }); pushPickedUser(null); } setShowModal(false); @@ -176,35 +160,42 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { if (!pickedUserWithEntryId && !currentUser?.presenter) setShowModal(false); }, [pickedUserWithEntryId]); + const value = useMemo( + () => ({ + filterOptions, + setFilterOptions, + }), + [filterOptions, setFilterOptions], + ); + useEffect(() => { - if (!currentUser?.presenter && dispatchModalInformationFromPresenter) handleCloseModal(); + if (!currentUser?.presenter) handleCloseModal(); }, [currentUser]); + if (!intl || localeMessagesLoading) return null; + return !shouldUnmountPlugin && ( <> - + + + > +} + +export const FilterOptionsContext = createContext({ + filterOptions: { + skipModerators: true, + skipPresenter: true, + includePickedUsers: true, + }, + setFilterOptions: () => {}, +}); diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index 5eb1586..f4ec451 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -1,13 +1,22 @@ -import { GraphqlResponseWrapper } from 'bigbluebutton-html-plugin-sdk'; +import { + DataChannelEntryResponseType, + DataChannelTypes, + GraphqlResponseWrapper, + PluginApi, + PushEntryFunction, +} from 'bigbluebutton-html-plugin-sdk'; import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; import { useEffect, useState } from 'react'; -import PICKED_USER_TIME_WINDOW from '../../commons/constants'; +import { PICKED_USER_TIME_WINDOW } from '../../commons/constants'; import { isNumber } from '../../commons/utils'; import { PickRandomUserSettings } from '../../commons/types'; import { WindowClientSettings } from '../modal/types'; +import { FilterOptionsType } from './types'; declare const window: WindowClientSettings; +// Settings hooks and utilities + const useSettingsLoaded = ( callback: (settings: PluginSettingsData) => void, settingsData: GraphqlResponseWrapper, @@ -68,3 +77,84 @@ export const useGetAllSettings = ( pickedUserTimeWindow, }; }; + +// Filter Options hooks and utilities + +const useUpdateFilterOptionsOnDataChannel = ( + pushFilterOptionsToDataChannel: PushEntryFunction, + filterOptions: FilterOptionsType, + isPresenter: boolean, + dataChannelLoading: boolean, +) => { + useEffect(() => { + if (isPresenter && !dataChannelLoading) { + pushFilterOptionsToDataChannel(filterOptions); + } + }, [isPresenter, filterOptions, dataChannelLoading]); +}; + +const hasFilterOptionsChanged = ( + currentFilterOptions: FilterOptionsType, + filterOptionsFromDataChannel?: FilterOptionsType, +) => filterOptionsFromDataChannel?.includePickedUsers !== currentFilterOptions.includePickedUsers + || filterOptionsFromDataChannel?.skipModerators !== currentFilterOptions.skipModerators + || filterOptionsFromDataChannel?.skipPresenter !== currentFilterOptions.skipPresenter; + +const useObserveFilterOptionsFromDataChannel = ( + currentFilterOptions: FilterOptionsType, + filterOptionsFromDataChannel: FilterOptionsType | undefined, + setFilterOptions: React.Dispatch>, +) => { + useEffect(() => { + if ( + filterOptionsFromDataChannel + && hasFilterOptionsChanged(currentFilterOptions, filterOptionsFromDataChannel)) { + setFilterOptions({ + includePickedUsers: filterOptionsFromDataChannel.includePickedUsers, + skipModerators: filterOptionsFromDataChannel.skipModerators, + skipPresenter: filterOptionsFromDataChannel.skipPresenter, + }); + } + }, [filterOptionsFromDataChannel]); +}; + +const getLatestFilterOptionsFromDataChannel = ( + filterOptionsFromDataChannelResponse: GraphqlResponseWrapper< + DataChannelEntryResponseType[] + >, +) => { + const persistedFilterOptionsList = filterOptionsFromDataChannelResponse.data; + const currentFilterOptionsFromDataChannel = persistedFilterOptionsList + ? persistedFilterOptionsList[0]?.payloadJson : null; + return currentFilterOptionsFromDataChannel; +}; + +export const useGetFilterOptions = ( + pluginApi: PluginApi, + currentUserPresenter: boolean, +): [FilterOptionsType, React.Dispatch>] => { + const [filterOptions, setFilterOptions] = useState({ + skipModerators: true, + skipPresenter: true, + includePickedUsers: true, + }); + const { + data: filterOptionsFromDataChannel, + pushEntry: pushFilterOptionsToDataChannel, + } = pluginApi.useDataChannel('filterOptions', DataChannelTypes.LATEST_ITEM); + const latestFilterOptionFromDataChannel = getLatestFilterOptionsFromDataChannel( + filterOptionsFromDataChannel, + ); + useObserveFilterOptionsFromDataChannel( + filterOptions, + latestFilterOptionFromDataChannel, + setFilterOptions, + ); + useUpdateFilterOptionsOnDataChannel( + pushFilterOptionsToDataChannel, + filterOptions, + currentUserPresenter, + filterOptionsFromDataChannel.loading, + ); + return [filterOptions, setFilterOptions]; +}; diff --git a/src/components/pick-random-user/types.ts b/src/components/pick-random-user/types.ts index f72deb1..587e1db 100644 --- a/src/components/pick-random-user/types.ts +++ b/src/components/pick-random-user/types.ts @@ -21,13 +21,13 @@ export interface UsersMoreInformationGraphqlResponse { user: PickedUser[]; } -export interface ModalInformationFromPresenter { - skipModerators: boolean; - skipPresenter: boolean; - includePickedUsers: boolean; -} - export interface PickedUserSeenEntryDataChannel { pickedUserId: string; seenByUserId: string; } + +export interface FilterOptionsType { + skipModerators: boolean; + skipPresenter: boolean; + includePickedUsers: boolean; +} From ee3a66acbe24741b378d84828c68f0570b9c7e95 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 13:50:39 -0300 Subject: [PATCH 3/7] [general-refactor] simplify with some more hooks and utilities new flows. --- src/commons/constants.ts | 2 + src/commons/hooks.ts | 29 ++++ .../action-button-dropdown/component.tsx | 6 +- .../action-button-dropdown/types.ts | 2 +- src/components/modal/component.tsx | 41 ++---- src/components/modal/types.ts | 2 +- src/components/modal/utils.ts | 19 +++ src/components/pick-random-user/component.tsx | 131 +++++------------- src/components/pick-random-user/hooks.ts | 87 +++++++++++- src/components/pick-random-user/utils.ts | 28 ++++ 10 files changed, 215 insertions(+), 132 deletions(-) create mode 100644 src/commons/hooks.ts create mode 100644 src/components/modal/utils.ts create mode 100644 src/components/pick-random-user/utils.ts diff --git a/src/commons/constants.ts b/src/commons/constants.ts index dfa0a11..3bf1ac6 100644 --- a/src/commons/constants.ts +++ b/src/commons/constants.ts @@ -1 +1,3 @@ export const PICKED_USER_TIME_WINDOW = 10; // seconds + +export const TIMEOUT_CLOSE_NOTIFICATION = 5000; diff --git a/src/commons/hooks.ts b/src/commons/hooks.ts new file mode 100644 index 0000000..627850c --- /dev/null +++ b/src/commons/hooks.ts @@ -0,0 +1,29 @@ +import { PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import { createIntl, createIntlCache } from 'react-intl'; + +const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') + ? { + headers: { + 'ngrok-skip-browser-warning': 'any', + }, + } : undefined; + +export const useGetInternationalization = (pluginApi: PluginApi) => { + const { + messages: localeMessages, + currentLocale, + loading: localeMessagesLoading, + } = pluginApi.useLocaleMessages!(LOCALE_REQUEST_OBJECT); + + const cache = createIntlCache(); + const intl = (!localeMessagesLoading && localeMessages) ? createIntl({ + locale: currentLocale, + messages: localeMessages, + fallbackOnEmptyString: true, + }, cache) : null; + + return { + intl, + localeMessagesLoading, + }; +}; diff --git a/src/components/extensible-areas/action-button-dropdown/component.tsx b/src/components/extensible-areas/action-button-dropdown/component.tsx index 8e4fb4b..6263e0a 100644 --- a/src/components/extensible-areas/action-button-dropdown/component.tsx +++ b/src/components/extensible-areas/action-button-dropdown/component.tsx @@ -21,7 +21,7 @@ const intlMessages = defineMessages({ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): React.ReactNode { const { intl, - pickedUserWithEntryId, + currentPickedUser, currentUser, pluginApi, setShowModal, @@ -42,7 +42,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R }, }), ]); - } else if (!currentUser?.presenter && pickedUserWithEntryId) { + } else if (!currentUser?.presenter && currentPickedUser) { pluginApi.setActionButtonDropdownItems([ new ActionButtonDropdownSeparator(), new ActionButtonDropdownOption({ @@ -58,7 +58,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R } else { pluginApi.setActionButtonDropdownItems([]); } - }, [currentUserInfo, pickedUserWithEntryId, intl]); + }, [currentUserInfo, currentPickedUser, intl]); return null; } diff --git a/src/components/extensible-areas/action-button-dropdown/types.ts b/src/components/extensible-areas/action-button-dropdown/types.ts index b390e3a..3b4e437 100644 --- a/src/components/extensible-areas/action-button-dropdown/types.ts +++ b/src/components/extensible-areas/action-button-dropdown/types.ts @@ -4,7 +4,7 @@ import { PickedUserWithEntryId } from '../../pick-random-user/types'; export interface ActionButtonDropdownManagerProps { intl: IntlShape - pickedUserWithEntryId: PickedUserWithEntryId; + currentPickedUser: PickedUserWithEntryId; currentUser: CurrentUserData; pluginApi: PluginApi; setShowModal: React.Dispatch>; diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index e22787a..55a1ccb 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; import { defineMessages } from 'react-intl'; -import { pluginLogger } from 'bigbluebutton-html-plugin-sdk'; import * as Styled from './styles'; -import { PickUserModalProps, WindowClientSettings } from './types'; +import { PickUserModalProps } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; +import { notifyRandomlyPickedUser } from './utils'; const intlMessages = defineMessages({ currentUserPicked: { @@ -16,23 +16,6 @@ const intlMessages = defineMessages({ }, }); -const TIMEOUT_CLOSE_NOTIFICATION = 5000; - -declare const window: WindowClientSettings; - -function notifyRandomlyPickedUser(message: string) { - if (!('Notification' in window)) { - pluginLogger.warn('This browser does not support notifications'); - } else if (Notification.permission === 'granted') { - const notification = new Notification(message); - setTimeout(() => { - notification.close(); - }, TIMEOUT_CLOSE_NOTIFICATION); - } else if (Notification.permission !== 'denied') { - pluginLogger.warn('Browser notification permission has been denied'); - } -} - export function PickUserModal(props: PickUserModalProps) { const { pickRandomUserSettings, @@ -40,7 +23,7 @@ export function PickUserModal(props: PickUserModalProps) { showModal, handleCloseModal, users, - pickedUserWithEntryId, + currentPickedUser, handlePickRandomUser, currentUser, dataChannelPickedUsers, @@ -51,7 +34,7 @@ export function PickUserModal(props: PickUserModalProps) { const { pingSoundEnabled, pingSoundUrl } = pickRandomUserSettings; const [showPresenterView, setShowPresenterView] = useState( - currentUser?.presenter && !pickedUserWithEntryId, + currentUser?.presenter && !currentPickedUser, ); const [userId, setUserId] = useState(currentUser?.userId || ''); @@ -61,24 +44,24 @@ export function PickUserModal(props: PickUserModalProps) { const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( pickedUserSeenEntries, userId, - pickedUserWithEntryId?.pickedUser?.userId, + currentPickedUser?.pickedUser?.userId, ); - if (pingSoundEnabled && pickedUserWithEntryId - && pickedUserWithEntryId?.pickedUser?.userId === userId + if (pingSoundEnabled && currentPickedUser + && currentPickedUser?.pickedUser?.userId === userId // Current user must not have seen this entry and data should be done loading && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { const audio = new Audio(pingSoundUrl); audio.play(); notifyRandomlyPickedUser(intl.formatMessage(intlMessages.currentUserPicked)); } - }, [userId, pickedUserWithEntryId, pickedUserSeenEntries]); + }, [userId, currentPickedUser, pickedUserSeenEntries]); useEffect(() => { - setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); + setShowPresenterView(currentUser?.presenter && !currentPickedUser); if (userId === '') { setUserId(currentUser.userId); } - }, [currentUser, pickedUserWithEntryId]); + }, [currentUser, currentPickedUser]); return ( @@ -116,7 +99,7 @@ export function PickUserModal(props: PickUserModalProps) { {...{ pickedUserSeenEntries, pushPickedUserSeen, - pickedUserWithEntryId, + pickedUserWithEntryId: currentPickedUser, intl, currentUser, showModal, diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index b02fea4..298cae3 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -10,7 +10,7 @@ export interface PickUserModalProps { showModal: boolean; handleCloseModal: () => void; users?: PickedUser[]; - pickedUserWithEntryId: PickedUserWithEntryId; + currentPickedUser: PickedUserWithEntryId; handlePickRandomUser: () => void; currentUser: CurrentUserData; dataChannelPickedUsers?: DataChannelEntryResponseType[]; diff --git a/src/components/modal/utils.ts b/src/components/modal/utils.ts new file mode 100644 index 0000000..4cfb41e --- /dev/null +++ b/src/components/modal/utils.ts @@ -0,0 +1,19 @@ +import { pluginLogger } from 'bigbluebutton-html-plugin-sdk'; + +import { TIMEOUT_CLOSE_NOTIFICATION } from '../../commons/constants'; +import { WindowClientSettings } from './types'; + +declare const window: WindowClientSettings; + +export function notifyRandomlyPickedUser(message: string) { + if (!('Notification' in window)) { + pluginLogger.warn('This browser does not support notifications'); + } else if (Notification.permission === 'granted') { + const notification = new Notification(message); + setTimeout(() => { + notification.close(); + }, TIMEOUT_CLOSE_NOTIFICATION); + } else if (Notification.permission !== 'denied') { + pluginLogger.warn('Browser notification permission has been denied'); + } +} diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 60ad8bf..4a76360 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { useState, useEffect, useMemo } from 'react'; -import { createIntl, createIntlCache } from 'react-intl'; import { BbbPluginSdk, PluginApi, RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; -import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; import { + useControlModalState, useGetAllSettings, + useGetCurrentPickedUser, useGetFilterOptions, useRequestPermissionForNotification, } from './hooks'; @@ -13,32 +13,23 @@ import { PickRandomUserPluginProps, PickedUserSeenEntryDataChannel, PickedUser, - PickedUserWithEntryId, UsersMoreInformationGraphqlResponse, } from './types'; import { FilterOptionsContext } from './context'; import { USERS_MORE_INFORMATION } from './queries'; import { PickUserModal } from '../modal/component'; -import { Role } from './enums'; import ActionButtonDropdownManager from '../extensible-areas/action-button-dropdown/component'; - -const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') - ? { - headers: { - 'ngrok-skip-browser-warning': 'any', - }, - } : null; +import { filterPossibleUsersToBePicked } from './utils'; +import { useGetInternationalization } from '../../commons/hooks'; function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { BbbPluginSdk.initialize(uuid); const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); const [showModal, setShowModal] = useState(false); - const [ - pickedUserWithEntryId, - setPickedUserWithEntryId] = useState(); const settingsResponseData = pluginApi.usePluginSettings(); + const pickRandomUserSettings = useGetAllSettings(settingsResponseData); const { pickedUserTimeWindow, pingSoundEnabled } = pickRandomUserSettings; @@ -52,30 +43,20 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const { data: allUsers } = allUsersInfo; const { - messages: localeMessages, - currentLocale, - loading: localeMessagesLoading, - } = pluginApi.useLocaleMessages(LOCALE_REQUEST_OBJECT); - - const cache = createIntlCache(); - const intl = (!localeMessagesLoading && localeMessages) ? createIntl({ - locale: currentLocale, - messages: localeMessages, - fallbackOnEmptyString: true, - }, cache) : null; + intl, + localeMessagesLoading, + } = useGetInternationalization(pluginApi); const { data: pickedUserFromDataChannelResponse, pushEntry: pushPickedUser, deleteEntry: deletePickedUser, } = pluginApi.useDataChannel('pickRandomUser'); + const pickedUserFromDataChannel = pickedUserFromDataChannelResponse?.data; const [filterOptions, setFilterOptions] = useGetFilterOptions(pluginApi, currentUser?.presenter); - const { - skipModerators, - skipPresenter, - includePickedUsers, - } = filterOptions; + + const currentPickedUser = useGetCurrentPickedUser(pickedUserFromDataChannel); const { data: pickedUserSeenEntries, @@ -83,36 +64,23 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { deleteEntry: deletePickedUserSeenEntries, } = pluginApi.useDataChannel('pickedUserSeenEntry'); - const pickedUserFromDataChannel = { - data: pickedUserFromDataChannelResponse?.data, - loading: false, - }; - - const usersToBePicked: UsersMoreInformationGraphqlResponse = { - user: allUsers?.user.filter((user) => { - let roleFilter = true; - if (skipModerators) roleFilter = user.role === Role.VIEWER; - if (!includePickedUsers && pickedUserFromDataChannel.data) { - return roleFilter && pickedUserFromDataChannel - .data.findIndex( - (u) => u?.payloadJson?.userId === user?.userId, - ) === -1; - } - return roleFilter; - }).filter((user) => { - if (skipPresenter) return !user.presenter; - return true; - }), - }; + const possibleUsersToBePicked = filterPossibleUsersToBePicked( + allUsers, + pickedUserFromDataChannel, + filterOptions, + ); const handlePickRandomUser = () => { - if (usersToBePicked && usersToBePicked.user.length > 0 && currentUser?.presenter) { + if ( + possibleUsersToBePicked + && possibleUsersToBePicked.user.length > 0 + && currentUser?.presenter + ) { deletePickedUserSeenEntries([RESET_DATA_CHANNEL]); - const randomIndex = Math.floor(Math.random() * usersToBePicked.user.length); - const randomlyPickedUser = usersToBePicked.user[randomIndex]; + const randomIndex = Math.floor(Math.random() * possibleUsersToBePicked.user.length); + const randomlyPickedUser = possibleUsersToBePicked.user[randomIndex]; pushPickedUser(randomlyPickedUser); } - setShowModal(true); }; const handleCloseModal = (): void => { @@ -122,43 +90,14 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { setShowModal(false); }; - useEffect(() => { - if (pickedUserFromDataChannel.data - && pickedUserFromDataChannel.data?.length > 0) { - const pickedUserToUpdate = pickedUserFromDataChannel - .data[0]; - if (pickedUserToUpdate?.entryId !== pickedUserWithEntryId?.entryId) { - setPickedUserWithEntryId({ - pickedUser: pickedUserToUpdate?.payloadJson, - entryId: pickedUserToUpdate.entryId, - }); - } - const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( - pickedUserSeenEntries, - currentUser?.userId, - pickedUserToUpdate?.payloadJson.userId, - ); - const isPreviousPickInTimeWindow = ( - (new Date().getTime() - new Date(pickedUserToUpdate.createdAt).getTime()) / 1000 - <= pickedUserTimeWindow - ); - if ( - !hasCurrentUserSeen - && !pickedUserSeenEntries?.loading - && isPreviousPickInTimeWindow - ) { - setShowModal(true); - } - } else if (pickedUserFromDataChannel.data - && pickedUserFromDataChannel.data?.length === 0) { - setPickedUserWithEntryId(null); - if (currentUser && !currentUser.presenter) setShowModal(false); - } - }, [pickedUserFromDataChannelResponse, pickedUserSeenEntries, pickedUserTimeWindow]); - - useEffect(() => { - if (!pickedUserWithEntryId && !currentUser?.presenter) setShowModal(false); - }, [pickedUserWithEntryId]); + useControlModalState( + pickedUserFromDataChannel, + pickedUserSeenEntries, + currentUser, + pickedUserTimeWindow, + currentPickedUser, + setShowModal, + ); const value = useMemo( () => ({ @@ -185,11 +124,11 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { intl, showModal, handleCloseModal, - users: usersToBePicked?.user, - pickedUserWithEntryId, + users: possibleUsersToBePicked?.user, + currentPickedUser, handlePickRandomUser, currentUser, - dataChannelPickedUsers: pickedUserFromDataChannel.data, + dataChannelPickedUsers: pickedUserFromDataChannel, deletionFunction: deletePickedUser, pickedUserSeenEntries, pushPickedUserSeen, @@ -199,7 +138,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { [], +): PickedUserWithEntryId | undefined => { + const [ + pickedUserWithEntryId, + setPickedUserWithEntryId] = useState(); + + useEffect(() => { + if (pickedUserFromDataChannel + && pickedUserFromDataChannel?.length > 0) { + const pickedUserToUpdate = pickedUserFromDataChannel[0]; + if (pickedUserToUpdate?.entryId !== pickedUserWithEntryId?.entryId) { + setPickedUserWithEntryId({ + pickedUser: pickedUserToUpdate?.payloadJson, + entryId: pickedUserToUpdate.entryId, + }); + } + } else if (pickedUserFromDataChannel + && pickedUserFromDataChannel?.length === 0) { + setPickedUserWithEntryId(null); + } + }, [pickedUserFromDataChannel]); + return pickedUserWithEntryId; +}; + +export const useControlModalState = ( + pickedUserFromDataChannel: DataChannelEntryResponseType[], + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentUser: CurrentUserData, + pickedUserTimeWindow: number, + currentPickedUser: PickedUserWithEntryId, + setShowModal: React.Dispatch>, +) => { + const hasValidPickInTimeWindow = (pickedUser?: DataChannelEntryResponseType) => { + if (!pickedUser) return false; + const secondsSincePick = (Date.now() - new Date(pickedUser.createdAt).getTime()) / 1000; + return secondsSincePick <= pickedUserTimeWindow; + }; + + const hasUserSeenPickedUser = (pickedUser?: DataChannelEntryResponseType) => { + if (!pickedUser) return false; + return hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUser?.userId, + pickedUser.payloadJson.userId, + ); + }; + + const shouldShowModal = (pickedUser?: DataChannelEntryResponseType) => { + if (!pickedUser) return false; + return ( + !hasUserSeenPickedUser(pickedUser) + && !pickedUserSeenEntries?.loading + && hasValidPickInTimeWindow(pickedUser) + ); + }; + + useEffect(() => { + const firstPick = pickedUserFromDataChannel?.[0]; + + if (firstPick && shouldShowModal(firstPick)) { + setShowModal(true); + return; + } + + const shouldCloseModal = (!firstPick && !currentUser?.presenter) + || (!currentPickedUser && !currentUser?.presenter); + + if (shouldCloseModal) { + setShowModal(false); + } + }, [currentPickedUser, pickedUserFromDataChannel, pickedUserSeenEntries, pickedUserTimeWindow]); +}; diff --git a/src/components/pick-random-user/utils.ts b/src/components/pick-random-user/utils.ts new file mode 100644 index 0000000..20861c2 --- /dev/null +++ b/src/components/pick-random-user/utils.ts @@ -0,0 +1,28 @@ +import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk'; +import { + FilterOptionsType, + PickedUser, + UsersMoreInformationGraphqlResponse, +} from './types'; +import { Role } from './enums'; + +export const filterPossibleUsersToBePicked = ( + allUsers: UsersMoreInformationGraphqlResponse, + pickedUserFromDataChannel: DataChannelEntryResponseType[], + filterOptions: FilterOptionsType, +) => ({ + user: allUsers?.user.filter((user) => { + if (filterOptions.skipModerators) return user.role === Role.VIEWER; + return true; + }).filter((user) => { + if (filterOptions.skipPresenter) return !user.presenter; + return true; + }).filter((user) => { + if (!filterOptions.includePickedUsers && pickedUserFromDataChannel) { + return pickedUserFromDataChannel.findIndex( + (u) => u?.payloadJson?.userId === user?.userId, + ) === -1; + } + return true; + }), +}); From 1a32f7e8c8071db9effe19727c2681f00a562b0d Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 13:55:14 -0300 Subject: [PATCH 4/7] [general-refactor] return back the original initial state of filterOptions --- src/components/pick-random-user/context.ts | 2 +- src/components/pick-random-user/hooks.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pick-random-user/context.ts b/src/components/pick-random-user/context.ts index 3bec055..08a2604 100644 --- a/src/components/pick-random-user/context.ts +++ b/src/components/pick-random-user/context.ts @@ -10,7 +10,7 @@ export const FilterOptionsContext = createContext({ filterOptions: { skipModerators: true, skipPresenter: true, - includePickedUsers: true, + includePickedUsers: false, }, setFilterOptions: () => {}, }); diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index 6c7cbda..79e0d1a 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -142,7 +142,7 @@ export const useGetFilterOptions = ( const [filterOptions, setFilterOptions] = useState({ skipModerators: true, skipPresenter: true, - includePickedUsers: true, + includePickedUsers: false, }); const { data: filterOptionsFromDataChannel, From 8c176e81ab175d96a9fdab19595a6a24354914f8 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 16:22:15 -0300 Subject: [PATCH 5/7] [general-refactor] rename skip presenter/moderator to include presenter/moderator --- public/locales/de.json | 2 - public/locales/en.json | 4 +- public/locales/fr-FR.json | 2 - public/locales/it.json | 2 - public/locales/ja.json | 2 - public/locales/pt-BR.json | 2 - .../modal/presenter-view/component.tsx | 42 +++++++++---------- src/components/pick-random-user/context.ts | 4 +- src/components/pick-random-user/hooks.ts | 12 +++--- src/components/pick-random-user/types.ts | 4 +- src/components/pick-random-user/utils.ts | 4 +- 11 files changed, 35 insertions(+), 45 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 455f371..d4aca6b 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -4,8 +4,6 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "zurück", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Avatar von Teilnehmer {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Optionen", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Überspringe Moderatoren", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Überspringe Präsentator", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Wähle bereits gewählte Teilnehmer", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Zur Auswahl stehen", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "Teilnehmer", diff --git a/public/locales/en.json b/public/locales/en.json index 5772509..e15908c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -4,8 +4,8 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "back", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Avatar image of user {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Skip moderators", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Skip presenter", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Include moderators", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Include presenter", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked user", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Available for selection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "user", diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index 02b93f1..b817097 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -3,8 +3,6 @@ "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Participant sélectionné au hasard", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "retour", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Ignorer les modérateurs", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Ignorer le présentateur", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure un participant déjà sélectionné", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponible pour la sélection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "participant", diff --git a/public/locales/it.json b/public/locales/it.json index 0b82bf4..01084d5 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -3,8 +3,6 @@ "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Utente scelto a caso", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "indietro", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opzioni", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Salta i moderatori", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Salta il presentatore", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi l'utente già selezionato", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponibile per la selezione", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "utente", diff --git a/public/locales/ja.json b/public/locales/ja.json index 2590bed..2454325 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -4,8 +4,6 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "戻る", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "ユーザー {0} のアバター画像", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "設定", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "司会者を含めない", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "発表者を含めない", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "既に指名した人を含める", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "指名可能な人", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "人のユーザー", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index 9915303..85bfd03 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -4,8 +4,6 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "voltar", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Iamgem avatar do usuário {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opções", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Ignorar moderadores", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Ignorar apresentador", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuário já selecionado", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponíveis para seleção", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "usuário", diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index ff19cc7..18b9adf 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -15,13 +15,13 @@ const intlMessages = defineMessages({ description: 'Title of the options section on modal`s presenter view', defaultMessage: 'Options', }, - skipModeratorsLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel', - description: 'Label of skip moderator`s option on modal`s presenter view', - defaultMessage: 'Skip moderators', + includeModeratorsLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel', + description: 'Label to include moderator`s option on modal`s presenter view', + defaultMessage: 'Include moderators', }, - skipPresenterLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel', + includePresenterLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel', description: 'Label of skip presenter`s option on modal`s presenter view', defaultMessage: 'Skip presenter', }, @@ -128,14 +128,14 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { } = useContext(FilterOptionsContext); const { - skipModerators, - skipPresenter, + includeModerators, + includePresenter, includePickedUsers, } = filterOptions; let userRoleLabel: string; const usersCountVariable = { 0: users?.length }; - if (skipModerators) { + if (!includeModerators) { userRoleLabel = (users?.length !== 1) ? intl.formatMessage(intlMessages.viewerLabelPlural, usersCountVariable) : intl.formatMessage(intlMessages.viewerLabel, usersCountVariable); @@ -151,40 +151,40 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { {intl.formatMessage(intlMessages.optionsTitle)} - + { setFilterOptions((filterOptionsPrevious) => ({ ...filterOptionsPrevious, - skipModerators: !filterOptionsPrevious.skipModerators, + includeModerators: !filterOptionsPrevious.includeModerators, })); }} name="options" - value="skipModerators" + value="includeModerators" /> - {intl.formatMessage(intlMessages.skipModeratorsLabel)} + {intl.formatMessage(intlMessages.includeModeratorsLabel)} - + { setFilterOptions((filterOptionsPrevious) => ({ ...filterOptionsPrevious, - skipPresenter: !filterOptionsPrevious.skipPresenter, + includePresenter: !filterOptionsPrevious.includePresenter, })); }} name="options" - value="skipPresenter" + value="includePresenter" /> - {intl.formatMessage(intlMessages.skipPresenterLabel)} + {intl.formatMessage(intlMessages.includePresenterLabel)} diff --git a/src/components/pick-random-user/context.ts b/src/components/pick-random-user/context.ts index 08a2604..018c1b6 100644 --- a/src/components/pick-random-user/context.ts +++ b/src/components/pick-random-user/context.ts @@ -8,8 +8,8 @@ interface FilterOptionsContextType { export const FilterOptionsContext = createContext({ filterOptions: { - skipModerators: true, - skipPresenter: true, + includeModerators: false, + includePresenter: false, includePickedUsers: false, }, setFilterOptions: () => {}, diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index 79e0d1a..3b9f88a 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -103,8 +103,8 @@ const hasFilterOptionsChanged = ( currentFilterOptions: FilterOptionsType, filterOptionsFromDataChannel?: FilterOptionsType, ) => filterOptionsFromDataChannel?.includePickedUsers !== currentFilterOptions.includePickedUsers - || filterOptionsFromDataChannel?.skipModerators !== currentFilterOptions.skipModerators - || filterOptionsFromDataChannel?.skipPresenter !== currentFilterOptions.skipPresenter; + || filterOptionsFromDataChannel?.includeModerators !== currentFilterOptions.includeModerators + || filterOptionsFromDataChannel?.includePresenter !== currentFilterOptions.includePresenter; const useObserveFilterOptionsFromDataChannel = ( currentFilterOptions: FilterOptionsType, @@ -117,8 +117,8 @@ const useObserveFilterOptionsFromDataChannel = ( && hasFilterOptionsChanged(currentFilterOptions, filterOptionsFromDataChannel)) { setFilterOptions({ includePickedUsers: filterOptionsFromDataChannel.includePickedUsers, - skipModerators: filterOptionsFromDataChannel.skipModerators, - skipPresenter: filterOptionsFromDataChannel.skipPresenter, + includeModerators: filterOptionsFromDataChannel.includeModerators, + includePresenter: filterOptionsFromDataChannel.includePresenter, }); } }, [filterOptionsFromDataChannel]); @@ -140,8 +140,8 @@ export const useGetFilterOptions = ( currentUserPresenter: boolean, ): [FilterOptionsType, React.Dispatch>] => { const [filterOptions, setFilterOptions] = useState({ - skipModerators: true, - skipPresenter: true, + includeModerators: false, + includePresenter: false, includePickedUsers: false, }); const { diff --git a/src/components/pick-random-user/types.ts b/src/components/pick-random-user/types.ts index 587e1db..bc5ee58 100644 --- a/src/components/pick-random-user/types.ts +++ b/src/components/pick-random-user/types.ts @@ -27,7 +27,7 @@ export interface PickedUserSeenEntryDataChannel { } export interface FilterOptionsType { - skipModerators: boolean; - skipPresenter: boolean; + includeModerators: boolean; + includePresenter: boolean; includePickedUsers: boolean; } diff --git a/src/components/pick-random-user/utils.ts b/src/components/pick-random-user/utils.ts index 20861c2..b1053af 100644 --- a/src/components/pick-random-user/utils.ts +++ b/src/components/pick-random-user/utils.ts @@ -12,10 +12,10 @@ export const filterPossibleUsersToBePicked = ( filterOptions: FilterOptionsType, ) => ({ user: allUsers?.user.filter((user) => { - if (filterOptions.skipModerators) return user.role === Role.VIEWER; + if (!filterOptions.includeModerators) return user.role === Role.VIEWER; return true; }).filter((user) => { - if (filterOptions.skipPresenter) return !user.presenter; + if (!filterOptions.includePresenter) return !user.presenter; return true; }).filter((user) => { if (!filterOptions.includePickedUsers && pickedUserFromDataChannel) { From 21803c5cc686eaeb733123127180c06e9e621ec2 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 17:24:12 -0300 Subject: [PATCH 6/7] [browser-notification-enabled] separate ping sound from browser notification settings --- README.md | 29 +++++++++++----- src/commons/types.ts | 1 + src/components/modal/component.tsx | 15 ++++---- src/components/modal/utils.ts | 5 +++ src/components/pick-random-user/component.tsx | 4 +-- src/components/pick-random-user/hooks.ts | 34 +++++++++++++++---- 6 files changed, 66 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 93476d8..1166d7f 100644 --- a/README.md +++ b/README.md @@ -26,29 +26,43 @@ Down below, we list all the possible configurations this plugin supports, and th ```yaml - name: PickRandomUserPlugin settings: - pingSoundEnabled: false + pingSoundEnabled: true pingSoundUrl: resources/sounds/doorbell.mp3 + browserNotificationEnabled: true pickedUserTimeWindow: 10 # seconds ``` | Name | Description | Default | |------------------------|--------------------------------------|-----------------------------| -| `pingSoundEnabled` | Whether the ping sound is enabled | `false` | +| `pingSoundEnabled` | Flag that decides whether the ping sound is enabled for picked user | `true` | | `pingSoundUrl` | URL of the ping sound file | `resources/sounds/doorbell.mp3` | +| `browserNotificationEnabled` | Flag that decides whether to send browser notification when user is picked | `false` | | `pickedUserTimeWindow` | Time window to consider a user as recently picked (users that join after that time will not see the last modal) | `30` | -### Notification/Ping sound +### Notification -By default, no ping sound is played for the randomly picked user. To activate this feature, one must add the following configurations in their `/etc/bigbluebutton/bbb-html5.yml` file. +By default, browser notification when user is randomly picked is not enabled. To enable it, add the following settings in the `/etc/bigbluebutton/bbb-html5.yml` file: + +```yaml +public: + # ... + plugins: + - name: PickRandomUserPlugin + settings: + browserNotificationEnabled: false +``` + +### Ping sound + +By default, ping sound is played for the randomly picked user. To remove this feature, one must add the following configurations in their `/etc/bigbluebutton/bbb-html5.yml` file. So within that file and in `public.plugins` add the following configurations: ```yaml - name: PickRandomUserPlugin settings: - pingSoundEnabled: true - pingSoundUrl: resources/sounds/doorbell.mp3 + pingSoundEnabled: false ``` The result yaml will look like: @@ -59,8 +73,7 @@ public: plugins: - name: PickRandomUserPlugin settings: - pingSoundEnabled: true # Enables the ping sound for this plugin true/false - pingSoundUrl: resources/sounds/doorbell.mp3 # This is the default and is not mandatory + pingSoundEnabled: false ``` Just a minor comment: This relative URLs can only be configured if the server on which BBB is running is not a cluster setup. If that's your case, you'll need to put the whole URL into the configuration. It's also worth mentioning that the default `pingSoundUrl` will work in cluser setups, so no worries on that. diff --git a/src/commons/types.ts b/src/commons/types.ts index fc54250..f309889 100644 --- a/src/commons/types.ts +++ b/src/commons/types.ts @@ -1,5 +1,6 @@ export interface PickRandomUserSettings { pingSoundEnabled: boolean; pingSoundUrl: string; + browserNotificationEnabled: boolean; pickedUserTimeWindow: number; } diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 55a1ccb..f730adb 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -6,7 +6,7 @@ import { PickUserModalProps } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; -import { notifyRandomlyPickedUser } from './utils'; +import { notifyRandomlyPickedUser, pingSoundForRandomlyPickedUser } from './utils'; const intlMessages = defineMessages({ currentUserPicked: { @@ -32,7 +32,7 @@ export function PickUserModal(props: PickUserModalProps) { pushPickedUserSeen, } = props; - const { pingSoundEnabled, pingSoundUrl } = pickRandomUserSettings; + const { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled } = pickRandomUserSettings; const [showPresenterView, setShowPresenterView] = useState( currentUser?.presenter && !currentPickedUser, ); @@ -46,13 +46,16 @@ export function PickUserModal(props: PickUserModalProps) { userId, currentPickedUser?.pickedUser?.userId, ); - if (pingSoundEnabled && currentPickedUser + if (currentPickedUser && currentPickedUser?.pickedUser?.userId === userId // Current user must not have seen this entry and data should be done loading && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { - const audio = new Audio(pingSoundUrl); - audio.play(); - notifyRandomlyPickedUser(intl.formatMessage(intlMessages.currentUserPicked)); + if (pingSoundEnabled) pingSoundForRandomlyPickedUser(pingSoundUrl); + if (browserNotificationEnabled) { + notifyRandomlyPickedUser( + intl.formatMessage(intlMessages.currentUserPicked), + ); + } } }, [userId, currentPickedUser, pickedUserSeenEntries]); diff --git a/src/components/modal/utils.ts b/src/components/modal/utils.ts index 4cfb41e..7c7e93a 100644 --- a/src/components/modal/utils.ts +++ b/src/components/modal/utils.ts @@ -17,3 +17,8 @@ export function notifyRandomlyPickedUser(message: string) { pluginLogger.warn('Browser notification permission has been denied'); } } + +export function pingSoundForRandomlyPickedUser(pingSoundUrl: string) { + const audio = new Audio(pingSoundUrl); + audio.play(); +} diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 4a76360..34985ac 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -31,9 +31,9 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const settingsResponseData = pluginApi.usePluginSettings(); const pickRandomUserSettings = useGetAllSettings(settingsResponseData); - const { pickedUserTimeWindow, pingSoundEnabled } = pickRandomUserSettings; + const { pickedUserTimeWindow, browserNotificationEnabled } = pickRandomUserSettings; - useRequestPermissionForNotification(pingSoundEnabled); + useRequestPermissionForNotification(browserNotificationEnabled); const currentUserInfo = pluginApi.useCurrentUser(); const shouldUnmountPlugin = pluginApi.useShouldUnmountPlugin(); diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index 3b9f88a..d85d9bd 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -36,18 +36,33 @@ const useSettingsLoaded = ( }; export const useRequestPermissionForNotification = ( - pingSoundEnabled: boolean, + browserNotificationEnabled: boolean, ) => { useEffect(() => { - if (pingSoundEnabled) { + if (browserNotificationEnabled) { Notification.requestPermission(); } - }, [pingSoundEnabled]); + }, [browserNotificationEnabled]); }; export const getPingSoundEnabled = ( settings: PluginSettingsData, -): boolean => !!settings.pingSoundEnabled; + previousState: boolean, +): boolean => { + if (settings.pingSoundEnabled === undefined || settings.pingSoundEnabled === null) { + return previousState; + } return !!settings.pingSoundEnabled; +}; + +export const getBrowserNotificationEnabled = ( + settings: PluginSettingsData, + previousState: boolean, +): boolean => { + if (settings.browserNotificationEnabled === undefined + || settings.browserNotificationEnabled === null) { + return previousState; + } return !!settings.browserNotificationEnabled; +}; const getPickedUserTimeWindowFromSettings = (settings: PluginSettingsData) => { const settingTimeWindow = settings.pickedUserTimeWindow as unknown; @@ -69,17 +84,24 @@ const getPingSoundUrl = (settings: PluginSettingsData): string => { export const useGetAllSettings = ( settingsData: GraphqlResponseWrapper, ): PickRandomUserSettings => { - const [pingSoundEnabled, setPingSoundEnabled] = useState(false); + const [pingSoundEnabled, setPingSoundEnabled] = useState(true); + const [browserNotificationEnabled, setBrowserNotificationEnabled] = useState(false); const [pingSoundUrl, setPingSoundUrl] = useState(''); const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); useSettingsLoaded((settings) => { + setBrowserNotificationEnabled( + (previousState) => getBrowserNotificationEnabled(settings, previousState), + ); + setPingSoundEnabled( + (previousState) => getPingSoundEnabled(settings, previousState), + ); setPickedUserTimeWindow(getPickedUserTimeWindowFromSettings(settings)); - setPingSoundEnabled(getPingSoundEnabled(settings)); setPingSoundUrl(getPingSoundUrl(settings)); }, settingsData); return { pingSoundEnabled, pingSoundUrl, + browserNotificationEnabled, pickedUserTimeWindow, }; }; From 39f85d890099d9484689fb77da23958e3ffa3d63 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 21 Aug 2025 17:42:27 -0300 Subject: [PATCH 7/7] [browser-notification-enabled] Fixed multiple ping sounds and fix no audio playing --- src/commons/constants.ts | 2 + src/components/modal/component.tsx | 35 +++------- src/components/modal/hooks.ts | 70 +++++++++++++++++++ .../modal/picked-user-view/component.tsx | 2 +- src/components/pick-random-user/hooks.ts | 6 +- 5 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 src/components/modal/hooks.ts diff --git a/src/commons/constants.ts b/src/commons/constants.ts index 3bf1ac6..56b86e5 100644 --- a/src/commons/constants.ts +++ b/src/commons/constants.ts @@ -1,3 +1,5 @@ export const PICKED_USER_TIME_WINDOW = 10; // seconds export const TIMEOUT_CLOSE_NOTIFICATION = 5000; + +export const DEFAULT_PING_SOUND_URL = 'resources/sounds/doorbell.mp3'; diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index f730adb..fa04e45 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -5,8 +5,7 @@ import * as Styled from './styles'; import { PickUserModalProps } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; -import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; -import { notifyRandomlyPickedUser, pingSoundForRandomlyPickedUser } from './utils'; +import { useHandleCurrentUserNotification } from './hooks'; const intlMessages = defineMessages({ currentUserPicked: { @@ -32,38 +31,20 @@ export function PickUserModal(props: PickUserModalProps) { pushPickedUserSeen, } = props; - const { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled } = pickRandomUserSettings; const [showPresenterView, setShowPresenterView] = useState( currentUser?.presenter && !currentPickedUser, ); - const [userId, setUserId] = useState(currentUser?.userId || ''); - - useEffect(() => { - // Play audio when user is selected - const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( - pickedUserSeenEntries, - userId, - currentPickedUser?.pickedUser?.userId, - ); - if (currentPickedUser - && currentPickedUser?.pickedUser?.userId === userId - // Current user must not have seen this entry and data should be done loading - && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { - if (pingSoundEnabled) pingSoundForRandomlyPickedUser(pingSoundUrl); - if (browserNotificationEnabled) { - notifyRandomlyPickedUser( - intl.formatMessage(intlMessages.currentUserPicked), - ); - } - } - }, [userId, currentPickedUser, pickedUserSeenEntries]); + useHandleCurrentUserNotification( + currentUser, + pickedUserSeenEntries, + currentPickedUser, + pickRandomUserSettings, + intl.formatMessage(intlMessages.currentUserPicked), + ); useEffect(() => { setShowPresenterView(currentUser?.presenter && !currentPickedUser); - if (userId === '') { - setUserId(currentUser.userId); - } }, [currentUser, currentPickedUser]); return ( { + const [currentUserId, setCurrentUserId] = useState(currentUser?.userId || ''); + + useEffect(() => { + if (currentUserId === '') { + setCurrentUserId(currentUser.userId); + } + }, [currentUser]); + + return currentUserId; +}; + +export const useHandleCurrentUserNotification = ( + currentUser: CurrentUserData, + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentPickedUser: PickedUserWithEntryId, + pickRandomUserSettings: PickRandomUserSettings, + notificationMessage: string, +) => { + const currentUserId = useCurrentUserId(currentUser); + + const { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled } = pickRandomUserSettings; + + const [currentUserNotified, setCurrentUserNotified] = useState(false); + + const currentPickedUserId = currentPickedUser?.pickedUser?.userId; + + // Control internal state of user-notification to not rely only on data-channel information + useEffect(() => { + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUserId, + currentPickedUserId, + ); + if (hasCurrentUserSeen) { + setCurrentUserNotified(false); + } + }, [pickedUserSeenEntries]); + + // Notify or ping audio (or both) when user is selected + useEffect(() => { + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUserId, + currentPickedUserId, + ); + if (currentPickedUser + && currentPickedUserId === currentUserId + // Current user must not have seen this entry and data should be done loading + && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading + && !currentUserNotified + ) { + if (pingSoundEnabled) pingSoundForRandomlyPickedUser(pingSoundUrl); + if (browserNotificationEnabled) { + notifyRandomlyPickedUser( + notificationMessage, + ); + } + setCurrentUserNotified(true); + } + }, [currentUserId, currentPickedUser, pickedUserSeenEntries, pickRandomUserSettings]); +}; diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index 87b8b2b..cba664a 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -57,7 +57,7 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { seenByUserId: currentUser.userId, }); } - }, []); + }, [pickedUserWithEntryId]); const title = (pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) ? intl.formatMessage(intlMessages.currentUserPicked) : intl.formatMessage(intlMessages.randomUserPicked); diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index d85d9bd..f0c9636 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -8,7 +8,7 @@ import { } from 'bigbluebutton-html-plugin-sdk'; import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; import { useEffect, useState } from 'react'; -import { PICKED_USER_TIME_WINDOW } from '../../commons/constants'; +import { DEFAULT_PING_SOUND_URL, PICKED_USER_TIME_WINDOW } from '../../commons/constants'; import { hasCurrentUserSeenPickedUser, isNumber } from '../../commons/utils'; import { PickRandomUserSettings } from '../../commons/types'; import { WindowClientSettings } from '../modal/types'; @@ -77,7 +77,7 @@ const getPingSoundUrl = (settings: PluginSettingsData): string => { const host = cdn + basename; const pingSoundUrl: string = settings.pingSoundUrl ? String(settings.pingSoundUrl) - : `${host}/resources/sounds/doorbell.mp3`; + : `${host}/${DEFAULT_PING_SOUND_URL}`; return pingSoundUrl; }; @@ -86,7 +86,7 @@ export const useGetAllSettings = ( ): PickRandomUserSettings => { const [pingSoundEnabled, setPingSoundEnabled] = useState(true); const [browserNotificationEnabled, setBrowserNotificationEnabled] = useState(false); - const [pingSoundUrl, setPingSoundUrl] = useState(''); + const [pingSoundUrl, setPingSoundUrl] = useState(DEFAULT_PING_SOUND_URL); const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); useSettingsLoaded((settings) => { setBrowserNotificationEnabled(