diff --git a/app/assets/locales/en.json b/app/assets/locales/en.json
index 60a29c0d66..c701b5cb7c 100644
--- a/app/assets/locales/en.json
+++ b/app/assets/locales/en.json
@@ -164,6 +164,10 @@
"wrong_access_code": "Wrong Access Code",
"generate_viewers_access_code": "Generate access code for viewers",
"generate_mods_access_code": "Generate access code for moderators",
+ "server_tag": "Select a server type for this room",
+ "default_tag_name": "Default",
+ "server_tag_desired": "Desired",
+ "server_tag_required": "Required",
"are_you_sure_delete_room": "Are you sure you want to delete this room?"
}
},
@@ -274,8 +278,8 @@
"administration": {
"administration": "Administration",
"terms": "Terms & Conditions",
- "privacy": "Privacy Policy",
- "privacy_policy": "Privacy Policy",
+ "privacy": "Privacy Notice",
+ "privacy_policy": "Privacy Notice",
"change_term_links": "Change the terms links that appears at the bottom of the page",
"change_privacy_link": "Change the privacy link that appears at the bottom of the page",
"helpcenter": "Help Center",
@@ -413,7 +417,7 @@
"brand_color_updated": "The brand color has been updated.",
"brand_image_updated": "The brand image has been updated.",
"brand_image_deleted": "The brand image has been deleted.",
- "privacy_policy_updated": "The privacy policy has been updated.",
+ "privacy_policy_updated": "The privacy notice has been updated.",
"helpcenter_updated": "The help center link has been updated.",
"terms_of_service_updated": "The terms of service have been updated.",
"maintenance_updated": "The maintenance banner has been updated."
@@ -437,6 +441,7 @@
},
"error": {
"problem_completing_action": "The action can't be completed. \n Please try again.",
+ "server_type_unavailable": "The required server type is unavailable. Please select a different type in the room settings.",
"file_type_not_supported": "The file type is not supported.",
"file_size_too_large": "The file size is too large.",
"file_upload_error": "The file can't be uploaded.",
@@ -533,6 +538,11 @@
},
"url": {
"invalid": "Invalid URL"
+ },
+ "text_form": {
+ "value": {
+ "required": "Please enter some message"
+ }
}
},
"room": {
diff --git a/app/controllers/api/v1/locales_controller.rb b/app/controllers/api/v1/locales_controller.rb
index 911f2d73af..f86af3b2eb 100644
--- a/app/controllers/api/v1/locales_controller.rb
+++ b/app/controllers/api/v1/locales_controller.rb
@@ -48,11 +48,13 @@ def index
# Returns the requested language's locale strings (returns 406 if locale doesn't exist)
def show
language = params[:name].tr('-', '_')
+ language_file = Dir.entries('app/assets/locales').select { |f| f.starts_with?(language) }
+ final_language = language_file.min&.gsub('.json', '')
# Serve locales files directly in development (not through asset pipeline)
- return render file: Rails.root.join('app', 'assets', 'locales', "#{language}.json") if Rails.env.development?
+ return render file: Rails.root.join('app', 'assets', 'locales', "#{final_language}.json") if Rails.env.development?
- redirect_to ActionController::Base.helpers.asset_path("#{language}.json")
+ redirect_to ActionController::Base.helpers.asset_path("#{final_language}.json")
rescue StandardError
head :not_acceptable
end
diff --git a/app/controllers/api/v1/meetings_controller.rb b/app/controllers/api/v1/meetings_controller.rb
index be51924d4b..f0a68b5819 100644
--- a/app/controllers/api/v1/meetings_controller.rb
+++ b/app/controllers/api/v1/meetings_controller.rb
@@ -31,7 +31,7 @@ def start
begin
MeetingStarter.new(room: @room, base_url: request.base_url, current_user:, provider: current_provider).call
rescue BigBlueButton::BigBlueButtonException => e
- return render_error status: :bad_request unless e.key == 'idNotUnique'
+ return render_error status: :bad_request, errors: e.key unless e.key == 'idNotUnique'
end
render_data data: BigBlueButtonApi.new(provider: current_provider).join_meeting(
diff --git a/app/controllers/api/v1/server_tags_controller.rb b/app/controllers/api/v1/server_tags_controller.rb
new file mode 100644
index 0000000000..8692a657f4
--- /dev/null
+++ b/app/controllers/api/v1/server_tags_controller.rb
@@ -0,0 +1,37 @@
+# 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
+
+module Api
+ module V1
+ class ServerTagsController < ApiController
+ # GET /api/v1/server_tags/:friendly_id
+ # Returns a list of all allowed tags&names for the room's owner
+ def show
+ tag_names = Rails.configuration.server_tag_names
+ tag_roles = Rails.configuration.server_tag_roles
+ return render_data data: {}, status: :ok if tag_names.blank?
+
+ room = Room.find_by(friendly_id: params[:friendly_id])
+ return render_data data: {}, status: :ok if room.nil?
+
+ allowed_tag_names = tag_names.reject { |tag, _| tag_roles.key?(tag) && tag_roles[tag].exclude?(room.user.role_id) }
+ render_data data: allowed_tag_names, status: :ok
+ end
+ end
+ end
+end
diff --git a/app/javascript/components/admin/site_settings/administration/TextForm.jsx b/app/javascript/components/admin/site_settings/administration/TextForm.jsx
index ff2567fb2a..4e17e8e51c 100644
--- a/app/javascript/components/admin/site_settings/administration/TextForm.jsx
+++ b/app/javascript/components/admin/site_settings/administration/TextForm.jsx
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Lesser General Public License along
// with Greenlight; if not, see .
-import React, { useEffect } from 'react';
+import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
@@ -25,39 +25,80 @@ import FormControl from '../../../shared_components/forms/FormControl';
import useTextForm from '../../../../hooks/forms/admin/site_settings/useTextForm';
export default function TextForm({ id, value, mutation: useUpdateSiteSettingsAPI }) {
- const updateSiteSettingsAPI = useUpdateSiteSettingsAPI();
+ const updateSiteSettingsAPISetText = useUpdateSiteSettingsAPI();
+ const updateSiteSettingsAPIClearText = useUpdateSiteSettingsAPI();
+
const { t } = useTranslation();
- const maintenanceBannerId = localStorage.getItem('maintenanceBannerId');
const { methods, fields } = useTextForm({ defaultValues: { value } });
+ const formText = useRef('');
+
useEffect(() => {
- if (!methods) { return; }
- methods.reset({ value });
+ if (methods) {
+ methods.reset({ value });
+ formText.current = value;
+ }
}, [methods, value]);
+ const dismissMaintenanceBannerToast = () => {
+ const maintenanceBannerId = localStorage.getItem('maintenanceBannerId');
+ if (maintenanceBannerId) {
+ toast.dismiss(maintenanceBannerId);
+ localStorage.removeItem('maintenanceBannerId');
+ }
+ };
+
// Function to clear the form
const clearForm = () => {
methods.reset({ value: '' });
- toast.dismiss(maintenanceBannerId);
- updateSiteSettingsAPI.mutate('');
+ dismissMaintenanceBannerToast();
+ if (formText.current) {
+ formText.current = '';
+ updateSiteSettingsAPIClearText.mutate('');
+ }
};
+ const handleSubmit = useCallback((formData) => {
+ if (formText.current !== formData[`${fields.value.hookForm.id}`]) {
+ dismissMaintenanceBannerToast();
+ formText.current = formData[`${fields.value.hookForm.id}`];
+ return updateSiteSettingsAPISetText.mutate(formData);
+ }
+ return null;
+ }, [updateSiteSettingsAPISetText.mutate]);
+
return (
-
);
diff --git a/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx b/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx
index 159f8ed303..973ca6afc0 100644
--- a/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx
+++ b/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx
@@ -33,6 +33,8 @@ import { useAuth } from '../../../../contexts/auth/AuthProvider';
import UpdateRoomNameForm from './forms/UpdateRoomNameForm';
import useRoom from '../../../../hooks/queries/rooms/useRoom';
import UnshareRoom from './UnshareRoom';
+import useServerTags from '../../../../hooks/queries/rooms/useServerTags';
+import ServerTagRow from './ServerTagRow';
export default function RoomSettings() {
const { t } = useTranslation();
@@ -41,6 +43,7 @@ export default function RoomSettings() {
const roomSetting = useRoomSettings(friendlyId);
const { data: roomConfigs } = useRoomConfigs();
const { data: room } = useRoom(friendlyId);
+ const { data: serverTags } = useServerTags(friendlyId);
const updateMutationWrapper = () => useUpdateRoomSetting(friendlyId);
const deleteMutationWrapper = (args) => useDeleteRoom({ friendlyId, ...args });
@@ -66,6 +69,15 @@ export default function RoomSettings() {
config={roomConfigs?.glModeratorAccessCode}
description={t('room.settings.generate_mods_access_code')}
/>
+ {serverTags && Object.keys(serverTags).length !== 0 && (
+
+ )}
{ t('room.settings.user_settings') }
diff --git a/app/javascript/components/rooms/room/room_settings/ServerTagRow.jsx b/app/javascript/components/rooms/room/room_settings/ServerTagRow.jsx
new file mode 100644
index 0000000000..044df49710
--- /dev/null
+++ b/app/javascript/components/rooms/room/room_settings/ServerTagRow.jsx
@@ -0,0 +1,118 @@
+// 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 .
+
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import {
+ Row, Col, Dropdown, ButtonGroup, ToggleButton,
+} from 'react-bootstrap';
+import SimpleSelect from '../../../shared_components/utilities/SimpleSelect';
+
+export default function ServerTagRow({
+ updateMutation: useUpdateAPI, currentTag, tagRequired, serverTags, description,
+}) {
+ const updateAPI = useUpdateAPI();
+ const { t } = useTranslation();
+
+ function getDefaultTagName() {
+ return t('room.settings.default_tag_name');
+ }
+
+ function getTagName(tag) {
+ if (tag in serverTags) {
+ return serverTags[tag];
+ }
+ return getDefaultTagName();
+ }
+
+ const dropdownTags = Object.entries(serverTags).map(([tagString, tagName]) => (
+ (
+ updateAPI.mutate({ settingName: 'serverTag', settingValue: tagString })}
+ >
+ {tagName}
+
+ )
+ ));
+
+ return (
+
+ {description}
+
+
+ {[
+ updateAPI.mutate({ settingName: 'serverTag', settingValue: '' })}
+ >
+ {getDefaultTagName()}
+ ,
+ ].concat(dropdownTags)}
+
+
+
+
+ {
+ updateAPI.mutate({ settingName: 'serverTagRequired', settingValue: false });
+ }}
+ >
+ {t('room.settings.server_tag_desired')}
+
+ {
+ updateAPI.mutate({ settingName: 'serverTagRequired', settingValue: true });
+ }}
+ >
+ {t('room.settings.server_tag_required')}
+
+
+
+
+ );
+}
+
+ServerTagRow.defaultProps = {
+ currentTag: '',
+ tagRequired: false,
+};
+
+ServerTagRow.propTypes = {
+ updateMutation: PropTypes.func.isRequired,
+ currentTag: PropTypes.string,
+ tagRequired: PropTypes.bool,
+ serverTags: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
+ description: PropTypes.string.isRequired,
+};
diff --git a/app/javascript/components/shared_components/Footer.jsx b/app/javascript/components/shared_components/Footer.jsx
index 30a3b5b2b4..2b30506e3c 100644
--- a/app/javascript/components/shared_components/Footer.jsx
+++ b/app/javascript/components/shared_components/Footer.jsx
@@ -19,17 +19,20 @@ import { useTranslation } from 'react-i18next';
import { Container } from 'react-bootstrap';
import useEnv from '../../hooks/queries/env/useEnv';
import useSiteSetting from '../../hooks/queries/site_settings/useSiteSetting';
+import { useAuth } from '../../contexts/auth/AuthProvider';
export default function Footer() {
const { t } = useTranslation();
const { data: env } = useEnv();
const { data: links } = useSiteSetting(['Terms', 'PrivacyPolicy']);
+ const currentUser = useAuth();
+ const isAdmin = currentUser && currentUser.role && currentUser?.role.name === 'Administrator';
return (