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})$/,