+ );
+}
+
+AccessCodeForm.propTypes = {
+ onAccessCodeSubmit: PropTypes.func.isRequired,
+ error: PropTypes.bool,
+ friendlyId: PropTypes.string.isRequired,
+};
+
+AccessCodeForm.defaultProps = {
+ error: false,
+};
diff --git a/app/javascript/components/rooms/room/public_recordings/PublicRecordings.jsx b/app/javascript/components/rooms/room/public_recordings/PublicRecordings.jsx
index 922a142647..dfb3d37c37 100644
--- a/app/javascript/components/rooms/room/public_recordings/PublicRecordings.jsx
+++ b/app/javascript/components/rooms/room/public_recordings/PublicRecordings.jsx
@@ -14,20 +14,56 @@
// You should have received a copy of the GNU Lesser General Public License along
// with Greenlight; if not, see .
-/* eslint-disable consistent-return */
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import { Row } from 'react-bootstrap';
+import { useSearchParams } from 'react-router-dom';
import Logo from '../../../shared_components/Logo';
import PublicRecordingsCard from './PublicRecordingsCard';
-export default function RoomJoin() {
+export default function PublicRecordings() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [accessCode, setAccessCode] = useState('');
+ const [accessCodeError, setAccessCodeError] = useState(false);
+
+ useEffect(() => {
+ // Check if there's an access code in the URL
+ const code = searchParams.get('access_code');
+ if (code) {
+ setAccessCode(code);
+ }
+ }, [searchParams]);
+
+ const handleAccessCodeSubmit = (code) => {
+ // Update state and reset any previous errors
+ setAccessCode(code);
+ setAccessCodeError(false);
+
+ // Persist access code in URL for bookmarking and page refreshes
+ const newSearchParams = new URLSearchParams(searchParams);
+ newSearchParams.set('access_code', code);
+ setSearchParams(newSearchParams);
+
+ // Explicitly return a Promise for await in PublicRecordingsCard
+ return Promise.resolve();
+ };
+
+ const handleAccessCodeError = () => {
+ setAccessCodeError(true);
+ // Error message remains until the user enters a correct code
+ };
+
return (
-
+
);
diff --git a/app/javascript/components/rooms/room/public_recordings/PublicRecordingsCard.jsx b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsCard.jsx
index ea570c0ee0..4efe82f2ed 100644
--- a/app/javascript/components/rooms/room/public_recordings/PublicRecordingsCard.jsx
+++ b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsCard.jsx
@@ -15,19 +15,87 @@
// with Greenlight; if not, see .
/* eslint-disable consistent-return */
-import React from 'react';
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
import Card from 'react-bootstrap/Card';
import { useParams } from 'react-router-dom';
import PublicRecordingsList from './PublicRecordingsList';
+import AccessCodeForm from './AccessCodeForm';
-export default function PublicRecordingsCard() {
+export default function PublicRecordingsCard({
+ accessCode,
+ onAccessCodeSubmit,
+ onAccessCodeError,
+ accessCodeError
+}) {
const { friendlyId } = useParams();
+ const [requiresAccessCode, setRequiresAccessCode] = useState(false);
+ const [isValidating, setIsValidating] = useState(!!accessCode);
+
+ const handleAccessCodeSubmit = async (code) => {
+ setIsValidating(true);
+
+ try {
+ // Wait until the access code is submitted
+ await onAccessCodeSubmit(code);
+
+ // Check the access code validity immediately after the API response
+ // This is more reliable than using a timeout
+ const checkAccessCodeValidity = () => {
+ if (requiresAccessCode) {
+ // If we still need the access code, the entered code was incorrect
+ onAccessCodeError();
+ }
+ };
+
+ // Use requestAnimationFrame to ensure we check after the state has been updated
+ requestAnimationFrame(checkAccessCodeValidity);
+ } catch (error) {
+ // Keep only essential error logging
+ console.error('Error submitting access code:', error);
+ onAccessCodeError();
+ }
+ };
+
+ // Reset requiresAccessCode when accessCode changes
+ useEffect(() => {
+ if (accessCode) {
+ setRequiresAccessCode(false);
+ setIsValidating(false); // Reset validation state when access code is provided
+ }
+ }, [accessCode]);
return (
-
-
+
+
+
+ {requiresAccessCode || (isValidating && !accessCode) ? (
+
+ ) : (
+ setRequiresAccessCode(true)}
+ />
+ )}
);
}
+
+PublicRecordingsCard.propTypes = {
+ accessCode: PropTypes.string,
+ onAccessCodeSubmit: PropTypes.func.isRequired,
+ onAccessCodeError: PropTypes.func.isRequired,
+ accessCodeError: PropTypes.bool,
+};
+
+PublicRecordingsCard.defaultProps = {
+ accessCode: '',
+ accessCodeError: false,
+};
diff --git a/app/javascript/components/rooms/room/public_recordings/PublicRecordingsList.jsx b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsList.jsx
index eac1767155..340f369316 100644
--- a/app/javascript/components/rooms/room/public_recordings/PublicRecordingsList.jsx
+++ b/app/javascript/components/rooms/room/public_recordings/PublicRecordingsList.jsx
@@ -14,10 +14,10 @@
// You should have received a copy of the GNU Lesser General Public License along
// with Greenlight; if not, see .
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { VideoCameraIcon } from '@heroicons/react/24/outline';
-import { Card, Stack, Table } from 'react-bootstrap';
+import { Card, Stack, Table, Alert } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import SortBy from '../../../shared_components/search/SortBy';
import NoSearchResults from '../../../shared_components/search/NoSearchResults';
@@ -29,11 +29,32 @@ import PublicRecordingsRowPlaceHolder from './PublicRecordingsRowPlaceHolder';
import ButtonLink from '../../../shared_components/utilities/ButtonLink';
import UserBoardIcon from '../../UserBoardIcon';
-export default function PublicRecordingsList({ friendlyId }) {
+export default function PublicRecordingsList({ friendlyId, accessCode, onRequiresAccessCode }) {
const { t } = useTranslation();
const [page, setPage] = useState(1);
const [searchInput, setSearchInput] = useState('');
- const { data: recordings, ...publicRecordingsAPI } = usePublicRecordings({ friendlyId, page, search: searchInput });
+ const [showAccessCodeError, setShowAccessCodeError] = useState(false);
+
+ const { data: recordings, ...publicRecordingsAPI } = usePublicRecordings({
+ friendlyId,
+ page,
+ search: searchInput,
+ accessCode: accessCode || undefined
+ });
+
+ // Check if access code is required
+ useEffect(() => {
+ if (publicRecordingsAPI.isFetched && recordings?.meta?.requires_access_code) {
+ onRequiresAccessCode();
+ // Only show error if we've attempted to submit an access code
+ if (accessCode !== '') {
+ setShowAccessCodeError(true);
+ // Access code is invalid
+ }
+ } else {
+ setShowAccessCodeError(false);
+ }
+ }, [recordings, publicRecordingsAPI.isFetched, onRequiresAccessCode, accessCode]);
if (!publicRecordingsAPI.isLoading && recordings?.data?.length === 0 && !searchInput) {
return (
@@ -70,6 +91,13 @@ export default function PublicRecordingsList({ friendlyId }) {
{t('join_session')}
+
+ {showAccessCodeError && (
+
+ {t('recording.invalid_access_code')}
+
{t('recording.invalid_access_code_try_again')}
+
+ )}
{
(searchInput && recordings?.data.length === 0)
? (
@@ -123,4 +151,10 @@ export default function PublicRecordingsList({ friendlyId }) {
PublicRecordingsList.propTypes = {
friendlyId: PropTypes.string.isRequired,
+ accessCode: PropTypes.string,
+ onRequiresAccessCode: PropTypes.func.isRequired,
+};
+
+PublicRecordingsList.defaultProps = {
+ accessCode: '',
};
diff --git a/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx b/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx
index 9d8d9285f0..a736b1015f 100644
--- a/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx
+++ b/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx
@@ -71,6 +71,13 @@ export default function RoomSettings() {
config={roomConfigs?.glModeratorAccessCode}
description={t('room.settings.generate_mods_access_code')}
/>
+
{serverTags && Object.keys(serverTags).length !== 0 && (
axios.get(`/rooms/${friendlyId}/public_recordings.json`, { params }).then((resp) => resp.data),
+ ['getPublicRecordings', friendlyId, { ...params }],
+ () => {
+ return axios.get(`/rooms/${friendlyId}/public_recordings.json`, { params })
+ .then((resp) => {
+ return resp.data;
+ })
+ .catch((error) => {
+ // Only keep console.error for important errors
+ console.error('usePublicRecordings: API error:', error);
+ throw error;
+ });
+ },
{
keepPreviousData: true,
+ retry: false, // Don't retry on 401/403 errors
+ staleTime: 60000, // Data remains fresh for 1 minute
+ cacheTime: 300000, // Cache data for 5 minutes
+ refetchOnWindowFocus: false, // Don't refetch when window regains focus
+ // Only refetch if access code changes to avoid unnecessary requests
+ refetchOnMount: accessCode ? true : 'stale',
},
);
}
diff --git a/db/migrate/20250514211900_add_recordings_access_code.rb b/db/migrate/20250514211900_add_recordings_access_code.rb
new file mode 100644
index 0000000000..ac108b1126
--- /dev/null
+++ b/db/migrate/20250514211900_add_recordings_access_code.rb
@@ -0,0 +1,56 @@
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with Greenlight; if not, see .
+
+# frozen_string_literal: true
+
+class AddRecordingsAccessCode < ActiveRecord::Migration[7.0]
+ def up
+ # 1. Create the MeetingOption for glRecordingsAccessCode
+ meeting_option = MeetingOption.create!(name: 'glRecordingsAccessCode', default_value: '')
+
+ # 2. Add the option to the RoomsConfigurations
+ provider_name = 'greenlight'
+
+ # Check if the entry already exists
+ unless RoomsConfiguration.exists?(provider: provider_name, meeting_option_id: meeting_option.id)
+ RoomsConfiguration.create!(
+ provider: provider_name,
+ meeting_option_id: meeting_option.id,
+ value: 'optional' # optional means that the setting can be changed by the user
+ )
+ end
+ end
+
+ def down
+ # Find the MeetingOption for glRecordingsAccessCode
+ meeting_option = MeetingOption.find_by(name: 'glRecordingsAccessCode')
+
+ if meeting_option
+ provider_name = 'greenlight'
+
+ # Delete all room settings for this option
+ RoomMeetingOption.joins(:meeting_option)
+ .where(meeting_options: { name: 'glRecordingsAccessCode' })
+ .destroy_all
+
+ # Remove from RoomsConfigurations
+ RoomsConfiguration.where(provider: provider_name, meeting_option_id: meeting_option.id).destroy_all
+
+ # Delete the MeetingOption
+ meeting_option.destroy
+ end
+ end
+end