From 05368963736c7504343ce7c4c145a3fcdf754254 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 3 Aug 2026 20:48:19 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(sdk)=20add=20a=20room=20configuration?= =?UTF-8?q?=20popup=20from=20CreateMeetingButton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a room configuration popup opened from the SDK's CreateMeetingButton, laid out like the Google Meet "call options" dialog: logo header, grey section bands, and a footer bar with the close action. Like CreatePopup, it runs in a dedicated popup window so it can access session cookies, which would be blocked in an iframe. If the user is not authenticated, they are redirected to login and come back to this popup afterwards. Permissions are enforced server-side. The room is fetched with the user's session, and settings are only shown when the room is administrable by this user. Since #1482 removed the is_administrable flag from the room serializer (roles now live in the LiveKit participant attributes, only available in-meeting), administrability is detected here through the presence of the `accesses` field, which the backend only serializes for administrators and owners. The PATCH endpoint enforces the same permissions server-side regardless. The settings mirror the in-room Admin panel. Unlike the Admin panel, there is no LiveKit connection here, so changes are only persisted in the room configuration (and applied when a session starts): participants of an ongoing session are not live-synced or notified. --- CHANGELOG.md | 1 + .../src/features/rooms/api/ApiRoom.ts | 19 +- .../sdk/routes/CreateMeetingButton.tsx | 23 +- .../src/features/sdk/routes/SettingsPopup.tsx | 383 ++++++++++++++++++ .../src/features/sdk/utils/PopupManager.ts | 14 + src/frontend/src/locales/de/sdk.json | 10 +- src/frontend/src/locales/en/sdk.json | 10 +- src/frontend/src/locales/fr/sdk.json | 10 +- src/frontend/src/locales/nl/sdk.json | 10 +- src/frontend/src/routes.ts | 7 + 10 files changed, 478 insertions(+), 9 deletions(-) create mode 100644 src/frontend/src/features/sdk/routes/SettingsPopup.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 419e7776..b198d70f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to - ✨(frontend) expose media state to external gateways - ✨(frontend) add connection test feature - ✨(sdk) allow passing a background color to the calendar iframe +- ✨(sdk) add a room configuration popup from CreateMeetingButton ### Changed diff --git a/src/frontend/src/features/rooms/api/ApiRoom.ts b/src/frontend/src/features/rooms/api/ApiRoom.ts index 7c8fde65..86309a83 100644 --- a/src/frontend/src/features/rooms/api/ApiRoom.ts +++ b/src/frontend/src/features/rooms/api/ApiRoom.ts @@ -18,6 +18,14 @@ export type RoomConfiguration = { everyone_can_mute?: boolean | null } +export type ParticipantRole = 'member' | 'administrator' | 'owner' +export type AssignableParticipantRole = Exclude + +export type ApiResourceAccess = { + id: string + role: ParticipantRole +} + export type ApiRoom = { id: string name: string @@ -27,7 +35,12 @@ export type ApiRoom = { access_level: ApiAccessLevel livekit?: ApiLiveKit configuration?: RoomConfiguration + /** + * Only present in the API response when the requesting user is an + * administrator or owner of the room (see RoomSerializer.to_representation + * in the backend). Its presence can therefore be used to detect + * administrability outside of a LiveKit session, where the room_role + * participant attribute is not available. + */ + accesses?: ApiResourceAccess[] } - -export type ParticipantRole = 'member' | 'administrator' | 'owner' -export type AssignableParticipantRole = Exclude diff --git a/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx b/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx index ca8445bb..6527f80f 100644 --- a/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx +++ b/src/frontend/src/features/sdk/routes/CreateMeetingButton.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-aria-components' import { useTranslation } from 'react-i18next' import { HStack, VStack } from '@/styled-system/jsx' import { css } from '@/styled-system/css' -import { RiCloseLine, RiFileCopyLine } from '@remixicon/react' +import { RiCloseLine, RiFileCopyLine, RiSettings3Line } from '@remixicon/react' import { Text } from '@/primitives' import { Spinner } from '@/primitives/Spinner' import { buttonRecipe } from '@/primitives/buttonRecipe' @@ -37,6 +37,11 @@ const CreateMeetingButton = () => { initialRoom ) + const [isRoomCreatedInSession, setIsRoomCreatedInSession] = useState(false) + + const showSettingsButton = + isRoomCreatedInSession || searchParams.get('settings') === 'true' + const { data } = useRoomCreationCallback({ callbackId }) const roomUrl = useMemo(() => { @@ -74,6 +79,7 @@ const CreateMeetingButton = () => { useEffect(() => { if (!data?.room?.slug) return setRoom(data.room) + setIsRoomCreatedInSession(true) setCallbackId(undefined) setIsPending(false) popupManager.sendRoomData({ @@ -89,6 +95,7 @@ const CreateMeetingButton = () => { (id) => setCallbackId(id), (data) => { setRoom(data) + setIsRoomCreatedInSession(true) setIsPending(false) } ) @@ -98,6 +105,7 @@ const CreateMeetingButton = () => { const resetState = () => { setRoom(undefined) + setIsRoomCreatedInSession(false) setCallbackId(undefined) setIsPending(false) popupManager.clearState() @@ -158,14 +166,25 @@ const CreateMeetingButton = () => { {t('joinButton')} + {showSettingsButton && ( + + + + ) +} + +export default SettingsPopup diff --git a/src/frontend/src/features/sdk/utils/PopupManager.ts b/src/frontend/src/features/sdk/utils/PopupManager.ts index e6a7afc8..9d4362a3 100644 --- a/src/frontend/src/features/sdk/utils/PopupManager.ts +++ b/src/frontend/src/features/sdk/utils/PopupManager.ts @@ -24,6 +24,20 @@ export class PopupManager { } } + public createSettingsPopupWindow(roomSlug: string, onFailure: () => void) { + const popupWindow = window.open( + `${window.location.origin}/sdk/settings-popup?slug=${encodeURIComponent(roomSlug)}`, + 'SettingsPopupWindow', + `status=no,location=no,toolbar=no,menubar=no,width=600,height=800,left=100,top=100, resizable=yes,scrollbars=yes` + ) + + if (popupWindow) { + popupWindow.focus() + } else { + onFailure() + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private messageParent(type: ClientMessageType, data: any) { window?.parent.postMessage( diff --git a/src/frontend/src/locales/de/sdk.json b/src/frontend/src/locales/de/sdk.json index 8c76fb3e..fa7abd51 100644 --- a/src/frontend/src/locales/de/sdk.json +++ b/src/frontend/src/locales/de/sdk.json @@ -5,6 +5,14 @@ "copyLinkTooltip": "Link kopieren", "resetLabel": "Zurücksetzen", "participantLimit": "Bis zu 150 Teilnehmende.", - "popupBlocked": "Popup wurde blockiert. Bitte erlaube Popups für diese Website." + "popupBlocked": "Popup wurde blockiert. Bitte erlaube Popups für diese Website.", + "settingsTooltip": "Besprechungseinstellungen" + }, + "roomSettings": { + "title": "Besprechungseinstellungen", + "description": "Konfiguriere die Besprechung {{roomSlug}}.", + "notAllowed": "Du hast keine Berechtigung, die Einstellungen dieser Besprechung zu ändern.", + "error": "Die Besprechungseinstellungen konnten nicht geladen werden.", + "closeButton": "Schließen" } } diff --git a/src/frontend/src/locales/en/sdk.json b/src/frontend/src/locales/en/sdk.json index 80438d83..86d13bef 100644 --- a/src/frontend/src/locales/en/sdk.json +++ b/src/frontend/src/locales/en/sdk.json @@ -5,6 +5,14 @@ "copyLinkTooltip": "Copy link", "resetLabel": "Reset", "participantLimit": "Up to 150 participants.", - "popupBlocked": "Popup was blocked. Please allow popups for this site." + "popupBlocked": "Popup was blocked. Please allow popups for this site.", + "settingsTooltip": "Meeting settings" + }, + "roomSettings": { + "title": "Meeting settings", + "description": "Configure the meeting {{roomSlug}}.", + "notAllowed": "You don't have permission to modify this meeting's settings.", + "error": "Unable to load meeting settings.", + "closeButton": "Close" } } diff --git a/src/frontend/src/locales/fr/sdk.json b/src/frontend/src/locales/fr/sdk.json index 5f04ca3a..a48edff1 100644 --- a/src/frontend/src/locales/fr/sdk.json +++ b/src/frontend/src/locales/fr/sdk.json @@ -5,6 +5,14 @@ "copyLinkTooltip": "Copier le lien", "resetLabel": "Réinitialiser", "participantLimit": "Jusqu'à 150 participants.", - "popupBlocked": "La fenêtre pop-up a été bloquée. Veuillez autoriser les pop-ups pour ce site." + "popupBlocked": "La fenêtre pop-up a été bloquée. Veuillez autoriser les pop-ups pour ce site.", + "settingsTooltip": "Paramètres de la réunion" + }, + "roomSettings": { + "title": "Paramètres de la réunion", + "description": "Configurez la réunion {{roomSlug}}.", + "notAllowed": "Vous n'avez pas les droits pour modifier les paramètres de cette réunion.", + "error": "Impossible de charger les paramètres de la réunion.", + "closeButton": "Fermer" } } diff --git a/src/frontend/src/locales/nl/sdk.json b/src/frontend/src/locales/nl/sdk.json index c82ac9a7..a365fdbc 100644 --- a/src/frontend/src/locales/nl/sdk.json +++ b/src/frontend/src/locales/nl/sdk.json @@ -5,6 +5,14 @@ "copyLinkTooltip": "Link kopiëren", "resetLabel": "Resetten", "participantLimit": "Tot 150 deelnemers.", - "popupBlocked": "Pop-up werd geblokkeerd. Sta pop-ups toe voor deze site." + "popupBlocked": "Pop-up werd geblokkeerd. Sta pop-ups toe voor deze site.", + "settingsTooltip": "Vergaderinstellingen" + }, + "roomSettings": { + "title": "Vergaderinstellingen", + "description": "Configureer de vergadering {{roomSlug}}.", + "notAllowed": "Je hebt geen toestemming om de instellingen van deze vergadering te wijzigen.", + "error": "De vergaderinstellingen konden niet worden geladen.", + "closeButton": "Sluiten" } } diff --git a/src/frontend/src/routes.ts b/src/frontend/src/routes.ts index aa9b75a6..6e1a0061 100644 --- a/src/frontend/src/routes.ts +++ b/src/frontend/src/routes.ts @@ -9,6 +9,7 @@ const CreatePopup = lazy(() => import('@/features/sdk/routes/CreatePopup')) const CreateMeetingButton = lazy( () => import('@/features/sdk/routes/CreateMeetingButton') ) +const SettingsPopup = lazy(() => import('@/features/sdk/routes/SettingsPopup')) const LegalTermsRoute = lazy( () => import('@/features/legalsTerms/LegalTermsRoute') ) @@ -36,6 +37,7 @@ export const routes: Record< | 'termsOfService' | 'sdkCreatePopup' | 'sdkCreateButton' + | 'sdkSettingsPopup' | 'recordingDownload', { name: RouteName @@ -91,6 +93,11 @@ export const routes: Record< path: '/sdk/create-button', Component: CreateMeetingButton, }, + sdkSettingsPopup: { + name: 'sdkSettingsPopup', + path: '/sdk/settings-popup', + Component: SettingsPopup, + }, recordingDownload: { name: 'recordingDownload', path: /^\/recording\/(?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/,