diff --git a/README.md b/README.md index debb4230..cb4ec06f 100644 --- a/README.md +++ b/README.md @@ -77,5 +77,8 @@ can be found here: [config.example.js](public/config/config.example.js). | logo | If not null, it shows the logo loaded from the specified URL, otherwise it shows the title. | `"url"` | ``"images/logo.edumeet.svg"`` | | title | The title to show if the logo is not specified. | `"string"` | ``"edumeet"`` | | randomizeOnBlank | Enable or disable randomize room name when it is blank. | `"boolean"` | ``true`` | +| transcriptionEnabled | Enable or disable transcription. | `"boolean"` | ``true`` | +| imprintUrl | Show a link to an imprint in the edumeet UI, keep blank to not show a link | `"string"` | ``""`` | +| privacyUrl | Show a link to a privacy notice in the edumeet UI, keep blank to not show a link | `"string"` | ``""`` | --- diff --git a/public/config/config.example.js b/public/config/config.example.js index 8931c694..132d2132 100644 --- a/public/config/config.example.js +++ b/public/config/config.example.js @@ -146,6 +146,15 @@ var config = { // otherwise, it will remain empty and users will have to enter a room name. randomizeOnBlank: true, + // Enable or disable transcription. + transcriptionEnabled: true, + + // Imprint. If you want to link your imprint, please provide a URL in this variable. If it is empty, no link will be shown. + imprintUrl: '', + + // Privacy notice. If you want to link your privacy notices, please provide a URL in this variable. If it is empty, no link will be shown. + privacyUrl: '', + // Client theme. Take a look at mui theme documentation. theme: { palette: { diff --git a/src/App.tsx b/src/App.tsx index 481c90cc..ced67581 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -69,6 +69,9 @@ const App = (): JSX.Element => { if (roomState ==='left') { dispatch(roomActions.setState('new')); navigate('/'); + setTimeout(() => { + window.location.reload(); + }, 0); } }, [ roomState ]); diff --git a/src/components/controlbuttons/ImpressumButton.tsx b/src/components/controlbuttons/ImpressumButton.tsx deleted file mode 100644 index c9020051..00000000 --- a/src/components/controlbuttons/ImpressumButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import InfoIcon from '@mui/icons-material/Info'; -import React from 'react'; -import { Box, Button } from '@mui/material'; -import { styled } from '@mui/material/styles'; -import edumeetConfig from '../../utils/edumeetConfig'; - -const ImpressumContainer = styled(Box)(({ theme }) => ({ - position: 'absolute', - bottom: theme.spacing(1.5), - left: theme.spacing(1.5), - color: 'white', -})); - -const impressumUrl = edumeetConfig.impressumUrl; - -const ImpressumButton: React.FC = () => { - return ( -
- - - -
- ); -}; - -export default ImpressumButton; \ No newline at end of file diff --git a/src/components/helpdialog/HelpDialog.tsx b/src/components/helpdialog/HelpDialog.tsx index cf3dde71..b878d61f 100644 --- a/src/components/helpdialog/HelpDialog.tsx +++ b/src/components/helpdialog/HelpDialog.tsx @@ -1,11 +1,11 @@ -import { Button } from '@mui/material'; +import { Button, Box, Link, Typography } from '@mui/material'; import { Close } from '@mui/icons-material'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { uiActions } from '../../store/slices/uiSlice'; -import { closeLabel } from '../translated/translatedComponents'; +import { closeLabel, imprintLabel, privacyLabel } from '../translated/translatedComponents'; import ShortcutKeys from './ShortcutKeys'; import GenericDialog from '../genericdialog/GenericDialog'; -import ImpressumButton from '../controlbuttons/ImpressumButton'; +import edumeetConfig from '../../utils/edumeetConfig'; const HelpDialog = (): JSX.Element => { const dispatch = useAppDispatch(); @@ -17,24 +17,41 @@ const HelpDialog = (): JSX.Element => { })); }; + const privacyUrl = edumeetConfig.privacyUrl ?? ''; + const imprintUrl = edumeetConfig.imprintUrl ?? ''; + return ( } + content={ <> } actions={ - + + + {imprintUrl.trim() !== '' && ( + + { imprintLabel() } + + )} + {privacyUrl.trim() !== '' && ( + + { privacyLabel() } + + )} + + + } /> ); }; -export default HelpDialog; \ No newline at end of file +export default HelpDialog; diff --git a/src/components/translated/translatedComponents.tsx b/src/components/translated/translatedComponents.tsx index 74d58f0f..3df87d8d 100644 --- a/src/components/translated/translatedComponents.tsx +++ b/src/components/translated/translatedComponents.tsx @@ -805,4 +805,13 @@ export const managementExtraSettingsLabel = (): string => intl.formatMessage({ export const ruleSettingsLabel = (): string => intl.formatMessage({ id: 'label.managementRuleSettings', defaultMessage: 'Rule settings' + +export const imprintLabel = (): string => intl.formatMessage({ + id: 'label.imprint', + defaultMessage: 'Imprint' +}); + +export const privacyLabel = (): string => intl.formatMessage({ + id: 'label.privacy', + defaultMessage: 'Privacy' }); diff --git a/src/components/volume/MicVolume.tsx b/src/components/volume/MicVolume.tsx new file mode 100644 index 00000000..b02a27c1 --- /dev/null +++ b/src/components/volume/MicVolume.tsx @@ -0,0 +1,46 @@ +import LinearProgress from '@mui/material/LinearProgress'; +import { useContext, useEffect, useState } from 'react'; +import { ServiceContext } from '../../store/store'; +import { VolumeWatcher } from '../../utils/volumeWatcher'; +import hark from 'hark'; + +const MicVolume = (): JSX.Element => { + const { mediaService } = useContext(ServiceContext); + const [ volumeLevel, setVolume ] = useState(0); + + useEffect(() => { + let volumeWatcher: VolumeWatcher | undefined; + + // Add hark for mic + if (mediaService.previewMicTrack) { + const harkStream = new MediaStream(); + + harkStream.addTrack(mediaService.previewMicTrack.clone()); + + const micHark = hark(harkStream, { + play: false, + interval: 100, + threshold: -60, + history: 100 + }); + + volumeWatcher = new VolumeWatcher({ hark: micHark }); + } + + const onVolumeChange = ({ scaledVolume }: { scaledVolume: number }): void => { + setVolume(scaledVolume*10); // Range: 0-100 + }; + + volumeWatcher?.on('volumeChange', onVolumeChange); + + return () => { + volumeWatcher?.off('volumeChange', onVolumeChange); + }; + }, [ mediaService.previewMicTrack ]); + + return ( + + ); +}; + +export default MicVolume; diff --git a/src/services/mediaService.tsx b/src/services/mediaService.tsx index 39fa7913..315dee44 100644 --- a/src/services/mediaService.tsx +++ b/src/services/mediaService.tsx @@ -17,6 +17,7 @@ import { ProducerSource } from '../utils/types'; import { MediaSender } from '../utils/mediaSender'; import type { ClientMonitor } from '@observertc/client-monitor-js'; import { Logger } from '../utils/Logger'; +import edumeetConfig from '../utils/edumeetConfig'; const logger = new Logger('MediaService'); @@ -665,7 +666,7 @@ export class MediaService extends EventEmitter { get localCapabilities(): LocalCapabilities { return { canRecord: Boolean(MediaRecorder), - canTranscribe: Boolean(window.webkitSpeechRecognition), + canTranscribe: Boolean(window.webkitSpeechRecognition) && edumeetConfig.transcriptionEnabled, }; } diff --git a/src/translations/de.json b/src/translations/de.json index f48e7735..7296e166 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -59,6 +59,7 @@ "label.fullscreen": "Vollbild", "label.guest": null, "label.high": "Hoch (HD)", + "label.imprint": "Impressum", "label.italic": "kursiv", "label.join": "Eintreten", "label.joinBreakoutRoom": null, @@ -77,6 +78,7 @@ "label.openSettings": null, "label.participants": "Teilnehmer", "label.password": "Passwort", + "label.privacy": "Datenschutz", "label.promoteAllPeers": "Alle Teilnehmer reinlassen", "label.recordingInProgress": "Aufnahme läuft...", "label.removeBreakoutRoom": null, diff --git a/src/utils/types.tsx b/src/utils/types.tsx index 0ab2ef4c..fab7e8e5 100644 --- a/src/utils/types.tsx +++ b/src/utils/types.tsx @@ -3,7 +3,6 @@ import { TFLite } from '../services/effectsService'; export const defaultEdumeetConfig: EdumeetConfig = { managementUrl: undefined, - impressumUrl: '/privacy/privacy.html', p2penabled: false, loginEnabled: false, developmentPort: 8443, @@ -78,6 +77,7 @@ export const defaultEdumeetConfig: EdumeetConfig = { }, title: 'edumeet', randomizeOnBlank: true, + transcriptionEnabled: true, theme: { background: 'linear-gradient(135deg, rgba(1,42,74,1) 0%, rgba(1,58,99,1) 50%, rgba(1,73,124,1) 100%)', appBarColor: 'rgba(0, 0, 0, 0.4)', @@ -91,12 +91,13 @@ export const defaultEdumeetConfig: EdumeetConfig = { sideContentItemDarkColor: 'rgba(150, 150, 150, 0.4)', sideContainerBackgroundColor: 'rgba(255, 255, 255, 0.7)', }, - reduxLoggingEnabled: false + reduxLoggingEnabled: false, + imprintUrl: '', + privacyUrl: '' }; export interface EdumeetConfig { managementUrl?: string; - impressumUrl: string; p2penabled: boolean; loginEnabled: boolean; developmentPort: number; @@ -129,8 +130,11 @@ export interface EdumeetConfig { notificationSounds: Record; title: string; randomizeOnBlank: boolean; + transcriptionEnabled: boolean; theme: ThemeOptions; reduxLoggingEnabled: boolean; + imprintUrl: string; + privacyUrl: string; } export interface HTMLMediaElementWithSink extends HTMLMediaElement { diff --git a/src/views/join/Join.tsx b/src/views/join/Join.tsx index 535b13ce..8f81def9 100644 --- a/src/views/join/Join.tsx +++ b/src/views/join/Join.tsx @@ -1,8 +1,8 @@ import { useEffect } from 'react'; -import { Button } from '@mui/material'; +import { Button, Box, Link, Typography } from '@mui/material'; import TextInputField from '../../components/textinputfield/TextInputField'; import { useAppDispatch, useAppSelector, useNotifier } from '../../store/hooks'; -import { joinLabel, yourNameLabel } from '../../components/translated/translatedComponents'; +import { joinLabel, yourNameLabel, imprintLabel, privacyLabel } from '../../components/translated/translatedComponents'; import { AccountCircle } from '@mui/icons-material'; import MediaPreview from '../../components/mediapreview/MediaPreview'; import AudioInputChooser from '../../components/devicechooser/AudioInputChooser'; @@ -18,7 +18,10 @@ import { meActions } from '../../store/slices/meSlice'; import AudioOutputChooser from '../../components/devicechooser/AudioOutputChooser'; import { canSelectAudioOutput } from '../../store/selectors'; import TestAudioOutputButton from '../../components/audiooutputtest/AudioOutputTest'; +import edumeetConfig from '../../utils/edumeetConfig'; import ImpressumButton from '../../components/controlbuttons/ImpressumButton'; +import MicVolume from '../../components/volume/MicVolume'; + interface JoinProps { roomId: string; @@ -61,6 +64,9 @@ const Join = ({ roomId }: JoinProps): React.JSX.Element => { } }, []); + const privacyUrl = edumeetConfig.privacyUrl ?? ''; + const imprintUrl = edumeetConfig.imprintUrl ?? ''; + return ( } @@ -70,6 +76,7 @@ const Join = ({ roomId }: JoinProps): React.JSX.Element => { { showAudioOutputChooser && } + @@ -85,15 +92,28 @@ const Join = ({ roomId }: JoinProps): React.JSX.Element => { } actions={ - <> - + + + {imprintUrl.trim() !== '' && ( + + { imprintLabel() } + + )} + {privacyUrl.trim() !== '' && ( + + { privacyLabel() } + + )} + + + } /> ); diff --git a/src/views/landingpage/LandingPage.tsx b/src/views/landingpage/LandingPage.tsx index 52467bf0..3882d159 100644 --- a/src/views/landingpage/LandingPage.tsx +++ b/src/views/landingpage/LandingPage.tsx @@ -1,14 +1,13 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Button, Container } from '@mui/material'; +import { Button, Container, Box, Link, Typography } from '@mui/material'; import randomString from 'random-string'; import TextInputField from '../../components/textinputfield/TextInputField'; -import { joinLabel, roomNameLabel } from '../../components/translated/translatedComponents'; +import { joinLabel, roomNameLabel, imprintLabel, privacyLabel } from '../../components/translated/translatedComponents'; import GenericDialog from '../../components/genericdialog/GenericDialog'; import StyledBackground from '../../components/StyledBackground'; import PrecallTitle from '../../components/precalltitle/PrecallTitle'; import { QRCode } from 'react-qrcode-logo'; -import ImpressumButton from '../../components/controlbuttons/ImpressumButton'; import edumeetConfig from '../../utils/edumeetConfig'; const LandingPage = (): JSX.Element => { @@ -17,6 +16,9 @@ const LandingPage = (): JSX.Element => { const [ roomId, setRoomId ] = useState(randomizeOnBlank ? randomString({ length: 8 }).toLowerCase() : ''); const onClicked = () => navigate(`/${roomId}`); + const privacyUrl = edumeetConfig.privacyUrl ?? ''; + const imprintUrl = edumeetConfig.imprintUrl ?? ''; + return ( { } actions={ - <> + + + {imprintUrl.trim() !== '' && ( + + { imprintLabel() } + + )} + {privacyUrl.trim() !== '' && ( + + { privacyLabel() } + + )} + + + } /> @@ -52,4 +67,4 @@ const LandingPage = (): JSX.Element => { ); }; -export default LandingPage; \ No newline at end of file +export default LandingPage; diff --git a/src/views/lobby/Lobby.tsx b/src/views/lobby/Lobby.tsx index a4a1a038..21bcfa1e 100644 --- a/src/views/lobby/Lobby.tsx +++ b/src/views/lobby/Lobby.tsx @@ -1,11 +1,12 @@ import { AccountCircle } from '@mui/icons-material'; +import { Box, Link, Typography } from '@mui/material'; import { useState } from 'react'; import AudioInputChooser from '../../components/devicechooser/AudioInputChooser'; import VideoInputChooser from '../../components/devicechooser/VideoInputChooser'; import MediaPreview from '../../components/mediapreview/MediaPreview'; import GenericDialog from '../../components/genericdialog/GenericDialog'; import TextInputField from '../../components/textinputfield/TextInputField'; -import { roomLockedLabel, yourNameLabel } from '../../components/translated/translatedComponents'; +import { roomLockedLabel, yourNameLabel, imprintLabel, privacyLabel } from '../../components/translated/translatedComponents'; import { setDisplayName } from '../../store/actions/meActions'; import { useAppDispatch, @@ -17,6 +18,7 @@ import { ChooserDiv } from '../../components/devicechooser/DeviceChooser'; import AudioOutputChooser from '../../components/devicechooser/AudioOutputChooser'; import { canSelectAudioOutput } from '../../store/selectors'; import TestAudioOutputButton from '../../components/audiooutputtest/AudioOutputTest'; +import edumeetConfig from '../../utils/edumeetConfig'; const Lobby = (): React.JSX.Element => { useNotifier(); @@ -32,6 +34,9 @@ const Lobby = (): React.JSX.Element => { )); }; + const privacyUrl = edumeetConfig.privacyUrl ?? ''; + const imprintUrl = edumeetConfig.imprintUrl ?? ''; + return ( } @@ -53,9 +58,25 @@ const Lobby = (): React.JSX.Element => { } - actions={roomLockedLabel()} + actions={ + + {roomLockedLabel()} + + {imprintUrl.trim() !== '' && ( + + { imprintLabel() } + + )} + {privacyUrl.trim() !== '' && ( + + { privacyLabel() } + + )} + + + } /> ); }; -export default Lobby; \ No newline at end of file +export default Lobby; diff --git a/src/views/unsupported/UnsupportedBrowser.tsx b/src/views/unsupported/UnsupportedBrowser.tsx index 00ef8cc2..33ac9fca 100644 --- a/src/views/unsupported/UnsupportedBrowser.tsx +++ b/src/views/unsupported/UnsupportedBrowser.tsx @@ -8,13 +8,17 @@ import { List, ListItem, ListItemAvatar, - ListItemText + ListItemText, + Link, + Typography } from '@mui/material'; import WebAssetIcon from '@mui/icons-material/WebAsset'; import ErrorIcon from '@mui/icons-material/Error'; import { FormattedMessage } from 'react-intl'; import { memo } from 'react'; import { RawStyledDialog } from '../../components/genericdialog/GenericDialog'; +import { imprintLabel, privacyLabel } from '../../components/translated/translatedComponents'; +import edumeetConfig from '../../utils/edumeetConfig'; interface UnsupportedBrowserProps { platform: string; @@ -39,6 +43,9 @@ const UnsupportedBrowser = ({ if (platform !== 'desktop') dense = true; + const privacyUrl = edumeetConfig.privacyUrl ?? ''; + const imprintUrl = edumeetConfig.imprintUrl ?? ''; + return ( ); }; -export default memo(UnsupportedBrowser); \ No newline at end of file +export default memo(UnsupportedBrowser);