From 7660171aff27c5ab59cb68eaf67496393b33e8af Mon Sep 17 00:00:00 2001 From: Jan Kessler Date: Tue, 16 May 2023 07:06:55 +0200 Subject: [PATCH 001/512] set bbb-rap-resque-worker niceness to 19 --- record-and-playback/core/systemd/bbb-rap-resque-worker.service | 1 + 1 file changed, 1 insertion(+) diff --git a/record-and-playback/core/systemd/bbb-rap-resque-worker.service b/record-and-playback/core/systemd/bbb-rap-resque-worker.service index a3debec52c..9192426084 100644 --- a/record-and-playback/core/systemd/bbb-rap-resque-worker.service +++ b/record-and-playback/core/systemd/bbb-rap-resque-worker.service @@ -14,6 +14,7 @@ Environment=COUNT=1 User=bigbluebutton Restart=always RestartSec=3 +Nice=19 [Install] WantedBy=multi-user.target bigbluebutton.target From 13ddb913984227739dc0fe7bbfdec283ec39fb2b Mon Sep 17 00:00:00 2001 From: Jan Kessler Date: Tue, 16 May 2023 07:09:26 +0200 Subject: [PATCH 002/512] set bbb-rap-caption-inbox niceness to 19 --- record-and-playback/core/systemd/bbb-rap-caption-inbox.service | 1 + 1 file changed, 1 insertion(+) diff --git a/record-and-playback/core/systemd/bbb-rap-caption-inbox.service b/record-and-playback/core/systemd/bbb-rap-caption-inbox.service index 9f0a0d9a47..d4c30819e4 100644 --- a/record-and-playback/core/systemd/bbb-rap-caption-inbox.service +++ b/record-and-playback/core/systemd/bbb-rap-caption-inbox.service @@ -9,6 +9,7 @@ WorkingDirectory=/usr/local/bigbluebutton/core User=bigbluebutton Slice=bbb_record_core.slice Restart=on-failure +Nice=19 [Install] WantedBy=multi-user.target bigbluebutton.target From aa27e8be68bccfbe5c9062fd4c4cd5ef37547466 Mon Sep 17 00:00:00 2001 From: Tainan Felipe Date: Thu, 21 Sep 2023 10:48:00 -0300 Subject: [PATCH 003/512] Refactor: migrate audio captions to TS + Graphql --- bigbluebutton-html5/.eslintrc.js | 4 +- .../audio-captions/button/component.tsx | 294 ++++++++++++++++++ .../audio-captions/button/queries.ts | 23 ++ .../audio-captions/button/styles.ts | 62 ++++ .../audio-captions/captions/component.tsx | 153 +++++++++ .../audio-captions/live/component.tsx | 108 +++++++ .../audio-captions/live/queries.ts | 34 ++ .../audio-captions/live/styles.ts | 139 +++++++++ .../audio-graphql/audio-captions/service.ts | 55 ++++ .../audio-captions/speech/component.tsx | 170 ++++++++++ .../audio-captions/speech/service.ts | 121 +++++++ .../audio/captions/button/container.jsx | 5 +- .../audio/captions/live/container.jsx | 5 +- .../audio/captions/select/container.jsx | 5 +- .../audio/captions/speech/container.jsx | 5 +- .../queries/currentUserSubscription.ts | 6 +- .../local-states/useAudioCaptionEnable.ts | 7 + .../ui/services/audio-manager/index.js | 2 +- 18 files changed, 1190 insertions(+), 8 deletions(-) create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/component.tsx create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/queries.ts create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/styles.ts create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/captions/component.tsx create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/component.tsx create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/queries.ts create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/styles.ts create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/service.ts create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/speech/component.tsx create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/speech/service.ts create mode 100644 bigbluebutton-html5/imports/ui/core/local-states/useAudioCaptionEnable.ts diff --git a/bigbluebutton-html5/.eslintrc.js b/bigbluebutton-html5/.eslintrc.js index c0fc280715..ceb6090379 100644 --- a/bigbluebutton-html5/.eslintrc.js +++ b/bigbluebutton-html5/.eslintrc.js @@ -28,10 +28,12 @@ module.exports = { overrides: [ { files: ['*.ts', '*.tsx'], - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'airbnb'], + extends: ['eslint:recommended', 'airbnb', 'plugin:@typescript-eslint/recommended'], parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], rules: { + '@typescript-eslint/ban-ts-comment': 'off', + camelcase: 'off', 'no-use-before-define': 'off', 'import/no-absolute-path': 0, 'import/no-unresolved': 0, diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/component.tsx b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/component.tsx new file mode 100644 index 0000000000..f4c37699e8 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/component.tsx @@ -0,0 +1,294 @@ +import React, { useEffect, useRef } from 'react'; +import { useSubscription } from '@apollo/client'; +import { layoutSelect } from '/imports/ui/components/layout/context'; +import { Layout } from '/imports/ui/components/layout/layoutTypes'; +import { useCurrentUser } from '/imports/ui/core/hooks/useCurrentUser'; +import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji'; +import BBBMenu from '/imports/ui/components/common/menu/component'; +import Styled from './styles'; +import { getSpeechVoices, setAudioCaptions, setSpeechLocale } from '../service'; +import { GET_AUDIO_CAPTIONS_COUNT, GetAudioCaptionsCountResponse } from './queries'; +import { defineMessages, useIntl } from 'react-intl'; +import { MenuSeparatorItemType, MenuOptionItemType } from '/imports/ui/components/common/menu/menuTypes'; +import useAudioCaptionEnable from '/imports/ui/core/local-states/useAudioCaptionEnable'; +import logger from '/imports/startup/client/logger'; + +const intlMessages = defineMessages({ + start: { + id: 'app.audio.captions.button.start', + description: 'Start audio captions', + }, + stop: { + id: 'app.audio.captions.button.stop', + description: 'Stop audio captions', + }, + transcriptionSettings: { + id: 'app.audio.captions.button.transcriptionSettings', + description: 'Audio captions settings modal', + }, + transcription: { + id: 'app.audio.captions.button.transcription', + description: 'Audio speech transcription label', + }, + transcriptionOn: { + id: 'app.switch.onLabel', + }, + transcriptionOff: { + id: 'app.switch.offLabel', + }, + language: { + id: 'app.audio.captions.button.language', + description: 'Audio speech recognition language label', + }, + 'de-DE': { + id: 'app.audio.captions.select.de-DE', + description: 'Audio speech recognition german language', + }, + 'en-US': { + id: 'app.audio.captions.select.en-US', + description: 'Audio speech recognition english language', + }, + 'es-ES': { + id: 'app.audio.captions.select.es-ES', + description: 'Audio speech recognition spanish language', + }, + 'fr-FR': { + id: 'app.audio.captions.select.fr-FR', + description: 'Audio speech recognition french language', + }, + 'hi-ID': { + id: 'app.audio.captions.select.hi-ID', + description: 'Audio speech recognition indian language', + }, + 'it-IT': { + id: 'app.audio.captions.select.it-IT', + description: 'Audio speech recognition italian language', + }, + 'ja-JP': { + id: 'app.audio.captions.select.ja-JP', + description: 'Audio speech recognition japanese language', + }, + 'pt-BR': { + id: 'app.audio.captions.select.pt-BR', + description: 'Audio speech recognition portuguese language', + }, + 'ru-RU': { + id: 'app.audio.captions.select.ru-RU', + description: 'Audio speech recognition russian language', + }, + 'zh-CN': { + id: 'app.audio.captions.select.zh-CN', + description: 'Audio speech recognition chinese language', + }, +}); + +interface AudioCaptionsButtonProps { + isRTL: boolean; + availableVoices: string[]; + currentSpeechLocale: string; + isSupported: boolean; + isVoiceUser: boolean; +} + +const DISABLED = ''; + +const AudioCaptionsButton: React.FC = ({ + isRTL, + currentSpeechLocale, + availableVoices, + isSupported, + isVoiceUser, +}) => { + const intl = useIntl(); + const [active] = useAudioCaptionEnable(); + + const isTranscriptionDisabled = () => currentSpeechLocale === DISABLED; + const fallbackLocale = availableVoices.includes(navigator.language) + ? navigator.language + : 'en-US'; // Assuming 'en-US' is the default fallback locale + + const getSelectedLocaleValue = isTranscriptionDisabled() + ? fallbackLocale + : currentSpeechLocale; + + const selectedLocale = useRef(getSelectedLocaleValue); + + useEffect(() => { + if (!isTranscriptionDisabled()) selectedLocale.current = getSelectedLocaleValue; + }, [currentSpeechLocale]); + + const shouldRenderChevron = isSupported && isVoiceUser; + + const toggleTranscription = () => { + setSpeechLocale(isTranscriptionDisabled() ? selectedLocale.current : DISABLED); + }; + + const getAvailableLocales = () => { + let indexToInsertSeparator = -1; + const availableVoicesObjectToMenu: (MenuOptionItemType | MenuSeparatorItemType)[] = availableVoices + .map((availableVoice: string, index: number) => { + if (availableVoice === availableVoices[0]) { + indexToInsertSeparator = index; + } + return ( + { + icon: '', + label: intl.formatMessage(intlMessages[availableVoice as keyof typeof intlMessages]), + key: availableVoice, + iconRight: selectedLocale.current === availableVoice ? 'check' : null, + customStyles: (selectedLocale.current === availableVoice) && Styled.SelectedLabel, + disabled: isTranscriptionDisabled(), + onClick: () => { + selectedLocale.current = availableVoice; + setSpeechLocale(selectedLocale.current); + }, + } + ); + }); + if (indexToInsertSeparator >= 0) { + availableVoicesObjectToMenu.splice(indexToInsertSeparator, 0, { + key: 'separator-01', + isSeparator: true, + }); + } + return [ + ...availableVoicesObjectToMenu, + ]; + }; + + const getAvailableLocalesList = () => ( + [{ + key: 'availableLocalesList', + label: intl.formatMessage(intlMessages.language), + customStyles: Styled.TitleLabel, + disabled: true, + }, + ...getAvailableLocales(), + { + key: 'divider', + label: intl.formatMessage(intlMessages.transcription), + customStyles: Styled.TitleLabel, + disabled: true, + }, + { + key: 'separator-02', + isSeparator: true, + }, + { + key: 'transcriptionStatus', + label: intl.formatMessage( + isTranscriptionDisabled() + ? intlMessages.transcriptionOn + : intlMessages.transcriptionOff, + ), + customStyles: isTranscriptionDisabled() + ? Styled.EnableTrascription : Styled.DisableTrascription, + disabled: false, + onClick: toggleTranscription, + }] + ); + const onToggleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setAudioCaptions(!active); + }; + + const startStopCaptionsButton = ( + + ); + + return ( + shouldRenderChevron + ? ( + + + { startStopCaptionsButton } + + + )} + actions={getAvailableLocalesList()} + opts={{ + id: 'default-dropdown-menu', + keepMounted: true, + transitionDuration: 0, + elevation: 3, + getcontentanchorel: null, + fullwidth: 'true', + anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, + transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, + }} + /> + + ) : startStopCaptionsButton + ); +}; + +const AudioCaptionsButtonContainer: React.FC = () => { + const isRTL = layoutSelect((i: Layout) => i.isRTL); + const currentUser = useCurrentUser( + (user) => ({ + speechLocale: user.speechLocale, + voice: user.voice, + }), + ); + + const { + data: audioCaptionsCountData, + loading: audioCaptionsCountLoading, + error: audioCaptionsCountError, + } = useSubscription(GET_AUDIO_CAPTIONS_COUNT); + + if (!currentUser || audioCaptionsCountLoading) return null; + + if (audioCaptionsCountError) { + logger.error(audioCaptionsCountError); + return ( +
+ { + JSON.stringify(audioCaptionsCountError) + } +
+ ); + } + + if (audioCaptionsCountData) { + const hasAudioCaptions = audioCaptionsCountData + .audio_caption_aggregate + .aggregate.count > 0; + + if (!hasAudioCaptions) return null; + } + + const availableVoices = getSpeechVoices(); + const currentSpeechLocale = currentUser.speechLocale || ''; + const isSupported = availableVoices.length > 0; + const isVoiceUser = !!currentUser.voice; + + return ( + + ); +}; + +export default AudioCaptionsButtonContainer; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/queries.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/queries.ts new file mode 100644 index 0000000000..5221a454d3 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/queries.ts @@ -0,0 +1,23 @@ +import { gql } from '@apollo/client'; + +export interface GetAudioCaptionsCountResponse { + audio_caption_aggregate: { + aggregate: { + count: number; + } + }; +} + +export const GET_AUDIO_CAPTIONS_COUNT = gql` + subscription GetAudioCaptionsCount { + audio_caption_aggregate { + aggregate { + count(columns: transcriptId) + } + } + } +`; + +export default { + GET_AUDIO_CAPTIONS_COUNT, +}; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/styles.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/styles.ts new file mode 100644 index 0000000000..92f7c04797 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/button/styles.ts @@ -0,0 +1,62 @@ +import styled from 'styled-components'; +import Button from '/imports/ui/components/common/button/component'; +import Toggle from '/imports/ui/components/common/switch/component'; +import { + colorWhite, + colorPrimary, + colorOffWhite, + colorDangerDark, + colorSuccess, +} from '/imports/ui/stylesheets/styled-components/palette'; + +// @ts-ignore - as button comes from JS, we can't provide its props +const ClosedCaptionToggleButton = styled(Button)` + ${({ ghost }) => ghost && ` + span { + box-shadow: none; + background-color: transparent !important; + border-color: ${colorWhite} !important; + } + i { + margin-top: .4rem; + } + `} +`; + +const SpanButtonWrapper = styled.span` + position: relative; +`; + +const TranscriptionToggle = styled(Toggle)` + display: flex; + justify-content: flex-start; + padding-left: 1em; +`; + +const TitleLabel = { + fontWeight: 'bold', + opacity: 1, +}; + +const EnableTrascription = { + color: colorSuccess, +}; + +const DisableTrascription = { + color: colorDangerDark, +}; + +const SelectedLabel = { + color: colorPrimary, + backgroundColor: colorOffWhite, +}; + +export default { + ClosedCaptionToggleButton, + SpanButtonWrapper, + TranscriptionToggle, + TitleLabel, + EnableTrascription, + DisableTrascription, + SelectedLabel, +}; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/captions/component.tsx b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/captions/component.tsx new file mode 100644 index 0000000000..f0ff7e19d0 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/captions/component.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { + getSpeechVoices, + isAudioTranscriptionEnabled, + setSpeechLocale, + useFixedLocale, +} from '../service'; +import { useCurrentUser } from '/imports/ui/core/hooks/useCurrentUser'; + +const intlMessages = defineMessages({ + title: { + id: 'app.audio.captions.speech.title', + description: 'Audio speech recognition title', + }, + disabled: { + id: 'app.audio.captions.speech.disabled', + description: 'Audio speech recognition disabled', + }, + unsupported: { + id: 'app.audio.captions.speech.unsupported', + description: 'Audio speech recognition unsupported', + }, + 'de-DE': { + id: 'app.audio.captions.select.de-DE', + description: 'Audio speech recognition german language', + }, + 'en-US': { + id: 'app.audio.captions.select.en-US', + description: 'Audio speech recognition english language', + }, + 'es-ES': { + id: 'app.audio.captions.select.es-ES', + description: 'Audio speech recognition spanish language', + }, + 'fr-FR': { + id: 'app.audio.captions.select.fr-FR', + description: 'Audio speech recognition french language', + }, + 'hi-ID': { + id: 'app.audio.captions.select.hi-ID', + description: 'Audio speech recognition indian language', + }, + 'it-IT': { + id: 'app.audio.captions.select.it-IT', + description: 'Audio speech recognition italian language', + }, + 'ja-JP': { + id: 'app.audio.captions.select.ja-JP', + description: 'Audio speech recognition japanese language', + }, + 'pt-BR': { + id: 'app.audio.captions.select.pt-BR', + description: 'Audio speech recognition portuguese language', + }, + 'ru-RU': { + id: 'app.audio.captions.select.ru-RU', + description: 'Audio speech recognition russian language', + }, + 'zh-CN': { + id: 'app.audio.captions.select.zh-CN', + description: 'Audio speech recognition chinese language', + }, +}); + +interface AudioCaptionsSelectProps { + isTranscriptionEnabled: boolean; + speechLocale: string; + speechVoices: string[]; +} + +const AudioCaptionsSelect: React.FC = ({ + isTranscriptionEnabled, + speechLocale, + speechVoices, +}) => { + const useLocaleHook = useFixedLocale(); + const intl = useIntl(); + if (!isTranscriptionEnabled || useLocaleHook) return null; + + if (speechVoices.length === 0) { + return ( +
+ {`*${intl.formatMessage(intlMessages.unsupported)}`} +
+ ); + } + + const onChange = (e: React.ChangeEvent) => { + const { value } = e.target; + setSpeechLocale(value); + }; + + return ( +
+ + +
+ ); +}; + +const AudioCaptionsSelectContainer: React.FC = () => { + const currentUser = useCurrentUser( + (user) => ({ + speechLocale: user.speechLocale, + voice: user.voice, + }), + ); + const isEnabled = isAudioTranscriptionEnabled(); + const voices = getSpeechVoices(); + + if (!currentUser || !isEnabled || !voices) return null; + + return ( + + ); +}; + +export default AudioCaptionsSelectContainer; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/component.tsx b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/component.tsx new file mode 100644 index 0000000000..cd3426dc47 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/component.tsx @@ -0,0 +1,108 @@ +import { useSubscription } from '@apollo/client'; +import { Meteor } from 'meteor/meteor'; +import React, { useEffect, useRef, useState } from 'react'; +import { GET_AUDIO_CAPTIONS, GetAudioCaptions } from './queries'; +import logger from '/imports/startup/client/logger'; +import { User } from '/imports/ui/Types/user'; +import Styled from './styles'; + +const CAPTIONS_CONFIG = Meteor.settings.public.captions; + +interface AudioCaptionsLiveProps { + transcript: string; + user: Pick; +} + +const AudioCaptionsLive: React.FC = ({ + transcript, + user, +}) => { + const [clear, setClear] = useState(true); + const timerRef = useRef | null>(null); + const prevTranscriptRef = useRef(''); + + const resetTimer = () => { + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + }; + // did update + useEffect(() => { + if (clear) { + if (prevTranscriptRef.current !== transcript) { + prevTranscriptRef.current = transcript; + setClear(false); + } + } else { + resetTimer(); + timerRef.current = setTimeout(() => setClear(true), CAPTIONS_CONFIG.time); + } + }, [transcript, clear]); + // will unmount + useEffect(() => () => resetTimer(), []); + + const hasContent = transcript.length > 0 && !clear; + + return ( + + {clear ? null : ( + + + {user.name.slice(0, 2)} + + + )} + + {clear ? '' : transcript} + + + {clear ? '' : transcript} + + + ); +}; + +const AudioCaptionsLiveContainer: React.FC = () => { + const { + data: AudioCaptionsLiveData, + loading: AudioCaptionsLiveLoading, + error: AudioCaptionsLiveError, + } = useSubscription(GET_AUDIO_CAPTIONS, { + variables: { + time: new Date().toISOString(), + }, + }); + + if (AudioCaptionsLiveLoading) return null; + + if (AudioCaptionsLiveError) { + logger.error(AudioCaptionsLiveError); + return ( +
+ {JSON.stringify(AudioCaptionsLiveError)} +
+ ); + } + + if (!AudioCaptionsLiveData) return null; + const { + transcript, + user, + } = AudioCaptionsLiveData.audio_caption[0]; + return ( + + ); +}; + +export default AudioCaptionsLiveContainer; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/queries.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/queries.ts new file mode 100644 index 0000000000..73803d63c6 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/queries.ts @@ -0,0 +1,34 @@ +import { gql } from '@apollo/client'; + +export interface GetAudioCaptions { + audio_caption: Array<{ + user: { + avatar: string; + color: string; + isModerator: boolean; + name: string; + }; + transcript: string; + transcriptId: string; + }>; +} + +export const GET_AUDIO_CAPTIONS = gql` + subscription MySubscription ($time: Date!){ + audio_caption(where: {createdAt: {_lte: $time}}, order_by: {createdAt: desc}, limit: 10) { + user { + avatar + color + isModerator + name + } + transcript + transcriptId + createdAt + } + } +`; + +export default { + GET_AUDIO_CAPTIONS, +}; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/styles.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/styles.ts new file mode 100644 index 0000000000..81bdc2073a --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/live/styles.ts @@ -0,0 +1,139 @@ +import styled from 'styled-components'; + +import { + userIndicatorsOffset, +} from '/imports/ui/stylesheets/styled-components/general'; +import { + colorWhite, + userListBg, + colorSuccess, +} from '/imports/ui/stylesheets/styled-components/palette'; + +type CaptionsProps = { + hasContent: boolean; +}; + +interface UserAvatarProps { + color: string; + moderator: boolean; + avatar: string; + emoji?: string; +} + +const Wrapper = styled.div` + display: flex; +`; + +const Captions = styled.div` + white-space: pre-line; + word-wrap: break-word; + font-family: Verdana, Arial, Helvetica, sans-serif; + font-size: 1.5rem; + background: #000000a0; + color: white; + ${({ hasContent }) => hasContent && ` + padding: 0.5rem; + `} +`; + +const VisuallyHidden = styled.div` + position: absolute; + overflow: hidden; + clip: rect(0 0 0 0); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + border: 0; +`; + +const UserAvatarWrapper = styled.div` + background: #000000a0; + min-height: 3.25; + padding: 0.5rem; + text-transform: capitalize; + width: 3.25rem; +`; + +const UserAvatar = styled.div` + flex: 0 0 2.25rem; + margin: 0px calc(0.5rem) 0px 0px; + box-flex: 0; + position: relative; + height: 2.25rem; + width: 2.25rem; + border-radius: 50%; + text-align: center; + font-size: .85rem; + border: 2px solid transparent; + user-select: none; + ${ + ({ color }: UserAvatarProps) => ` + background-color: ${color}; + `} + } + &:after, + &:before { + content: ""; + position: absolute; + width: 0; + height: 0; + padding-top: .5rem; + padding-right: 0; + padding-left: 0; + padding-bottom: 0; + color: inherit; + top: auto; + left: auto; + bottom: ${userIndicatorsOffset}; + right: ${userIndicatorsOffset}; + border: 1.5px solid ${userListBg}; + border-radius: 50%; + background-color: ${colorSuccess}; + color: ${colorWhite}; + opacity: 0; + font-family: 'bbb-icons'; + font-size: .65rem; + line-height: 0; + text-align: center; + vertical-align: middle; + letter-spacing: -.65rem; + z-index: 1; + [dir="rtl"] & { + left: ${userIndicatorsOffset}; + right: auto; + padding-right: .65rem; + padding-left: 0; + } + } + ${({ moderator }: UserAvatarProps) => moderator && ` + border-radius: 5px; + `} + // ================ image ================ + ${({ avatar, emoji }: UserAvatarProps) => avatar?.length !== 0 && !emoji && ` + background-image: url(${avatar}); + background-repeat: no-repeat; + background-size: contain; + `} + // ================ image ================ + // ================ content ================ + color: ${colorWhite}; + font-size: 110%; + text-transform: capitalize; + display: flex; + justify-content: center; + align-items:center; + // ================ content ================ + & .react-loading-skeleton { + height: 2.25rem; + width: 2.25rem; + } +`; + +export default { + Wrapper, + Captions, + VisuallyHidden, + UserAvatarWrapper, + UserAvatar, +}; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/service.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/service.ts new file mode 100644 index 0000000000..c1070c92aa --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/service.ts @@ -0,0 +1,55 @@ +import { Meteor } from 'meteor/meteor'; +import { unique } from 'radash'; +import { makeCall } from '/imports/ui/services/api'; +import logger from '/imports/startup/client/logger'; +import { setAudioCaptionEnable } from '/imports/ui/core/local-states/useAudioCaptionEnable'; +import { isLiveTranscriptionEnabled } from '/imports/ui/services/features'; + +const CONFIG = Meteor.settings.public.app.audioCaptions; +const PROVIDER = CONFIG.provider; +const LANGUAGES = CONFIG.language.available; + +export const isAudioTranscriptionEnabled = () => isLiveTranscriptionEnabled(); + +export const isWebSpeechApi = () => PROVIDER === 'webspeech'; + +export const getSpeechVoices = () => { + if (!isWebSpeechApi()) return LANGUAGES; + + return unique( + window + .speechSynthesis + .getVoices() + .map((v) => v.lang) + .filter((v) => LANGUAGES.includes(v)), + ); +}; + +export const setAudioCaptions = (value: boolean) => { + setAudioCaptionEnable(value); + // @ts-ignore - Exist while we have meteor in the project + Session.set('audioCaptions', value); +}; + +export const setSpeechLocale = (value: string) => { + const voices = getSpeechVoices(); + + if (voices.includes(value) || value === '') { + makeCall('setSpeechLocale', value, CONFIG.provider); + } else { + logger.error({ + logCode: 'captions_speech_locale', + }, 'Captions speech set locale error'); + } +}; + +export const useFixedLocale = () => isAudioTranscriptionEnabled() && CONFIG.language.forceLocale; + +export default { + getSpeechVoices, + isAudioTranscriptionEnabled, + setSpeechLocale, + setAudioCaptions, + isWebSpeechApi, + useFixedLocale, +}; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/speech/component.tsx b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/speech/component.tsx new file mode 100644 index 0000000000..8b95ab9607 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/speech/component.tsx @@ -0,0 +1,170 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import { + SpeechRecognitionAPI, + generateId, + initSpeechRecognition, + isLocaleValid, + updateFinalTranscript, + updateInterimTranscript, +} from './service'; +import logger from '/imports/startup/client/logger'; +import { useReactiveVar } from '@apollo/client'; +import AudioManager from '/imports/ui/services/audio-manager'; +import { useCurrentUser } from '/imports/ui/core/hooks/useCurrentUser'; + +type SpeechRecognitionEvent = { + resultIndex: number; + results: SpeechRecognitionResult[]; +} + +type SpeechRecognitionErrorEvent = { + error: string; + message: string; +} + +interface AudioCaptionsSpeechProps { + locale: string; + connected: boolean; +} +const speechHasStarted = { + started: false, +}; +const AudioCaptionsSpeech: React.FC = ({ + locale, + connected, +}) => { + const resultRef = useRef({ + id: generateId(), + transcript: '', + isFinal: true, + }); + + const idleRef = useRef(true); + const speechRecognitionRef = useRef>(null); + const onEnd = useCallback(() => { + stop(); + }, []); + const onError = useCallback((event: SpeechRecognitionErrorEvent) => { + stop(); + logger.error({ + logCode: 'captions_speech_recognition', + extraInfo: { + error: event.error, + message: event.message, + }, + }, 'Captions speech recognition error'); + }, []); + + const onResult = useCallback((event: SpeechRecognitionEvent) => { + const { + resultIndex, + results, + } = event; + + const { id } = resultRef.current; + + const { transcript } = results[resultIndex][0]; + const { isFinal } = results[resultIndex]; + + resultRef.current.transcript = transcript; + resultRef.current.isFinal = isFinal; + + if (isFinal) { + updateFinalTranscript(id, transcript, locale); + resultRef.current.id = generateId(); + } else { + updateInterimTranscript(id, transcript, locale); + } + }, [locale]); + + const stop = useCallback(() => { + idleRef.current = true; + if (speechRecognitionRef.current) { + const { + isFinal, + transcript, + } = resultRef.current; + + if (!isFinal) { + const { id } = resultRef.current; + updateFinalTranscript(id, transcript, locale); + speechRecognitionRef.current.abort(); + } else { + speechRecognitionRef.current.stop(); + speechHasStarted.started = false; + } + } + }, [locale]); + + const start = (settedLocale: string) => { + if (speechRecognitionRef.current && isLocaleValid(settedLocale)) { + speechRecognitionRef.current.lang = settedLocale; + try { + resultRef.current.id = generateId(); + speechRecognitionRef.current.start(); + idleRef.current = false; + } catch (event: unknown) { + onError(event as SpeechRecognitionErrorEvent); + } + } + }; + + useEffect(() => { + speechRecognitionRef.current = initSpeechRecognition(); + }, []); + + useEffect(() => { + if (speechRecognitionRef.current) { + speechRecognitionRef.current.onend = () => onEnd(); + speechRecognitionRef.current.onerror = (event: SpeechRecognitionErrorEvent) => onError(event); + speechRecognitionRef.current.onresult = (event: SpeechRecognitionEvent) => onResult(event); + } + }, [speechRecognitionRef.current]); + + const connectedRef = useRef(connected); + const localeRef = useRef(locale); + useEffect(() => { + // Connected + if (!connectedRef.current && connected) { + start(locale); + connectedRef.current = connected; + } else if (connectedRef.current && !connected) { + // Disconnected + stop(); + connectedRef.current = connected; + } else if (localeRef.current !== locale) { + // Locale changed + if (connectedRef.current && connected) { + stop(); + start(locale); + localeRef.current = locale; + } + } + }, [connected, locale]); + + return null; +}; + +const AudioCaptionsSpeechContainer: React.FC = () => { + /* eslint no-underscore-dangle: 0 */ + // @ts-ignore - temporary while hybrid (meteor+GraphQl) + const isConnected = useReactiveVar(AudioManager._isConnected.value) as boolean; + + const currentUser = useCurrentUser( + (user) => ({ + speechLocale: user.speechLocale, + voice: user.voice, + }), + ); + + if (!currentUser) return null; + + return ( + + ); +}; + +export default AudioCaptionsSpeechContainer; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/speech/service.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/speech/service.ts new file mode 100644 index 0000000000..a5f2e9e998 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-captions/speech/service.ts @@ -0,0 +1,121 @@ +import { Meteor } from 'meteor/meteor'; +import { isAudioTranscriptionEnabled, isWebSpeechApi, setSpeechLocale } from '../service'; +import Auth from '/imports/ui/services/auth'; +import deviceInfo from '/imports/utils/deviceInfo'; +import { unique } from 'radash'; +// @ts-ignore - bbb-diff is not typed +import { diff } from '@mconf/bbb-diff'; +import { Session } from 'meteor/session'; +import { throttle } from '/imports/utils/throttle'; +import { makeCall } from '/imports/ui/services/api'; + +const CONFIG = Meteor.settings.public.app.audioCaptions; +const LANGUAGES = CONFIG.language.available; +const VALID_ENVIRONMENT = !deviceInfo.isMobile || CONFIG.mobile; +const THROTTLE_TIMEOUT = 2000; +// Reason: SpeechRecognition is not in window type definition +// Fix based on: https://stackoverflow.com/questions/41740683/speechrecognition-and-speechsynthesis-in-typescript +/* eslint @typescript-eslint/no-explicit-any: 0 */ +export const SpeechRecognitionAPI = (window as any).SpeechRecognition +|| (window as any).webkitSpeechRecognition; + +export const generateId = () => `${Auth.userID}-${Date.now()}`; + +export const hasSpeechRecognitionSupport = () => typeof SpeechRecognitionAPI !== 'undefined' + && typeof window.speechSynthesis !== 'undefined' + && VALID_ENVIRONMENT; + +export const setSpeechVoices = () => { + if (!hasSpeechRecognitionSupport()) return; + + Session.set('speechVoices', unique(window.speechSynthesis.getVoices().map((v) => v.lang))); +}; + +export const useFixedLocale = () => isAudioTranscriptionEnabled() && CONFIG.language.forceLocale; + +export const localeAsDefaultSelected = () => CONFIG.language.defaultSelectLocale; + +export const getLocale = () => { + const { locale } = CONFIG.language; + if (locale === 'browserLanguage') return navigator.language; + if (locale === 'disabled') return ''; + return locale; +}; + +let prevId: string = ''; +let prevTranscript: string = ''; +const updateTranscript = ( + id: string, + transcript: string, + locale: string, + isFinal: boolean, +) => { + // If it's a new sentence + if (id !== prevId) { + prevId = id; + prevTranscript = ''; + } + + const transcriptDiff = diff(prevTranscript, transcript); + + let start = 0; + let end = 0; + let text = ''; + if (transcriptDiff) { + start = transcriptDiff.start; + end = transcriptDiff.end; + text = transcriptDiff.text; + } + + // Stores current transcript as previous + prevTranscript = transcript; + + makeCall('updateTranscript', id, start, end, text, transcript, locale, isFinal); +}; + +const throttledTranscriptUpdate = throttle(updateTranscript, THROTTLE_TIMEOUT, { + leading: false, + trailing: true, +}); + +export const updateInterimTranscript = (id: string, transcript: string, locale: string) => { + throttledTranscriptUpdate(id, transcript, locale, false); +}; + +export const updateFinalTranscript = (id: string, transcript: string, locale: string) => { + throttledTranscriptUpdate.cancel(); + updateTranscript(id, transcript, locale, true); +}; + +export const initSpeechRecognition = () => { + if (!isAudioTranscriptionEnabled() && !isWebSpeechApi()) return null; + + if (!hasSpeechRecognitionSupport()) return null; + + setSpeechVoices(); + const speechRecognition = new SpeechRecognitionAPI(); + + speechRecognition.continuous = true; + speechRecognition.interimResults = true; + + if (useFixedLocale() || localeAsDefaultSelected()) { + setSpeechLocale(getLocale()); + } else { + setSpeechLocale(navigator.language); + } + + return speechRecognition; +}; + +export const isLocaleValid = (locale: string) => LANGUAGES.includes(locale); + +export default { + generateId, + initSpeechRecognition, + getLocale, + localeAsDefaultSelected, + useFixedLocale, + setSpeechVoices, + hasSpeechRecognitionSupport, + isLocaleValid, +}; diff --git a/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx index 44a08b88c9..11fc878ad3 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx @@ -4,10 +4,11 @@ import Service from '/imports/ui/components/audio/captions/service'; import Button from './component'; import SpeechService from '/imports/ui/components/audio/captions/speech/service'; import AudioService from '/imports/ui/components/audio/service'; +import AudioCaptionsButtonContainer from '../../audio-graphql/audio-captions/button/component'; const Container = (props) => + + } else if(curr.isOnline) { + return
+ {curr.meeting.name} +

+ You are online, welcome {curr.name} ({curr.userId}) + + + {/**/} + {/*
*/} + + {/**/} + {/*
*/} + + + + + + +
+ + +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ + +
+ +
+ + // return ( + // + // {curr.userId} + // {curr.name} + // {curr.joined && !curr.loggedOut && !curr.ejected ? 'joined' : ''} + // {curr.loggedOut ? 'loggedOut' : ''} + // {curr.ejected ? 'ejected' : ''} + // {!curr.joined && !curr.loggedOut ? : ''} + // {curr.joined && !curr.loggedOut && !curr.ejected ? : ''} + // + // {curr.joinErrorCode} + // {curr.joinErrorMessage} + // + // ); + } + + + })} + ) + } +} + diff --git a/bbb-graphql-client-test/src/ChatMessages.js b/bbb-graphql-client-test/src/ChatMessages.js index eb938c4025..780bd6f23a 100644 --- a/bbb-graphql-client-test/src/ChatMessages.js +++ b/bbb-graphql-client-test/src/ChatMessages.js @@ -25,12 +25,11 @@ export default function ChatMessages({userId}) { const { loading, error, data } = usePatchedSubscription( gql`subscription { - chat_message_private(limit: 20, order_by: [{createdTime: desc}, {senderName: asc}]) { + chat_message_private(limit: 20, order_by: [{createdAt: desc}, {senderName: asc}]) { chatEmphasizedText chatId correlationId - createdTime - createdTimeAsDate + createdAt message messageId senderId @@ -63,7 +62,7 @@ export default function ChatMessages({userId}) { {curr.chatId} {curr.senderName} {curr.message} - {curr.createdTimeAsDate} ({curr.createdTime}) + {curr.createdAt} { curr.senderId !== userId ? diff --git a/bbb-graphql-client-test/src/ChatPublicMessages.js b/bbb-graphql-client-test/src/ChatPublicMessages.js index e5860c669a..0fd0f46e30 100644 --- a/bbb-graphql-client-test/src/ChatPublicMessages.js +++ b/bbb-graphql-client-test/src/ChatPublicMessages.js @@ -1,7 +1,10 @@ import {useSubscription, gql, useMutation} from '@apollo/client'; import usePatchedSubscription from "./usePatchedSubscription"; +import React, {useState} from "react"; export default function ChatPublicMessages({userId}) { + const [textAreaValue, setTextAreaValue] = useState(``); + const [updateLastSeen] = useMutation(gql` mutation UpdateChatUser($chatId: String, $lastSeenAt: bigint) { update_chat_user( @@ -23,14 +26,36 @@ export default function ChatPublicMessages({userId}) { }; + const [sendChatMessage] = useMutation(gql` + mutation ChatSendMessage($chatId: String!, $message: String!) { + chatSendMessage( + chatId: $chatId, + chatMessageInMarkdownFormat: $message + ) + } + `); + + const handleSendMessage = () => { + if (textAreaValue.trim() !== '') { + sendChatMessage({ + variables: { + chatId: 'MAIN-PUBLIC-GROUP-CHAT', + message: textAreaValue, + }, + }); + + setTextAreaValue(''); + } + }; + + const { loading, error, data } = usePatchedSubscription( gql`subscription { - chat_message_public(limit: 20, order_by: {createdTime: desc}) { + chat_message_public(limit: 20, order_by: {createdAt: desc}) { chatId chatEmphasizedText correlationId - createdTime - createdTimeAsDate + createdAt message messageId senderId @@ -42,7 +67,7 @@ export default function ChatPublicMessages({userId}) { return !loading && !error && ( - + @@ -53,27 +78,40 @@ export default function ChatPublicMessages({userId}) { - - + + {data.map((curr) => { console.log('message', curr); - return ( - - - - - - - - ); + return ( + + + + + + + + ); })} - + + + + + +
Public Chat Messages
Sent At
{curr.chatId}{curr.senderName}{curr.message}{curr.createdTimeAsDate} ({curr.createdTime}) - { - curr.senderId !== userId ? - - : '' - } -
{curr.chatId}{curr.senderName}{curr.message}{curr.createdAt} + { + curr.senderId !== userId ? + + : '' + } +
+ + +
); } diff --git a/bbb-graphql-client-test/src/CursorsAll.js b/bbb-graphql-client-test/src/CursorsAll.js index f684bb3e52..4b34323be3 100644 --- a/bbb-graphql-client-test/src/CursorsAll.js +++ b/bbb-graphql-client-test/src/CursorsAll.js @@ -24,7 +24,7 @@ export default function CursorsAll() { ( - + diff --git a/bbb-graphql-client-test/src/MeetingInfo.js b/bbb-graphql-client-test/src/MeetingInfo.js index 07c84d9f39..d4d1992668 100644 --- a/bbb-graphql-client-test/src/MeetingInfo.js +++ b/bbb-graphql-client-test/src/MeetingInfo.js @@ -7,7 +7,7 @@ export default function MeetingInfo() { meetingId createdTime disabledFeatures - duration + durationInSeconds extId html5InstanceId isBreakout @@ -31,7 +31,7 @@ export default function MeetingInfo() { {/**/} - + @@ -42,7 +42,7 @@ export default function MeetingInfo() { {/**/} - + ); })} diff --git a/bbb-graphql-client-test/src/MyInfo.js b/bbb-graphql-client-test/src/MyInfo.js index ea61db4af5..10525d789b 100644 --- a/bbb-graphql-client-test/src/MyInfo.js +++ b/bbb-graphql-client-test/src/MyInfo.js @@ -22,12 +22,13 @@ export default function MyInfo({userAuthToken}) { const [dispatchUserJoin] = useMutation(gql` mutation UserJoin($authToken: String!, $clientType: String!) { - userJoin( + userJoinMeeting( authToken: $authToken, clientType: $clientType, ) } `); + const handleDispatchUserJoin = (authToken) => { dispatchUserJoin({ variables: { @@ -37,12 +38,23 @@ export default function MyInfo({userAuthToken}) { }); }; + const [dispatchUserLeave] = useMutation(gql` + mutation UserLeaveMeeting { + userLeaveMeeting + } + `); + const handleDispatchUserLeave = (authToken) => { + dispatchUserLeave(); + }; + const { loading, error, data } = useSubscription( gql`subscription { user_current { userId name + loggedOut + ejected joined joinErrorCode joinErrorMessage @@ -60,7 +72,7 @@ export default function MyInfo({userAuthToken}) { {/**/} - + @@ -72,8 +84,11 @@ export default function MyInfo({userAuthToken}) { - diff --git a/bbb-graphql-client-test/src/UserConnectionStatus.js b/bbb-graphql-client-test/src/UserConnectionStatus.js index 2ed6bfc1ec..cc5ed9c404 100644 --- a/bbb-graphql-client-test/src/UserConnectionStatus.js +++ b/bbb-graphql-client-test/src/UserConnectionStatus.js @@ -52,7 +52,7 @@ export default function UserConnectionStatus() { setTimeout(() => { handleUpdateConnectionAliveAt(); - }, 5000); + }, 25000); }; useEffect(() => { diff --git a/bbb-graphql-client-test/src/UserList.js b/bbb-graphql-client-test/src/UserList.js index 38af55611a..64bdaafccb 100644 --- a/bbb-graphql-client-test/src/UserList.js +++ b/bbb-graphql-client-test/src/UserList.js @@ -1,9 +1,10 @@ -import {gql} from '@apollo/client'; +import {gql, useMutation} from '@apollo/client'; import React, { useState } from "react"; import usePatchedSubscription from "./usePatchedSubscription"; -const ParentOfUserList = ({userId}) => { +const ParentOfUserList = ({user}) => { const [shouldRender, setShouldRender] = useState(true); + return (
Userlist: @@ -13,12 +14,30 @@ const ParentOfUserList = ({userId}) => { setShouldRender(e.target.checked); } }> - {shouldRender && } + {shouldRender && }
); } function UserList({userId}) { + + const [dispatchUserEject] = useMutation(gql` + mutation UserEject($userId: String!) { + userEjectFromMeeting( + userId: $userId, + banUser: false, + ) + } + `); + + const handleDispatchUserEject = (userId) => { + dispatchUserEject({ + variables: { + userId: userId, + }, + }); + }; + const { loading, error, data } = usePatchedSubscription( gql`subscription { user(limit: 50, order_by: [ @@ -120,7 +139,9 @@ function UserList({userId}) { - + ); })} diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index 4f2ca1f10d..ebbef3916e 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -233,6 +233,7 @@ CREATE TABLE "user" ( "avatar" varchar(500), "color" varchar(7), "sessionToken" varchar(16), + "authToken" varchar(16), "authed" bool, "joined" bool, "joinErrorCode" varchar(50), @@ -384,6 +385,7 @@ CREATE INDEX "idx_v_user_meetingId_orderByColumns" ON "user"("meetingId","role", CREATE OR REPLACE VIEW "v_user_current" AS SELECT "user"."userId", "user"."extId", + "user"."authToken", "user"."meetingId", "user"."name", "user"."nameSortable", @@ -419,7 +421,8 @@ AS SELECT "user"."userId", "user"."hasDrawPermissionOnCurrentPage", "user"."echoTestRunningAt", CASE WHEN "user"."echoTestRunningAt" > current_timestamp - INTERVAL '3 seconds' THEN TRUE ELSE FALSE END "isRunningEchoTest", - CASE WHEN "user"."role" = 'MODERATOR' THEN true ELSE false END "isModerator" + CASE WHEN "user"."role" = 'MODERATOR' THEN true ELSE false END "isModerator", + CASE WHEN "user"."joined" IS true AND "user"."expired" IS false AND "user"."loggedOut" IS false AND "user"."ejected" IS NOT TRUE THEN true ELSE false END "isOnline" FROM "user"; CREATE OR REPLACE VIEW "v_user_guest" AS diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_current.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_current.yaml index 90f43241b5..993b22335d 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_current.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_current.yaml @@ -130,6 +130,7 @@ select_permissions: - role: bbb_client permission: columns: + - authToken - authed - avatar - away @@ -150,6 +151,7 @@ select_permissions: - hasDrawPermissionOnCurrentPage - isDialIn - isModerator + - isOnline - isRunningEchoTest - joinErrorCode - joinErrorMessage @@ -173,6 +175,7 @@ select_permissions: - role: pre_join_bbb_client permission: columns: + - authToken - authed - banned - color @@ -181,6 +184,8 @@ select_permissions: - ejectReasonCode - ejected - expired + - isOnline + - isModerator - extId - guest - guestStatus diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy index fb9b81a2a6..88c07e3776 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ConnectionController.groovy @@ -95,7 +95,7 @@ class ConnectionController { def builder = new JsonBuilder() builder { "response" "authorized" - "X-Hasura-Role" u ? "bbb_client" : "pre_join_bbb_client" + "X-Hasura-Role" u && !u.hasLeft() ? "bbb_client" : "pre_join_bbb_client" "X-Hasura-ModeratorInMeeting" u && u.isModerator() ? userSession.meetingID : "" "X-Hasura-PresenterInMeeting" u && u.isPresenter() ? userSession.meetingID : "" "X-Hasura-UserId" userSession.internalUserId From a4de358c48071a20e5eaeec9e5fd74b0c12200ea Mon Sep 17 00:00:00 2001 From: Tainan Felipe Date: Thu, 18 Jan 2024 13:09:33 -0300 Subject: [PATCH 091/512] Fix: Remove unnecessary component --- bigbluebutton-html5/imports/ui/components/app/component.jsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 444c57589d..a39f3ef238 100644 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -631,11 +631,6 @@ class App extends Component { {shouldShowPresentation ? : null} {shouldShowScreenshare ? : null} - { - shouldShowExternalVideo - ? - : null - } {shouldShowSharedNotes ? ( Date: Thu, 18 Jan 2024 13:21:57 -0300 Subject: [PATCH 092/512] Remove console.log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ramón Souza --- bigbluebutton-html5/imports/ui/services/auth/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/services/auth/index.js b/bigbluebutton-html5/imports/ui/services/auth/index.js index 604a40353d..d6617e2a7f 100755 --- a/bigbluebutton-html5/imports/ui/services/auth/index.js +++ b/bigbluebutton-html5/imports/ui/services/auth/index.js @@ -23,7 +23,6 @@ class Auth { return; } - console.log('-----------------'); this._meetingID = Storage.getItem('meetingID'); this._userID = Storage.getItem('userID'); From 961fb1940418aaffa8513da2a2acfabb3f6176a0 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Thu, 18 Jan 2024 16:40:09 -0300 Subject: [PATCH 093/512] Provide sharedNotes.diff through Graphql --- bbb-graphql-server/bbb_schema.sql | 5 +++++ .../tables/public_v_sharedNotes_diff.yaml | 22 +++++++++++++++++++ .../BigBlueButton/tables/tables.yaml | 1 + 3 files changed, 28 insertions(+) create mode 100644 bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_sharedNotes_diff.yaml diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index ab2da6a7a1..6d8360a18e 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1500,6 +1500,11 @@ create table "sharedNotes_rev" ( ); --create view "v_sharedNotes_rev" as select * from "sharedNotes_rev"; +create view "v_sharedNotes_diff" as +select "meetingId", "sharedNotesExtId", "userId", "start", "end", "diff" +from "sharedNotes_rev" +where "diff" is not null; + create table "sharedNotes_session" ( "meetingId" varchar(100) references "meeting"("meetingId") ON DELETE CASCADE, "sharedNotesExtId" varchar(25), diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_sharedNotes_diff.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_sharedNotes_diff.yaml new file mode 100644 index 0000000000..b4d4dc6579 --- /dev/null +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_sharedNotes_diff.yaml @@ -0,0 +1,22 @@ +table: + name: v_sharedNotes_diff + schema: public +configuration: + column_config: {} + custom_column_names: {} + custom_name: sharedNotes_diff + custom_root_fields: {} +select_permissions: + - role: bbb_client + permission: + columns: + - diff + - end + - rev + - sharedNotesExtId + - start + - userId + filter: + meetingId: + _eq: X-Hasura-MeetingId + comment: "" diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml index ba77948367..27edd90911 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml @@ -36,6 +36,7 @@ - "!include public_v_pres_presentation_uploadToken.yaml" - "!include public_v_screenshare.yaml" - "!include public_v_sharedNotes.yaml" +- "!include public_v_sharedNotes_diff.yaml" - "!include public_v_sharedNotes_session.yaml" - "!include public_v_timer.yaml" - "!include public_v_user.yaml" From 32b239459c642a0c668f6f7a7fb5f76caf8796ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Thu, 18 Jan 2024 17:20:28 -0300 Subject: [PATCH 094/512] fix reset active timer --- .../imports/ui/components/timer/component.jsx | 20 +++++++++++++++---- .../timer-graphql/indicator/component.tsx | 6 ++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/timer/component.jsx b/bigbluebutton-html5/imports/ui/components/timer/component.jsx index 0c1754d834..4c6bc9e4ac 100644 --- a/bigbluebutton-html5/imports/ui/components/timer/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/timer/component.jsx @@ -120,10 +120,20 @@ class Timer extends Component { } handleControlClick() { - const { timer, startTimer, stopTimer } = this.props; + const { + timer, startTimer, stopTimer, timeOffset, + } = this.props; + + const { + running, + accumulated, + timestamp, + } = timer; if (timer.running) { - stopTimer(this.getTime()); + const elapsedTime = Service.getElapsedTime(running, timestamp, timeOffset, accumulated); + + stopTimer(elapsedTime); } else { startTimer(); } @@ -160,17 +170,19 @@ class Timer extends Component { } handleSwitchToStopwatch() { - const { timer, switchTimer } = this.props; + const { timer, stopTimer, switchTimer } = this.props; if (!timer.stopwatch) { + stopTimer(this.getTime()); switchTimer(true); } } handleSwitchToTimer() { - const { timer, switchTimer } = this.props; + const { timer, stopTimer, switchTimer } = this.props; if (timer.stopwatch) { + stopTimer(this.getTime()); switchTimer(false); } } diff --git a/bigbluebutton-html5/imports/ui/components/timer/timer-graphql/indicator/component.tsx b/bigbluebutton-html5/imports/ui/components/timer/timer-graphql/indicator/component.tsx index c10be4ec97..17b4f64584 100644 --- a/bigbluebutton-html5/imports/ui/components/timer/timer-graphql/indicator/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/timer/timer-graphql/indicator/component.tsx @@ -107,6 +107,8 @@ const TimerIndicator: React.FC = ({ }, [running]); useEffect(() => { + if (!running) return; + const timePassed = passedTime >= 0 ? passedTime : 0; setTime((prev) => { @@ -114,7 +116,7 @@ const TimerIndicator: React.FC = ({ if (timePassed > prev) return timePassed; return prev; }); - }, [passedTime, stopwatch]); + }, [passedTime, stopwatch, startedAt]); useEffect(() => { if (!timeRef.current) { @@ -188,7 +190,7 @@ const TimerIndicatorContainer: React.FC = () => { const { timer } = timerData; const [currentTimer] = timer; - if (!currentTimer.active) return null; + if (!currentTimer?.active) return null; const { accumulated, From 82aa24164ac76fbb085626b33b889803bce92ac6 Mon Sep 17 00:00:00 2001 From: Paulo Lanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:45:13 -0300 Subject: [PATCH 095/512] build(bbb-webrtc-recorder): v0.6.0 * v0.6.0 * feat: recorder.writeToDevNull option to write files to /dev/null (testing) * fix: panic due to negative seqnums in sequence unwrapper --- bbb-webrtc-recorder.placeholder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-webrtc-recorder.placeholder.sh b/bbb-webrtc-recorder.placeholder.sh index e5ca651630..d30c36f9ac 100755 --- a/bbb-webrtc-recorder.placeholder.sh +++ b/bbb-webrtc-recorder.placeholder.sh @@ -1 +1 @@ -git clone --branch v0.5.2 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder +git clone --branch v0.6.0 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-recorder bbb-webrtc-recorder From 62cf89cf266e0ea7dae47ef2583a2a09dd9726c3 Mon Sep 17 00:00:00 2001 From: Paulo Lanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:47:06 -0300 Subject: [PATCH 096/512] build(bbb-webhooks): v3.0.0-beta.4 * fix: use ISO timestamps in production logs * chore: remove unused events * `rap-published`, `rap-unpublished`, `rap-deleted` * chore: support internal_meeting_id != record_id on rap events * !fix(webhooks): remove general getRaw configuration * fix(test): use redisUrl for node-redis client configuration * fix(test): pick up mocha configs via new .mocharc.yml file * build: set .nvmrc to lts/iron (Node.js 20) --- bbb-webhooks.placeholder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-webhooks.placeholder.sh b/bbb-webhooks.placeholder.sh index 9f4dbf861f..b69a58b4ec 100755 --- a/bbb-webhooks.placeholder.sh +++ b/bbb-webhooks.placeholder.sh @@ -1 +1 @@ -git clone --branch v3.0.0-beta.3 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks +git clone --branch v3.0.0-beta.4 --depth 1 https://github.com/bigbluebutton/bbb-webhooks bbb-webhooks From da31a8d0f7b557909ff1b1c9dadce0157ce11923 Mon Sep 17 00:00:00 2001 From: Paulo Lanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:52:21 -0300 Subject: [PATCH 097/512] build(bbb-webrtc-sfu): v2.13.0-alpha.1 * feat: add inbound queue size and job failure metrics * feat: add dry-run recording mode * feat: add time_to_mute/unmute metrics * fix(audio): log and track metrics for hold/unhold timeouts * fix(bbb-webrtc-recorder): exception when removing nullish recording callbacks * fix(mediasoup): check for null producers * fix(screenshare): resolve subscriberAnswer job * fix(audio): prevent false positives in TLO toggle metrics * refactor: replace logger lib, Winston -> Pino * Winston's been problematic (missing logs) and Pino is more performant * build: bump Docker and nvmrc to Node.js 20 (LTS) --- bbb-webrtc-sfu.placeholder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-webrtc-sfu.placeholder.sh b/bbb-webrtc-sfu.placeholder.sh index c1b39d78a9..eb42fb30e0 100755 --- a/bbb-webrtc-sfu.placeholder.sh +++ b/bbb-webrtc-sfu.placeholder.sh @@ -1 +1 @@ -git clone --branch v2.12.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu +git clone --branch v2.13.0-alpha.1 --depth 1 https://github.com/bigbluebutton/bbb-webrtc-sfu bbb-webrtc-sfu From 535703b644e54685e16587f4e1210208f0feb1df Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Thu, 18 Jan 2024 23:25:44 -0300 Subject: [PATCH 098/512] Add missing column to sharedNotes_diff --- bbb-graphql-server/bbb_schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index 6d8360a18e..aa87f66697 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1501,7 +1501,7 @@ create table "sharedNotes_rev" ( --create view "v_sharedNotes_rev" as select * from "sharedNotes_rev"; create view "v_sharedNotes_diff" as -select "meetingId", "sharedNotesExtId", "userId", "start", "end", "diff" +select "meetingId", "sharedNotesExtId", "userId", "start", "end", "diff", "rev" from "sharedNotes_rev" where "diff" is not null; From 5118d1f68b3c9786f26874a7907a80f2c25ecd07 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Thu, 18 Jan 2024 23:31:06 -0300 Subject: [PATCH 099/512] Add actions to delete/reset plugin data channel --- .../org/bigbluebutton/ClientSettings.scala | 36 ++++++++-- ...luginDataChannelDeleteMessageMsgHdlr.scala | 60 +++++++++++++++++ ...inDataChannelDispatchMessageMsgHdlr.scala} | 4 +- .../PluginDataChannelResetMsgHdlr.scala | 50 ++++++++++++++ .../core/apps/plugin/PluginHdlrs.scala | 4 +- .../core/db/PluginDataChannelMessageDAO.scala | 58 +++++++++++++++- .../senders/ReceivedJsonMsgHandlerActor.scala | 10 ++- .../core/running/MeetingActor.scala | 4 +- .../common2/msgs/PluginMsgs.scala | 22 ++++++- .../actions/pluginDataChannelDeleteMessage.ts | 25 +++++++ ...ts => pluginDataChannelDispatchMessage.ts} | 2 +- .../src/actions/pluginDataChannelReset.ts | 24 +++++++ .../src/PluginDataChannel.js | 66 +++++++++++++++---- bbb-graphql-server/bbb_schema.sql | 10 +-- bbb-graphql-server/metadata/actions.graphql | 35 +++++++--- bbb-graphql-server/metadata/actions.yaml | 24 +++++-- 16 files changed, 383 insertions(+), 51 deletions(-) create mode 100755 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteMessageMsgHdlr.scala rename akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/{DispatchPluginDataChannelMessageMsgHdlr.scala => PluginDataChannelDispatchMessageMsgHdlr.scala} (92%) create mode 100755 akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala create mode 100644 bbb-graphql-actions/src/actions/pluginDataChannelDeleteMessage.ts rename bbb-graphql-actions/src/actions/{dispatchPluginDataChannelMessageMsg.ts => pluginDataChannelDispatchMessage.ts} (93%) create mode 100644 bbb-graphql-actions/src/actions/pluginDataChannelReset.ts diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala index ce9edb046b..53000a6507 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala @@ -117,13 +117,37 @@ object ClientSettings extends SystemConfiguration { for { dataChannel <- dataChannels } yield { - if (dataChannel.contains("name") && dataChannel.contains("writePermission")) { + if (dataChannel.contains("name")) { val channelName = dataChannel("name").toString - val writePermission = dataChannel("writePermission") - writePermission match { - case wPerm: List[String] => pluginDataChannels += (channelName -> DataChannel(channelName, wPerm)) - case _ => logger.warn(s"Invalid writePermission for channel $channelName in plugin $pluginName") + val writePermission = { + if (dataChannel.contains("writePermission")) { + dataChannel("writePermission") match { + case wPerm: List[String] => wPerm + case _ => { + logger.warn(s"Invalid writePermission for channel $channelName in plugin $pluginName") + List() + } + } + } else { + logger.warn(s"Missing config writePermission for channel $channelName in plugin $pluginName") + List() + } } + val deletePermission = { + if (dataChannel.contains("deletePermission")) { + dataChannel("deletePermission") match { + case dPerm: List[String] => dPerm + case _ => { + logger.warn(s"Invalid deletePermission for channel $channelName in plugin $pluginName") + List() + } + } + } else { + List() + } + } + + pluginDataChannels += (channelName -> DataChannel(channelName, writePermission, deletePermission)) } } case _ => logger.warn(s"Plugin $pluginName has an invalid dataChannels format") @@ -139,7 +163,7 @@ object ClientSettings extends SystemConfiguration { pluginsFromConfig } - case class DataChannel(name: String, writePermission: List[String]) + case class DataChannel(name: String, writePermission: List[String], deletePermission: List[String]) case class Plugin(name: String, url: String, dataChannels: Map[String, DataChannel]) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteMessageMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteMessageMsgHdlr.scala new file mode 100755 index 0000000000..776a749566 --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDeleteMessageMsgHdlr.scala @@ -0,0 +1,60 @@ +package org.bigbluebutton.core.apps.plugin + +import org.bigbluebutton.ClientSettings +import org.bigbluebutton.common2.msgs.PluginDataChannelDeleteMessageMsg +import org.bigbluebutton.core.db.PluginDataChannelMessageDAO +import org.bigbluebutton.core.domain.MeetingState2x +import org.bigbluebutton.core.models.{ Roles, Users2x } +import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } + +trait PluginDataChannelDeleteMessageMsgHdlr extends HandlerHelpers { + + def handle(msg: PluginDataChannelDeleteMessageMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { + val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") + val meetingId = liveMeeting.props.meetingProp.intId + + for { + _ <- if (!pluginsDisabled) Some(()) else None + user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) + } yield { + val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile) + + if (!pluginsConfig.contains(msg.body.pluginName)) { + println(s"Plugin '${msg.body.pluginName}' not found.") + } else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.dataChannel)) { + println(s"Data channel '${msg.body.dataChannel}' not found in plugin '${msg.body.pluginName}'.") + } else { + val hasPermission = for { + deletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.dataChannel).deletePermission + } yield { + deletePermission.toLowerCase match { + case "all" => true + case "moderator" => user.role == Roles.MODERATOR_ROLE + case "presenter" => user.presenter + case "sender" => { + val senderUserId = PluginDataChannelMessageDAO.getMessageSender( + meetingId, + msg.body.pluginName, + msg.body.dataChannel, + msg.body.messageId + ) + senderUserId == msg.header.userId + } + case _ => false + } + } + + if (!hasPermission.contains(true)) { + println(s"No permission to delete in plugin: '${msg.body.pluginName}', data channel: '${msg.body.dataChannel}'.") + } else { + PluginDataChannelMessageDAO.delete( + meetingId, + msg.body.pluginName, + msg.body.dataChannel, + msg.body.messageId + ) + } + } + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/DispatchPluginDataChannelMessageMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDispatchMessageMsgHdlr.scala similarity index 92% rename from akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/DispatchPluginDataChannelMessageMsgHdlr.scala rename to akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDispatchMessageMsgHdlr.scala index db5a04c05e..7ac825961e 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/DispatchPluginDataChannelMessageMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelDispatchMessageMsgHdlr.scala @@ -7,9 +7,9 @@ import org.bigbluebutton.core.domain.MeetingState2x import org.bigbluebutton.core.models.{ Roles, Users2x } import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } -trait DispatchPluginDataChannelMessageMsgHdlr extends HandlerHelpers { +trait PluginDataChannelDispatchMessageMsgHdlr extends HandlerHelpers { - def handle(msg: DispatchPluginDataChannelMessageMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { + def handle(msg: PluginDataChannelDispatchMessageMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") val meetingId = liveMeeting.props.meetingProp.intId diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala new file mode 100755 index 0000000000..bec7301a0d --- /dev/null +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginDataChannelResetMsgHdlr.scala @@ -0,0 +1,50 @@ +package org.bigbluebutton.core.apps.plugin + +import org.bigbluebutton.ClientSettings +import org.bigbluebutton.common2.msgs.PluginDataChannelResetMsg +import org.bigbluebutton.core.db.PluginDataChannelMessageDAO +import org.bigbluebutton.core.domain.MeetingState2x +import org.bigbluebutton.core.models.{ Roles, Users2x } +import org.bigbluebutton.core.running.{ HandlerHelpers, LiveMeeting } + +trait PluginDataChannelResetMsgHdlr extends HandlerHelpers { + + def handle(msg: PluginDataChannelResetMsg, state: MeetingState2x, liveMeeting: LiveMeeting): Unit = { + val pluginsDisabled: Boolean = liveMeeting.props.meetingProp.disabledFeatures.contains("plugins") + val meetingId = liveMeeting.props.meetingProp.intId + + for { + _ <- if (!pluginsDisabled) Some(()) else None + user <- Users2x.findWithIntId(liveMeeting.users2x, msg.header.userId) + } yield { + val pluginsConfig = ClientSettings.getPluginsFromConfig(ClientSettings.clientSettingsFromFile) + + if (!pluginsConfig.contains(msg.body.pluginName)) { + println(s"Plugin '${msg.body.pluginName}' not found.") + } else if (!pluginsConfig(msg.body.pluginName).dataChannels.contains(msg.body.dataChannel)) { + println(s"Data channel '${msg.body.dataChannel}' not found in plugin '${msg.body.pluginName}'.") + } else { + val hasPermission = for { + deletePermission <- pluginsConfig(msg.body.pluginName).dataChannels(msg.body.dataChannel).deletePermission + } yield { + deletePermission.toLowerCase match { + case "all" => true + case "moderator" => user.role == Roles.MODERATOR_ROLE + case "presenter" => user.presenter + case _ => false + } + } + + if (!hasPermission.contains(true)) { + println(s"No permission to delete (reset) in plugin: '${msg.body.pluginName}', data channel: '${msg.body.dataChannel}'.") + } else { + PluginDataChannelMessageDAO.reset( + meetingId, + msg.body.pluginName, + msg.body.dataChannel + ) + } + } + } + } +} diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrs.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrs.scala index d706a99644..2fed179446 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrs.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/plugin/PluginHdlrs.scala @@ -4,7 +4,9 @@ import org.apache.pekko.actor.ActorContext import org.apache.pekko.event.Logging class PluginHdlrs(implicit val context: ActorContext) - extends DispatchPluginDataChannelMessageMsgHdlr { + extends PluginDataChannelDispatchMessageMsgHdlr + with PluginDataChannelDeleteMessageMsgHdlr + with PluginDataChannelResetMsgHdlr { val log = Logging(context.system, getClass) } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDataChannelMessageDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDataChannelMessageDAO.scala index 26602526de..bafc8d96a7 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDataChannelMessageDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/PluginDataChannelMessageDAO.scala @@ -1,9 +1,13 @@ package org.bigbluebutton.core.db import PostgresProfile.api._ +import org.bigbluebutton.core.db.DatabaseConnection.{db, logger} import spray.json.JsValue + import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.{Await, Future} import scala.util.{Failure, Success} +import scala.concurrent.duration.Duration object Permission { val allowedRoles = List("MODERATOR","VIEWER","PRESENTER") @@ -19,6 +23,7 @@ case class PluginDataChannelMessageDbModel( toRoles: Option[List[String]], toUserIds: Option[List[String]], createdAt: java.sql.Timestamp, + deletedAt: Option[java.sql.Timestamp], ) class PluginDataChannelMessageDbTableDef(tag: Tag) extends Table[PluginDataChannelMessageDbModel](tag, None, "pluginDataChannelMessage") { @@ -31,7 +36,8 @@ class PluginDataChannelMessageDbTableDef(tag: Tag) extends Table[PluginDataChann val toRoles = column[Option[List[String]]]("toRoles") val toUserIds = column[Option[List[String]]]("toUserIds") val createdAt = column[java.sql.Timestamp]("createdAt") - override def * = (meetingId, pluginName, dataChannel, payloadJson, fromUserId, toRoles, toUserIds, createdAt) <> (PluginDataChannelMessageDbModel.tupled, PluginDataChannelMessageDbModel.unapply) + val deletedAt = column[Option[java.sql.Timestamp]]("deletedAt") + override def * = (meetingId, pluginName, dataChannel, payloadJson, fromUserId, toRoles, toUserIds, createdAt, deletedAt) <> (PluginDataChannelMessageDbModel.tupled, PluginDataChannelMessageDbModel.unapply) } object PluginDataChannelMessageDAO { @@ -49,7 +55,8 @@ object PluginDataChannelMessageDAO { case filtered => Some(filtered) }, toUserIds = if(toUserIds.isEmpty) None else Some(toUserIds), - createdAt = new java.sql.Timestamp(System.currentTimeMillis()) + createdAt = new java.sql.Timestamp(System.currentTimeMillis()), + deletedAt = None ) ) ).onComplete { @@ -57,4 +64,51 @@ object PluginDataChannelMessageDAO { case Failure(e) => DatabaseConnection.logger.debug(s"Error inserting PluginDataChannelMessage: $e") } } + + def reset(meetingId: String, pluginName: String, dataChannel: String) = { + DatabaseConnection.db.run( + TableQuery[PluginDataChannelMessageDbTableDef] + .filter(_.meetingId === meetingId) + .filter(_.pluginName === pluginName) + .filter(_.dataChannel === dataChannel) + .map(u => (u.deletedAt)) + .update(Some(new java.sql.Timestamp(System.currentTimeMillis()))) + ).onComplete { + case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated deleted=now() on pluginDataChannelMessage table!") + case Failure(e) => DatabaseConnection.logger.error(s"Error updating deleted=now() pluginDataChannelMessage: $e") + } + } + + def getMessageSender(meetingId: String, pluginName: String, dataChannel: String, messageId: String): String = { + val query = sql"""SELECT "fromUserId" + FROM "pluginDataChannelMessage" + WHERE "deletedAt" is null + AND "meetingId" = ${meetingId} + AND "pluginName" = ${pluginName} + AND "dataChannel" = ${dataChannel} + AND "messageId" = ${messageId}""".as[String].headOption + + Await.result(DatabaseConnection.db.run(query), Duration.Inf) match { + case Some(userId) => userId + case None => { + logger.debug("Message {} not found in database (maybe it was deleted).", messageId) + "" + } + } + } + + def delete(meetingId: String, pluginName: String, dataChannel: String, messageId: String) = { + DatabaseConnection.db.run( + sqlu"""UPDATE "pluginDataChannelMessage" SET + "deletedAt" = current_timestamp + WHERE "meetingId" = ${meetingId} + AND "pluginName" = ${pluginName} + AND "dataChannel" = ${dataChannel} + AND "messageId" = ${messageId}""" + ).onComplete { + case Success(rowsAffected) => DatabaseConnection.logger.debug(s"$rowsAffected row(s) updated deleted=now() on pluginDataChannelMessage table!") + case Failure(e) => DatabaseConnection.logger.debug(s"Error updating deleted=now() pluginDataChannelMessage: $e") + } + } + } \ No newline at end of file diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala index d122c73886..d6e93fba7d 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/pubsub/senders/ReceivedJsonMsgHandlerActor.scala @@ -416,8 +416,14 @@ class ReceivedJsonMsgHandlerActor( routeGenericMsg[CreateGroupChatReqMsg](envelope, jsonNode) //Plugin - case DispatchPluginDataChannelMessageMsg.NAME => - routeGenericMsg[DispatchPluginDataChannelMessageMsg](envelope, jsonNode) + case PluginDataChannelDispatchMessageMsg.NAME => + routeGenericMsg[PluginDataChannelDispatchMessageMsg](envelope, jsonNode) + + case PluginDataChannelDeleteMessageMsg.NAME => + routeGenericMsg[PluginDataChannelDeleteMessageMsg](envelope, jsonNode) + + case PluginDataChannelResetMsg.NAME => + routeGenericMsg[PluginDataChannelResetMsg](envelope, jsonNode) // ExternalVideo case StartExternalVideoPubMsg.NAME => diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index 20769242c8..46e9742747 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -594,7 +594,9 @@ class MeetingActor( updateUserLastActivity(m.body.msg.sender.id) // Plugin - case m: DispatchPluginDataChannelMessageMsg => pluginHdlrs.handle(m, state, liveMeeting) + case m: PluginDataChannelDispatchMessageMsg => pluginHdlrs.handle(m, state, liveMeeting) + case m: PluginDataChannelDeleteMessageMsg => pluginHdlrs.handle(m, state, liveMeeting) + case m: PluginDataChannelResetMsg => pluginHdlrs.handle(m, state, liveMeeting) // Webcams case m: UserBroadcastCamStartMsg => webcamApp2x.handle(m, liveMeeting, msgBus) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PluginMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PluginMsgs.scala index e263923105..dc80f351b7 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PluginMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/PluginMsgs.scala @@ -5,12 +5,28 @@ package org.bigbluebutton.common2.msgs /** * Sent from graphql-actions to bbb-akka */ -object DispatchPluginDataChannelMessageMsg { val NAME = "DispatchPluginDataChannelMessageMsg" } -case class DispatchPluginDataChannelMessageMsg(header: BbbClientMsgHeader, body: DispatchPluginDataChannelMessageMsgBody) extends StandardMsg -case class DispatchPluginDataChannelMessageMsgBody( +object PluginDataChannelDispatchMessageMsg { val NAME = "PluginDataChannelDispatchMessageMsg" } +case class PluginDataChannelDispatchMessageMsg(header: BbbClientMsgHeader, body: PluginDataChannelDispatchMessageMsgBody) extends StandardMsg +case class PluginDataChannelDispatchMessageMsgBody( pluginName: String, dataChannel: String, payloadJson: String, toRoles: List[String], toUserIds: List[String], ) + +object PluginDataChannelDeleteMessageMsg { val NAME = "PluginDataChannelDeleteMessageMsg" } +case class PluginDataChannelDeleteMessageMsg(header: BbbClientMsgHeader, body: PluginDataChannelDeleteMessageMsgBody) extends StandardMsg +case class PluginDataChannelDeleteMessageMsgBody( + pluginName: String, + dataChannel: String, + messageId: String + ) + + +object PluginDataChannelResetMsg { val NAME = "PluginDataChannelResetMsg" } +case class PluginDataChannelResetMsg(header: BbbClientMsgHeader, body: PluginDataChannelResetMsgBody) extends StandardMsg +case class PluginDataChannelResetMsgBody( + pluginName: String, + dataChannel: String + ) diff --git a/bbb-graphql-actions/src/actions/pluginDataChannelDeleteMessage.ts b/bbb-graphql-actions/src/actions/pluginDataChannelDeleteMessage.ts new file mode 100644 index 0000000000..892d0c0706 --- /dev/null +++ b/bbb-graphql-actions/src/actions/pluginDataChannelDeleteMessage.ts @@ -0,0 +1,25 @@ +import { RedisMessage } from '../types'; +import { ValidationError } from '../types/ValidationError'; + +export default function buildRedisMessage(sessionVariables: Record, input: Record): RedisMessage { + const eventName = `PluginDataChannelDeleteMessageMsg`; + + const routing = { + meetingId: sessionVariables['x-hasura-meetingid'] as String, + userId: sessionVariables['x-hasura-userid'] as String + }; + + const header = { + name: eventName, + meetingId: routing.meetingId, + userId: routing.userId + }; + + const body = { + pluginName: input.pluginName, + dataChannel: input.dataChannel, + messageId: input.messageId + }; + + return { eventName, routing, header, body }; +} diff --git a/bbb-graphql-actions/src/actions/dispatchPluginDataChannelMessageMsg.ts b/bbb-graphql-actions/src/actions/pluginDataChannelDispatchMessage.ts similarity index 93% rename from bbb-graphql-actions/src/actions/dispatchPluginDataChannelMessageMsg.ts rename to bbb-graphql-actions/src/actions/pluginDataChannelDispatchMessage.ts index 2008398483..0a380d028c 100644 --- a/bbb-graphql-actions/src/actions/dispatchPluginDataChannelMessageMsg.ts +++ b/bbb-graphql-actions/src/actions/pluginDataChannelDispatchMessage.ts @@ -2,7 +2,7 @@ import { RedisMessage } from '../types'; import { ValidationError } from '../types/ValidationError'; export default function buildRedisMessage(sessionVariables: Record, input: Record): RedisMessage { - const eventName = `DispatchPluginDataChannelMessageMsg`; + const eventName = `PluginDataChannelDispatchMessageMsg`; const routing = { meetingId: sessionVariables['x-hasura-meetingid'] as String, diff --git a/bbb-graphql-actions/src/actions/pluginDataChannelReset.ts b/bbb-graphql-actions/src/actions/pluginDataChannelReset.ts new file mode 100644 index 0000000000..11045e9f06 --- /dev/null +++ b/bbb-graphql-actions/src/actions/pluginDataChannelReset.ts @@ -0,0 +1,24 @@ +import { RedisMessage } from '../types'; +import { ValidationError } from '../types/ValidationError'; + +export default function buildRedisMessage(sessionVariables: Record, input: Record): RedisMessage { + const eventName = `PluginDataChannelResetMsg`; + + const routing = { + meetingId: sessionVariables['x-hasura-meetingid'] as String, + userId: sessionVariables['x-hasura-userid'] as String + }; + + const header = { + name: eventName, + meetingId: routing.meetingId, + userId: routing.userId + }; + + const body = { + pluginName: input.pluginName, + dataChannel: input.dataChannel + }; + + return { eventName, routing, header, body }; +} diff --git a/bbb-graphql-client-test/src/PluginDataChannel.js b/bbb-graphql-client-test/src/PluginDataChannel.js index 18d3f3b460..711f75a1d5 100644 --- a/bbb-graphql-client-test/src/PluginDataChannel.js +++ b/bbb-graphql-client-test/src/PluginDataChannel.js @@ -5,9 +5,9 @@ import {useState} from "react"; export default function PluginDataChannel({userId}) { const [textAreaValue, setTextAreaValue] = useState(``); - const [dispatchPluginDataChannelMessage] = useMutation(gql` - mutation DispatchPluginDataChannelMessageMsg($pluginName: String!, $dataChannel: String!, $payloadJson: String!, $toRoles: [String]!,$toUserIds: [String]!) { - dispatchPluginDataChannelMessageMsg( + const [pluginDataChannelDispatchMessage] = useMutation(gql` + mutation PluginDataChannelDispatchMessage($pluginName: String!, $dataChannel: String!, $payloadJson: String!, $toRoles: [String]!,$toUserIds: [String]!) { + pluginDataChannelDispatchMessage( pluginName: $pluginName, dataChannel: $dataChannel, payloadJson: $payloadJson, @@ -16,12 +16,12 @@ export default function PluginDataChannel({userId}) { ) } `); - const handleDispatchPluginDataChannelMessage = (roles, userIds) => { + const handlePluginDataChannelDispatchMessage = (roles, userIds) => { if (textAreaValue.trim() !== '') { - dispatchPluginDataChannelMessage({ + pluginDataChannelDispatchMessage({ variables: { - pluginName: 'SamplePresentationToolbarPlugin', - dataChannel: 'public-channel', + pluginName: 'SelectRandomUserPlugin', + dataChannel: 'pickRandomUser', payloadJson: textAreaValue, toRoles: roles, toUserIds: userIds, @@ -30,6 +30,42 @@ export default function PluginDataChannel({userId}) { } }; + const [pluginDataChannelReset] = useMutation(gql` + mutation PluginDataChannelReset($pluginName: String!, $dataChannel: String!) { + pluginDataChannelReset( + pluginName: $pluginName, + dataChannel: $dataChannel + ) + } + `); + const handlePluginDataChannelReset = () => { + pluginDataChannelReset({ + variables: { + pluginName: 'SelectRandomUserPlugin', + dataChannel: 'pickRandomUser' + }, + }); + }; + + const [pluginDataChannelDeleteMessage] = useMutation(gql` + mutation PluginDataChannelDeleteMessage($pluginName: String!, $dataChannel: String!, $messageId: String!) { + pluginDataChannelDeleteMessage( + pluginName: $pluginName, + dataChannel: $dataChannel, + messageId: $messageId + ) + } + `); + const handlePluginDataChannelDeleteMessage = (messageId) => { + pluginDataChannelDeleteMessage({ + variables: { + pluginName: 'SelectRandomUserPlugin', + dataChannel: 'pickRandomUser', + messageId: messageId + }, + }); + }; + const { loading, error, data } = usePatchedSubscription( gql`subscription { pluginDataChannelMessage(order_by: {createdAt: asc}) { @@ -74,6 +110,9 @@ export default function PluginDataChannel({userId}) { + ); })} @@ -86,12 +125,13 @@ export default function PluginDataChannel({userId}) { value={textAreaValue} onChange={(e) => setTextAreaValue(e.target.value)} > - - - - - - + + + + + + + diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index 6d8360a18e..0450e87f8f 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1501,7 +1501,7 @@ create table "sharedNotes_rev" ( --create view "v_sharedNotes_rev" as select * from "sharedNotes_rev"; create view "v_sharedNotes_diff" as -select "meetingId", "sharedNotesExtId", "userId", "start", "end", "diff" +select "meetingId", "sharedNotesExtId", "userId", "start", "end", "diff", "rev" from "sharedNotes_rev" where "diff" is not null; @@ -1585,12 +1585,13 @@ CREATE TABLE "pluginDataChannelMessage" ( "fromUserId" varchar(50) REFERENCES "user"("userId") ON DELETE CASCADE, "toRoles" varchar[], --MODERATOR, VIEWER, PRESENTER "toUserIds" varchar[], - "createdAt" timestamp with time ZONE DEFAULT current_timestamp, + "createdAt" timestamp with time zone DEFAULT current_timestamp, + "deletedAt" timestamp with time zone, CONSTRAINT "pluginDataChannel_pkey" PRIMARY KEY ("meetingId","pluginName","dataChannel","messageId") ); -create index "idx_pluginDataChannelMessage_dataChannel" on "pluginDataChannelMessage"("meetingId", "pluginName", "dataChannel", "toRoles", "toUserIds", "createdAt"); -create index "idx_pluginDataChannelMessage_roles" on "pluginDataChannelMessage"("meetingId", "toRoles", "toUserIds", "createdAt"); +create index "idx_pluginDataChannelMessage_dataChannel" on "pluginDataChannelMessage"("meetingId", "pluginName", "dataChannel", "toRoles", "toUserIds", "createdAt") where "deletedAt" is null; +create index "idx_pluginDataChannelMessage_roles" on "pluginDataChannelMessage"("meetingId", "toRoles", "toUserIds", "createdAt") where "deletedAt" is null; CREATE OR REPLACE VIEW "v_pluginDataChannelMessage" AS SELECT u."meetingId", u."userId", m."pluginName", m."dataChannel", m."messageId", m."payloadJson", m."fromUserId", m."toRoles", m."createdAt" @@ -1601,6 +1602,7 @@ JOIN "pluginDataChannelMessage" m ON m."meetingId" = u."meetingId" OR u."role" = ANY(m."toRoles") OR (u."presenter" AND 'PRESENTER' = ANY(m."toRoles")) ) +WHERE "deletedAt" is null ORDER BY m."createdAt"; ------------------------ diff --git a/bbb-graphql-server/metadata/actions.graphql b/bbb-graphql-server/metadata/actions.graphql index ed7fddf9fb..cb2eae2798 100644 --- a/bbb-graphql-server/metadata/actions.graphql +++ b/bbb-graphql-server/metadata/actions.graphql @@ -100,16 +100,6 @@ type Mutation { ): Boolean } -type Mutation { - dispatchPluginDataChannelMessageMsg( - pluginName: String! - dataChannel: String! - payloadJson: String! - toRoles: [String]! - toUserIds: [String]! - ): Boolean -} - type Mutation { externalVideoStart( externalVideoUrl: String! @@ -210,6 +200,31 @@ type Mutation { ): Boolean } +type Mutation { + pluginDataChannelDeleteMessage( + pluginName: String! + dataChannel: String! + messageId: String! + ): Boolean +} + +type Mutation { + pluginDataChannelDispatchMessage( + pluginName: String! + dataChannel: String! + payloadJson: String! + toRoles: [String]! + toUserIds: [String]! + ): Boolean +} + +type Mutation { + pluginDataChannelReset( + pluginName: String! + dataChannel: String! + ): Boolean +} + type Mutation { pollCancel: Boolean } diff --git a/bbb-graphql-server/metadata/actions.yaml b/bbb-graphql-server/metadata/actions.yaml index ddfbbd7238..3a87de56bd 100644 --- a/bbb-graphql-server/metadata/actions.yaml +++ b/bbb-graphql-server/metadata/actions.yaml @@ -95,12 +95,6 @@ actions: handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' permissions: - role: bbb_client - - name: dispatchPluginDataChannelMessageMsg - definition: - kind: synchronous - handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' - permissions: - - role: bbb_client - name: externalVideoStart definition: kind: synchronous @@ -185,6 +179,24 @@ actions: handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' permissions: - role: bbb_client + - name: pluginDataChannelDeleteMessage + definition: + kind: synchronous + handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' + permissions: + - role: bbb_client + - name: pluginDataChannelDispatchMessage + definition: + kind: synchronous + handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' + permissions: + - role: bbb_client + - name: pluginDataChannelReset + definition: + kind: synchronous + handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' + permissions: + - role: bbb_client - name: pollCancel definition: kind: synchronous From 2ffd9ce7e9d11f6764c3784f90a213e1fa7f1096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 19 Jan 2024 10:44:27 -0300 Subject: [PATCH 100/512] migrate startWatchingExternalVideo action --- .../api/external-videos/server/methods.js | 2 - .../methods/startWatchingExternalVideo.js | 26 ------------ .../modal/component.tsx | 24 ++++++++++- .../modal/service.ts | 17 -------- .../external-video-player/mutations.tsx | 11 +++++ .../external-video-player/service.js | 19 --------- .../smart-video-share/component.jsx | 12 +++--- .../smart-video-share/container.jsx | 40 +++++++++++++++---- 8 files changed, 73 insertions(+), 78 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/external-videos/server/methods/startWatchingExternalVideo.js create mode 100644 bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods.js b/bigbluebutton-html5/imports/api/external-videos/server/methods.js index 10c056a2a9..be2877d4dd 100644 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods.js +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods.js @@ -1,10 +1,8 @@ import { Meteor } from 'meteor/meteor'; -import startWatchingExternalVideo from './methods/startWatchingExternalVideo'; import stopWatchingExternalVideo from './methods/stopWatchingExternalVideo'; import emitExternalVideoEvent from './methods/emitExternalVideoEvent'; Meteor.methods({ - startWatchingExternalVideo, stopWatchingExternalVideo, emitExternalVideoEvent, }); diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods/startWatchingExternalVideo.js b/bigbluebutton-html5/imports/api/external-videos/server/methods/startWatchingExternalVideo.js deleted file mode 100644 index cd579dabc5..0000000000 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods/startWatchingExternalVideo.js +++ /dev/null @@ -1,26 +0,0 @@ -import { check } from 'meteor/check'; -import Logger from '/imports/startup/server/logger'; -import RedisPubSub from '/imports/startup/server/redis'; -import { extractCredentials } from '/imports/api/common/server/helpers'; - -export default function startWatchingExternalVideo(externalVideoUrl) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'StartExternalVideoPubMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(externalVideoUrl, String); - - const payload = { externalVideoUrl }; - - Logger.info(`User ${requesterUserId} sharing an external video ${externalVideoUrl} for meeting ${meetingId}`); - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (error) { - Logger.error(`Error on sharing an external video for meeting ${meetingId}: ${error}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/component.tsx b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/component.tsx index 27f199d2de..202d760b5f 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/component.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import { useMutation } from '@apollo/client'; import Styled from './styles'; import SettingsSingleton from '/imports/ui/services/settings'; -import { startWatching, isUrlValid } from './service'; +import { isUrlValid } from './service'; +import { EXTERNAL_VIDEO_START } from '../../mutations'; const intlMessages = defineMessages({ start: { @@ -35,6 +37,9 @@ const intlMessages = defineMessages({ }, }); +const YOUTUBE_SHORTS_REGEX = new RegExp(/^(?:https?:\/\/)?(?:www\.)?(youtube\.com\/shorts)\/.+$/); +const PANOPTO_MATCH_URL = /https?:\/\/([^/]+\/Panopto)(\/Pages\/Viewer\.aspx\?id=)([-a-zA-Z0-9]+)/; + interface ExternalVideoPlayerModalProps { onRequestClose: () => void, priority: string, @@ -52,6 +57,23 @@ const ExternalVideoPlayerModal: React.FC = ({ // @ts-ignore - settings is a js singleton const { animations } = SettingsSingleton.application; const [videoUrl, setVideoUrl] = React.useState(''); + const [startExternalVideo] = useMutation(EXTERNAL_VIDEO_START); + + const startWatching = (url: string) => { + let externalVideoUrl = url; + + if (YOUTUBE_SHORTS_REGEX.test(url)) { + const shortsUrl = url.replace('shorts/', 'watch?v='); + externalVideoUrl = shortsUrl; + } else if (PANOPTO_MATCH_URL.test(url)) { + const m = url.match(PANOPTO_MATCH_URL); + if (m && m.length >= 4) { + externalVideoUrl = `https://${m[1]}/Podcast/Social/${m[3]}.mp4`; + } + } + + startExternalVideo({ variables: { externalVideoUrl } }); + }; const valid = isUrlValid(videoUrl); diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/service.ts b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/service.ts index ffc93d633e..a980a5b3b8 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/service.ts +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/service.ts @@ -8,22 +8,6 @@ export const stopWatching = () => { makeCall('stopWatchingExternalVideo'); }; -export const startWatching = (url: string) => { - let externalVideoUrl = url; - - if (YOUTUBE_SHORTS_REGEX.test(url)) { - const shortsUrl = url.replace('shorts/', 'watch?v='); - externalVideoUrl = shortsUrl; - } else if (PANOPTO_MATCH_URL.test(url)) { - const m = url.match(PANOPTO_MATCH_URL); - if (m && m.length >= 4) { - externalVideoUrl = `https://${m[1]}/Podcast/Social/${m[3]}.mp4`; - } - } - - makeCall('startWatchingExternalVideo', externalVideoUrl); -}; - export const isUrlValid = (url: string) => { if (YOUTUBE_SHORTS_REGEX.test(url)) { const shortsUrl = url.replace('shorts/', 'watch?v='); @@ -36,5 +20,4 @@ export const isUrlValid = (url: string) => { export default { stopWatching, - startWatching, }; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx b/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx new file mode 100644 index 0000000000..cc143f249a --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; + +export const EXTERNAL_VIDEO_START = gql` + mutation ExternalVideoStart($externalVideoUrl: String!) { + externalVideoStart( + externalVideoUrl: $externalVideoUrl + ) + } +`; + +export default { EXTERNAL_VIDEO_START }; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js index 4976f37529..5fc1064fec 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js @@ -2,8 +2,6 @@ import Auth from '/imports/ui/services/auth'; import { getStreamer } from '/imports/api/external-videos'; import { makeCall } from '/imports/ui/services/api'; -import NotesService from '/imports/ui/components/notes/service'; - import ReactPlayer from 'react-player'; import Panopto from './custom-players/panopto'; @@ -20,22 +18,6 @@ const isUrlValid = (url) => { return /^https.*$/.test(url) && (ReactPlayer.canPlay(url) || Panopto.canPlay(url)); }; -const startWatching = (url) => { - let externalVideoUrl = url; - - if (YOUTUBE_SHORTS_REGEX.test(url)) { - const shortsUrl = url.replace('shorts/', 'watch?v='); - externalVideoUrl = shortsUrl; - } else if (Panopto.canPlay(url)) { - externalVideoUrl = Panopto.getSocialUrl(url); - } - - // Close Shared Notes if open. - NotesService.pinSharedNotes(false); - - makeCall('startWatchingExternalVideo', externalVideoUrl); -}; - const stopWatching = () => { makeCall('stopWatchingExternalVideo'); }; @@ -87,7 +69,6 @@ export { onMessage, removeAllListeners, isUrlValid, - startWatching, stopWatching, getPlayingState, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/component.jsx index 8d4f470050..6b2f06e645 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/component.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages } from 'react-intl'; import { safeMatch } from '/imports/utils/string-utils'; -import { isUrlValid, startWatching } from '/imports/ui/components/external-video-player/service'; +import { isUrlValid } from '/imports/ui/components/external-video-player/service'; import BBBMenu from '/imports/ui/components/common/menu/component'; import Styled from './styles'; @@ -12,10 +12,10 @@ const intlMessages = defineMessages({ }, }); -const createAction = (url) => { - const hasHttps = url?.startsWith("https://"); +const createAction = (url, startWatching) => { + const hasHttps = url?.startsWith('https://'); const finalUrl = hasHttps ? url : `https://${url}`; - const label = hasHttps ? url?.replace("https://", "") : url; + const label = hasHttps ? url?.replace('https://', '') : url; if (isUrlValid(finalUrl)) { return { @@ -27,7 +27,7 @@ const createAction = (url) => { export const SmartMediaShare = (props) => { const { - currentSlide, intl, isMobile, isRTL, + currentSlide, intl, isMobile, isRTL, startWatching, } = props; const linkPatt = /(https?:\/\/.*?)(?=\s|$)/g; const externalLinks = safeMatch(linkPatt, currentSlide?.content?.replace(/[\r\n]/g, ' '), false); @@ -36,7 +36,7 @@ export const SmartMediaShare = (props) => { const actions = []; externalLinks?.forEach((l) => { - const action = createAction(l); + const action = createAction(l, startWatching); if (action) actions.push(action); }); diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/container.jsx index 0a28ce00fd..83913a47fb 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/smart-video-share/container.jsx @@ -1,16 +1,42 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; +import { useMutation } from '@apollo/client'; import { SmartMediaShare } from './component'; - +import NotesService from '/imports/ui/components/notes/service'; +import Panopto from '../../../external-video-player/custom-players/panopto'; import { layoutSelect } from '/imports/ui/components/layout/context'; import { isMobile } from '/imports/ui/components/layout/utils'; +import { EXTERNAL_VIDEO_START } from '../../../external-video-player/mutations'; -const SmartMediaShareContainer = (props) => ( - -); +const YOUTUBE_SHORTS_REGEX = new RegExp(/^(?:https?:\/\/)?(?:www\.)?(youtube\.com\/shorts)\/.+$/); + +const SmartMediaShareContainer = (props) => { + const [startExternalVideo] = useMutation(EXTERNAL_VIDEO_START); + + const startWatching = (url) => { + let externalVideoUrl = url; + + if (YOUTUBE_SHORTS_REGEX.test(url)) { + const shortsUrl = url.replace('shorts/', 'watch?v='); + externalVideoUrl = shortsUrl; + } else if (Panopto.canPlay(url)) { + externalVideoUrl = Panopto.getSocialUrl(url); + } + + // Close Shared Notes if open. + NotesService.pinSharedNotes(false); + + startExternalVideo({ variables: { externalVideoUrl } }); + }; + + return ( + + ); +}; export default withTracker(() => { const isRTL = layoutSelect((i) => i.isRTL); From 600fe50b731dc3c2d84b0fde11ee1e1458814735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 19 Jan 2024 11:46:11 -0300 Subject: [PATCH 101/512] migrate emitExternalVideoEvent action --- .../api/external-videos/server/methods.js | 2 - .../server/methods/emitExternalVideoEvent.js | 40 ----------------- .../component.tsx | 45 ++++++++++++++++++- .../external-video-player-graphql/service.ts | 36 --------------- .../external-video-player/mutations.tsx | 18 +++++++- .../external-video-player/service.js | 26 ----------- 6 files changed, 60 insertions(+), 107 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js delete mode 100644 bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/service.ts diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods.js b/bigbluebutton-html5/imports/api/external-videos/server/methods.js index be2877d4dd..710daa890b 100644 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods.js +++ b/bigbluebutton-html5/imports/api/external-videos/server/methods.js @@ -1,8 +1,6 @@ import { Meteor } from 'meteor/meteor'; import stopWatchingExternalVideo from './methods/stopWatchingExternalVideo'; -import emitExternalVideoEvent from './methods/emitExternalVideoEvent'; Meteor.methods({ stopWatchingExternalVideo, - emitExternalVideoEvent, }); diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js b/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js deleted file mode 100644 index 3169c96797..0000000000 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods/emitExternalVideoEvent.js +++ /dev/null @@ -1,40 +0,0 @@ -import { check } from 'meteor/check'; -import Logger from '/imports/startup/server/logger'; -import RedisPubSub from '/imports/startup/server/redis'; -import { extractCredentials } from '/imports/api/common/server/helpers'; - -export default function emitExternalVideoEvent(options) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'UpdateExternalVideoPubMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - - const { status, playerStatus } = options; - - check(status, String); - check(playerStatus, { - rate: Match.Maybe(Number), - time: Match.Maybe(Number), - state: Match.Maybe(Number), - }); - - const state = playerStatus.state || 0; - - const payload = { - status, - rate: playerStatus.rate || 0, - time: playerStatus.time || 0, - state, - }; - - Logger.debug(`User id=${requesterUserId} sending ${EVENT_NAME} event:${state} for meeting ${meetingId}`); - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method emitExternalVideoEvent ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/component.tsx index b34b391d4e..08bec6e428 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/component.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef } from 'react'; import ReactPlayer from 'react-player'; import { defineMessages, useIntl } from 'react-intl'; import audioManager from '/imports/ui/services/audio-manager'; -import { useReactiveVar } from '@apollo/client'; +import { useReactiveVar, useMutation } from '@apollo/client'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; import { OnProgressProps } from 'react-player/base'; @@ -24,8 +24,8 @@ import { uniqueId } from '/imports/utils/string-utils'; import useTimeSync from '/imports/ui/core/local-states/useTimeSync'; import ExternalVideoPlayerToolbar from './toolbar/component'; import deviceInfo from '/imports/utils/deviceInfo'; -import { sendMessage } from './service'; import { ACTIONS } from '../../layout/enums'; +import { EXTERNAL_VIDEO_UPDATE } from '../mutations'; import PeerTube from '../custom-players/peertube'; import { ArcPlayer } from '../custom-players/arc-player'; @@ -160,6 +160,47 @@ const ExternalVideoPlayer: React.FC = ({ const playerRef = useRef(); const playerParentRef = useRef(null); const timeoutRef = useRef>(); + + const [updateExternalVideo] = useMutation(EXTERNAL_VIDEO_UPDATE); + + let lastMessage: { + event: string; + rate: number; + time: number; + state?: string; + } = { event: '', rate: 0, time: 0 }; + + const sendMessage = (event: string, data: { rate: number; time: number; state?: string}) => { + // don't re-send repeated update messages + if ( + lastMessage.event === event + && lastMessage.time === data.time + ) { + return; + } + + // don't register to redis a viewer joined message + if (event === 'viewerJoined') { + return; + } + + lastMessage = { ...data, event }; + + // Use an integer for playing state + // 0: stopped 1: playing + // We might use more states in the future + const state = data.state ? 1 : 0; + + updateExternalVideo({ + variables: { + status: event, + rate: data?.rate, + time: data?.time, + state, + }, + }); + }; + useEffect(() => { timeoutRef.current = setTimeout(() => { setAutoPlayBlocked(true); diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/service.ts b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/service.ts deleted file mode 100644 index a7a721b9be..0000000000 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { makeCall } from '/imports/ui/services/api'; - -let lastMessage: { - event: string; - rate: number; - time: number; - state?: string; -} = { event: '', rate: 0, time: 0 }; - -export const sendMessage = (event: string, data: { rate: number; time: number; state?: string}) => { - // don't re-send repeated update messages - if ( - lastMessage.event === event - && lastMessage.time === data.time - ) { - return; - } - - // don't register to redis a viewer joined message - if (event === 'viewerJoined') { - return; - } - - lastMessage = { ...data, event }; - - // Use an integer for playing state - // 0: stopped 1: playing - // We might use more states in the future - const state = data.state ? 1 : 0; - - makeCall('emitExternalVideoEvent', { status: event, playerStatus: { ...data, state } }); -}; - -export default { - sendMessage, -}; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx b/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx index cc143f249a..78d44fbc59 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx @@ -8,4 +8,20 @@ export const EXTERNAL_VIDEO_START = gql` } `; -export default { EXTERNAL_VIDEO_START }; +export const EXTERNAL_VIDEO_UPDATE = gql` + mutation ExternalVideoUpdate( + $status: String! + $rate: Float!, + $time: Float!, + $state: Float!, + ) { + externalVideoUpdate( + status: $status, + rate: $rate, + time: $time, + state: $state, + ) + } +`; + +export default { EXTERNAL_VIDEO_START, EXTERNAL_VIDEO_UPDATE }; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js index 5fc1064fec..7d23437391 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js @@ -22,31 +22,6 @@ const stopWatching = () => { makeCall('stopWatchingExternalVideo'); }; -let lastMessage = null; - -const sendMessage = (event, data) => { - - // don't re-send repeated update messages - if (lastMessage && lastMessage.event === event - && event === 'playerUpdate' && lastMessage.time === data.time) { - return; - } - - // don't register to redis a viewer joined message - if (event === 'viewerJoined') { - return; - } - - lastMessage = { ...data, event }; - - // Use an integer for playing state - // 0: stopped 1: playing - // We might use more states in the future - data.state = data.state ? 1 : 0; - - makeCall('emitExternalVideoEvent', { status: event, playerStatus: data }); -}; - const onMessage = (message, func) => { const streamer = getStreamer(Auth.meetingID); streamer.on(message, func); @@ -65,7 +40,6 @@ const getPlayingState = (state) => { }; export { - sendMessage, onMessage, removeAllListeners, isUrlValid, From f4e5803b1514ee8c36aa7d7f995748b1c5f1a3a7 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Fri, 19 Jan 2024 13:36:20 -0300 Subject: [PATCH 102/512] Makes timer.accumulated be calculated on the backend --- .../org/bigbluebutton/core/apps/TimerModel.scala | 8 ++++++++ .../apps/timer/DeactivateTimerReqMsgHdlr.scala | 5 ++++- .../core/apps/timer/StartTimerReqMsgHdlr.scala | 2 +- .../core/apps/timer/StopTimerReqMsgHdlr.scala | 5 ++--- .../core/apps/timer/SwitchTimerReqMsgHdlr.scala | 1 + .../bigbluebutton/common2/msgs/TimerMsgs.scala | 2 +- bbb-graphql-actions/src/actions/timerStop.ts | 6 +----- bbb-graphql-server/metadata/actions.graphql | 4 +--- .../imports/ui/components/timer/component.jsx | 16 ++++------------ .../imports/ui/components/timer/container.jsx | 4 ++-- .../imports/ui/components/timer/mutations.jsx | 4 ++-- .../timer/timer-graphql/indicator/component.tsx | 8 +------- 12 files changed, 28 insertions(+), 37 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/TimerModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/TimerModel.scala index 2d5d4663ef..9924648a9e 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/TimerModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/TimerModel.scala @@ -45,6 +45,14 @@ object TimerModel { } def setRunning(model: TimerModel, running: Boolean): Unit = { + + //If it is running and will stop, calculate new Accumulated + if(getRunning(model) && !running) { + val now = System.currentTimeMillis() + val accumulated = getAccumulated(model) + Math.abs(now - getStartedAt(model)).toInt + this.setAccumulated(model, accumulated) + } + model.running = running } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/DeactivateTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/DeactivateTimerReqMsgHdlr.scala index 8b13394bfc..0035fdc2ea 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/DeactivateTimerReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/DeactivateTimerReqMsgHdlr.scala @@ -30,7 +30,10 @@ trait DeactivateTimerReqMsgHdlr extends RightsManagementTrait { val reason = "You need to be the presenter or moderator to deactivate timer" PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) } else { - TimerModel.setIsActive(liveMeeting.timerModel, false) + TimerModel.setRunning(liveMeeting.timerModel, running = false) + TimerModel.setIsActive(liveMeeting.timerModel, active = false) + TimerModel.setStopwatch(liveMeeting.timerModel, stopwatch = true) + TimerModel.reset(liveMeeting.timerModel) TimerDAO.update(liveMeeting.props.meetingProp.intId, liveMeeting.timerModel) broadcastEvent() } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StartTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StartTimerReqMsgHdlr.scala index aa1651fd53..d2d1a7b298 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StartTimerReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StartTimerReqMsgHdlr.scala @@ -31,7 +31,7 @@ trait StartTimerReqMsgHdlr extends RightsManagementTrait { PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) } else { TimerModel.setStartedAt(liveMeeting.timerModel, System.currentTimeMillis()) - TimerModel.setRunning(liveMeeting.timerModel, true) + TimerModel.setRunning(liveMeeting.timerModel, running = true) TimerDAO.update(liveMeeting.props.meetingProp.intId, liveMeeting.timerModel) broadcastEvent() } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StopTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StopTimerReqMsgHdlr.scala index 12317a0613..178f9641ac 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StopTimerReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/StopTimerReqMsgHdlr.scala @@ -33,10 +33,9 @@ trait StopTimerReqMsgHdlr extends RightsManagementTrait { val reason = "You need to be the presenter or moderator to stop timer" PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) } else { - TimerModel.setAccumulated(liveMeeting.timerModel, msg.body.accumulated) - TimerModel.setRunning(liveMeeting.timerModel, false) + TimerModel.setRunning(liveMeeting.timerModel, running = false) TimerDAO.update(liveMeeting.props.meetingProp.intId, liveMeeting.timerModel) - broadcastEvent(msg.body.accumulated) + broadcastEvent(TimerModel.getAccumulated(liveMeeting.timerModel)) } } } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SwitchTimerReqMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SwitchTimerReqMsgHdlr.scala index 3f897f04d1..526eb4a4c9 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SwitchTimerReqMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/timer/SwitchTimerReqMsgHdlr.scala @@ -34,6 +34,7 @@ trait SwitchTimerReqMsgHdlr extends RightsManagementTrait { } else { if (TimerModel.getStopwatch(liveMeeting.timerModel) != msg.body.stopwatch) { TimerModel.setStopwatch(liveMeeting.timerModel, msg.body.stopwatch) + TimerModel.setRunning(liveMeeting.timerModel, running = false) TimerModel.reset(liveMeeting.timerModel) //Reset on switch Stopwatch/Timer if (msg.body.stopwatch) { TimerModel.setTrack(liveMeeting.timerModel, "noTrack") diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/TimerMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/TimerMsgs.scala index 8e31df8412..34ebca4159 100644 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/TimerMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/TimerMsgs.scala @@ -19,7 +19,7 @@ case class StartTimerReqMsgBody() object StopTimerReqMsg { val NAME = "StopTimerReqMsg" } case class StopTimerReqMsg(header: BbbClientMsgHeader, body: StopTimerReqMsgBody) extends StandardMsg -case class StopTimerReqMsgBody(accumulated: Int) +case class StopTimerReqMsgBody() object SwitchTimerReqMsg { val NAME = "SwitchTimerReqMsg" } case class SwitchTimerReqMsg(header: BbbClientMsgHeader, body: SwitchTimerReqMsgBody) extends StandardMsg diff --git a/bbb-graphql-actions/src/actions/timerStop.ts b/bbb-graphql-actions/src/actions/timerStop.ts index 0acc6202c5..e38b1a6177 100644 --- a/bbb-graphql-actions/src/actions/timerStop.ts +++ b/bbb-graphql-actions/src/actions/timerStop.ts @@ -16,11 +16,7 @@ export default function buildRedisMessage(sessionVariables: Record { timerStart(); }; - const stopTimer = (accumulated) => { - timerStop({ variables: { accumulated } }); + const stopTimer = () => { + timerStop(); }; const switchTimer = (stopwatch) => { diff --git a/bigbluebutton-html5/imports/ui/components/timer/mutations.jsx b/bigbluebutton-html5/imports/ui/components/timer/mutations.jsx index 242d430eb6..b1a12bc1e4 100644 --- a/bigbluebutton-html5/imports/ui/components/timer/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/timer/mutations.jsx @@ -29,8 +29,8 @@ export const TIMER_START = gql` `; export const TIMER_STOP = gql` - mutation timerStop($accumulated: Float!) { - timerStop(accumulated: $accumulated) + mutation timerStop { + timerStop } `; diff --git a/bigbluebutton-html5/imports/ui/components/timer/timer-graphql/indicator/component.tsx b/bigbluebutton-html5/imports/ui/components/timer/timer-graphql/indicator/component.tsx index 17b4f64584..3bd5938c95 100644 --- a/bigbluebutton-html5/imports/ui/components/timer/timer-graphql/indicator/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/timer/timer-graphql/indicator/component.tsx @@ -53,13 +53,7 @@ const TimerIndicator: React.FC = ({ }; const stopTimer = () => { - stopTimerMutation( - { - variables: { - accumulated: time, - }, - }, - ); + stopTimerMutation(); }; useEffect(() => { From d30b806b47458ff481976752312f3101d4e843d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Barboza=20de=20S=C3=A1?= Date: Fri, 19 Jan 2024 13:42:01 -0300 Subject: [PATCH 103/512] test: Fix no-flaky tests and properly set the execution mode (#19436) * test: fix shortcuts, add flaky flag for test requiring graphql data, fix slide change for tldraw v2 * test: properly set the execution mode * test: use isMultiUser parameter inside options obj * test: fix banner color test * test: increase breakout test timeouts for user joining room * test: redo the change in the hide presentation on join test * test: change hide presentation steps and add flaky flag on it --- .../audio-controls/component.tsx | 6 ++--- .../imports/ui/core/hooks/useShortcut.tsx | 2 +- .../playwright/audio/audio.spec.js | 11 +++----- .../playwright/breakout/create.js | 2 +- .../playwright/breakout/join.js | 2 +- .../playwright/chat/chat.spec.js | 14 +++++----- bigbluebutton-tests/playwright/chat/util.js | 2 +- .../playwright/core/elements.js | 4 +-- .../playwright/core/helpers.js | 13 ++++++++++ bigbluebutton-tests/playwright/core/page.js | 1 - .../playwright/layouts/layouts.spec.js | 13 +++------- .../learningdashboard.spec.js | 10 +++---- .../notifications/notifications.spec.js | 3 ++- .../playwright/options/options.spec.js | 12 ++++----- .../playwright/parameters/constants.js | 12 ++++----- .../playwright/parameters/customparameters.js | 21 ++++++++------- .../playwright/parameters/parameters.spec.js | 26 ++++++++++++------- .../playwright/parameters/util.js | 16 ++++++------ .../playwright/polling/poll.js | 24 ++++++++++------- .../playwright/polling/polling.spec.js | 13 ++++------ .../playwright/presentation/presentation.js | 1 - .../playwright/presentation/util.js | 25 +++++++++++++----- .../reconnection/reconnection.spec.js | 5 +--- .../screenshare/screenshare.spec.js | 3 ++- .../sharednotes/sharednotes.spec.js | 13 +++++----- 25 files changed, 138 insertions(+), 116 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/component.tsx b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/component.tsx index 0a00851818..214a069679 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/component.tsx @@ -51,7 +51,7 @@ const AudioControls: React.FC = ({ updateEchoTestRunning, }) => { const intl = useIntl(); - const joinAudioShourtcut = useShortcut('joinaudio'); + const joinAudioShortcut = useShortcut('joinAudio'); const echoTestIntervalRef = React.useRef>(); const [isAudioModalOpen, setIsAudioModalOpen] = React.useState(false); @@ -79,10 +79,10 @@ const AudioControls: React.FC = ({ icon="no_audio" size="lg" circle - accessKey={joinAudioShourtcut} + accessKey={joinAudioShortcut} /> ); - }, [isConnected, disabled]); + }, [isConnected, disabled, joinAudioShortcut]); useEffect(() => { if (isEchoTest) { diff --git a/bigbluebutton-html5/imports/ui/core/hooks/useShortcut.tsx b/bigbluebutton-html5/imports/ui/core/hooks/useShortcut.tsx index 832b0c4a33..5bd1ae642d 100644 --- a/bigbluebutton-html5/imports/ui/core/hooks/useShortcut.tsx +++ b/bigbluebutton-html5/imports/ui/core/hooks/useShortcut.tsx @@ -17,7 +17,7 @@ export function useShortcut(param: string): string { useEffect(() => { const ENABLED_SHORTCUTS = getFromUserSettings('bbb_shortcuts', null); const filteredShortcuts: ShortcutObject[] = Object.values(BASE_SHORTCUTS).filter( - (el: ShortcutObject) => (ENABLED_SHORTCUTS ? ENABLED_SHORTCUTS.includes(el.descId) : true), + (el: ShortcutObject) => (ENABLED_SHORTCUTS ? ENABLED_SHORTCUTS.includes(el.descId.toLowerCase()) : true), ); const shortcutsString: string = filteredShortcuts diff --git a/bigbluebutton-tests/playwright/audio/audio.spec.js b/bigbluebutton-tests/playwright/audio/audio.spec.js index 99a7aa55be..732f399b42 100644 --- a/bigbluebutton-tests/playwright/audio/audio.spec.js +++ b/bigbluebutton-tests/playwright/audio/audio.spec.js @@ -1,17 +1,14 @@ const { test } = require('@playwright/test'); const { fullyParallel } = require('../playwright.config'); const { Audio } = require('./audio'); - -if (!fullyParallel) test.describe.configure({ mode: 'serial' }); +const { initializePages } = require('../core/helpers'); test.describe('Audio', () => { const audio = new Audio(); - test.beforeAll(async ({ browser }) => { - const context = await browser.newContext(); - const page = await context.newPage(); - await audio.initModPage(page, true); - await audio.initUserPage(true, context); + test.describe.configure({ mode: fullyParallel ? 'parallel' : 'serial' }); + test[fullyParallel ? 'beforeEach' : 'beforeAll'](async ({ browser }) => { + await initializePages(audio, browser, { isMultiUser: true }); }); // https://docs.bigbluebutton.org/2.6/release-tests.html#listen-only-mode-automated diff --git a/bigbluebutton-tests/playwright/breakout/create.js b/bigbluebutton-tests/playwright/breakout/create.js index d0ecdf39ff..340b6e4a45 100644 --- a/bigbluebutton-tests/playwright/breakout/create.js +++ b/bigbluebutton-tests/playwright/breakout/create.js @@ -117,7 +117,7 @@ class Create extends MultiUsers { await this.userPage.waitAndClick(e.modalConfirmButton); await this.modPage.waitAndClick(e.breakoutRoomsItem); - await this.modPage.hasText(e.userNameBreakoutRoom, /Attendee/); + await this.modPage.hasText(e.userNameBreakoutRoom, /Attendee/, ELEMENT_WAIT_LONGER_TIME); } } diff --git a/bigbluebutton-tests/playwright/breakout/join.js b/bigbluebutton-tests/playwright/breakout/join.js index 4781f62f94..ac34b1cd55 100644 --- a/bigbluebutton-tests/playwright/breakout/join.js +++ b/bigbluebutton-tests/playwright/breakout/join.js @@ -139,7 +139,7 @@ class Join extends Create { await breakoutUserPage.page.isClosed(); await this.userPage.waitAndClick(e.modalConfirmButton); - await this.modPage.hasText(e.userNameBreakoutRoom2, /Attendee/); + await this.modPage.hasText(e.userNameBreakoutRoom2, /Attendee/, ELEMENT_WAIT_LONGER_TIME); } async exportBreakoutNotes() { diff --git a/bigbluebutton-tests/playwright/chat/chat.spec.js b/bigbluebutton-tests/playwright/chat/chat.spec.js index 9e38f9c698..0d63de8c3a 100644 --- a/bigbluebutton-tests/playwright/chat/chat.spec.js +++ b/bigbluebutton-tests/playwright/chat/chat.spec.js @@ -1,18 +1,16 @@ const { test } = require('@playwright/test'); const { fullyParallel } = require('../playwright.config'); -const { linkIssue } = require('../core/helpers'); +const { linkIssue, initializePages } = require('../core/helpers'); const { Chat } = require('./chat'); -if (!fullyParallel) test.describe.configure({ mode: 'serial' }); - test.describe('Chat', () => { const chat = new Chat(); let context; - test.beforeAll(async ({ browser }) => { - context = await browser.newContext(); - const page = await context.newPage(); - await chat.initModPage(page, true); - await chat.initUserPage(true, context); + + test.describe.configure({ mode: fullyParallel ? 'parallel' : 'serial' }); + test[fullyParallel ? 'beforeEach' : 'beforeAll'](async ({ browser }) => { + const { context: innerContext } = await initializePages(chat, browser, { isMultiUser: true }); + context = innerContext; }); // https://docs.bigbluebutton.org/2.6/release-tests.html#public-message-automated diff --git a/bigbluebutton-tests/playwright/chat/util.js b/bigbluebutton-tests/playwright/chat/util.js index a6799043d4..3022df39e7 100644 --- a/bigbluebutton-tests/playwright/chat/util.js +++ b/bigbluebutton-tests/playwright/chat/util.js @@ -1,4 +1,4 @@ -const { default: test, expect } = require('@playwright/test'); +const { expect } = require('@playwright/test'); const e = require('../core/elements'); const { getSettings } = require('../core/settings'); diff --git a/bigbluebutton-tests/playwright/core/elements.js b/bigbluebutton-tests/playwright/core/elements.js index bffcb6510d..c1c83f333f 100644 --- a/bigbluebutton-tests/playwright/core/elements.js +++ b/bigbluebutton-tests/playwright/core/elements.js @@ -276,7 +276,7 @@ exports.yesNoAbstentionOption = 'li[role="menuitem"]>>nth=1'; exports.pollAnswerOptionE = 'button[data-test="pollAnswerOption"]>>nth=4'; exports.answerE = 'div[data-test="numberOfVotes"]>>nth=4'; // Presentation -exports.currentSlideImg = 'img[id="slide-background-shape_image"]'; +exports.currentSlideImg = '[id="whiteboard-element"] [class="tl-image"]'; exports.uploadPresentationFileName = 'uploadTest.png'; exports.presentationPPTX = 'BBB.pptx'; exports.presentationTXT = 'helloWorld.txt'; @@ -285,7 +285,7 @@ exports.noPresentationLabel = 'There is no currently active presentation'; exports.startScreenSharing = 'button[data-test="startScreenShare"]'; exports.stopScreenSharing = 'button[data-test="stopScreenShare"]'; exports.managePresentations = 'li[data-test="managePresentations"]'; -exports.fileUpload = 'input[type="file"]'; +exports.presentationFileUpload = 'div#upload-modal input[type="file"]'; exports.presentationToolbarWrapper = 'div[id="presentationToolbarWrapper"]'; exports.nextSlide = 'button[data-test="nextSlide"]'; exports.prevSlide = 'button[data-test="prevSlide"]'; diff --git a/bigbluebutton-tests/playwright/core/helpers.js b/bigbluebutton-tests/playwright/core/helpers.js index f497d7e70f..700ae53c6d 100644 --- a/bigbluebutton-tests/playwright/core/helpers.js +++ b/bigbluebutton-tests/playwright/core/helpers.js @@ -162,6 +162,18 @@ function sleep(time) { }); } +async function initializePages(testInstance, browser, initOptions) { + const { isMultiUser, createParameter, joinParameter } = initOptions || {}; + const context = await browser.newContext(); + const page = await context.newPage(); + await testInstance.initModPage(page, true, { createParameter, joinParameter }); + if (isMultiUser) await testInstance.initUserPage(true, context, { createParameter, joinParameter }); + + return { + context, + }; +} + exports.getRandomInt = getRandomInt; exports.apiCallUrl = apiCallUrl; exports.apiCall = apiCall; @@ -173,3 +185,4 @@ exports.checkRootPermission = checkRootPermission; exports.linkIssue = linkIssue; exports.sleep = sleep; exports.setBrowserLogs = setBrowserLogs; +exports.initializePages = initializePages; diff --git a/bigbluebutton-tests/playwright/core/page.js b/bigbluebutton-tests/playwright/core/page.js index dbe6c8f5c6..ee7029814c 100644 --- a/bigbluebutton-tests/playwright/core/page.js +++ b/bigbluebutton-tests/playwright/core/page.js @@ -7,7 +7,6 @@ const helpers = require('./helpers'); const e = require('./elements'); const { env } = require('node:process'); const { ELEMENT_WAIT_TIME, ELEMENT_WAIT_LONGER_TIME, VIDEO_LOADING_WAIT_TIME } = require('./constants'); -const { recordMeeting } = require('../parameters/constants'); const { checkElement, checkElementLengthEqualTo } = require('./util'); const { generateSettingsData, getSettings } = require('./settings'); diff --git a/bigbluebutton-tests/playwright/layouts/layouts.spec.js b/bigbluebutton-tests/playwright/layouts/layouts.spec.js index f8ac086b5d..b2e4af44a4 100644 --- a/bigbluebutton-tests/playwright/layouts/layouts.spec.js +++ b/bigbluebutton-tests/playwright/layouts/layouts.spec.js @@ -3,21 +3,16 @@ const { fullyParallel } = require('../playwright.config'); const { encodeCustomParams } = require('../parameters/util'); const { PARAMETER_HIDE_PRESENTATION_TOAST } = require('../core/constants'); const { Layouts } = require('./layouts'); +const { initializePages } = require('../core/helpers'); const hidePresentationToast = encodeCustomParams(PARAMETER_HIDE_PRESENTATION_TOAST); -const CUSTOM_MEETING_ID = 'layout_management_meeting'; - -if (!fullyParallel) test.describe.configure({ mode: 'serial' }); - test.describe("Layout management", () => { const layouts = new Layouts(); - test.beforeAll(async ({ browser }) => { - const context = await browser.newContext(); - const page = await context.newPage(); - await layouts.initModPage(page, true, { createParameter: hidePresentationToast, customMeetingId: CUSTOM_MEETING_ID }); - await layouts.initUserPage(true, context, { createParameter: hidePresentationToast }); + test.describe.configure({ mode: fullyParallel ? 'parallel' : 'serial' }); + test[fullyParallel ? 'beforeEach' : 'beforeAll'](async ({ browser }) => { + await initializePages(layouts, browser, true, { isMultiUser: true, createParameter: hidePresentationToast }); await layouts.modPage.shareWebcam(); await layouts.userPage.shareWebcam(); }); diff --git a/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.spec.js b/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.spec.js index 355d69a831..470c84010b 100644 --- a/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.spec.js +++ b/bigbluebutton-tests/playwright/learningdashboard/learningdashboard.spec.js @@ -2,16 +2,14 @@ const { test } = require('@playwright/test'); const { fullyParallel } = require('../playwright.config'); const { LearningDashboard } = require('./learningdashboard'); const c = require('../parameters/constants'); - -if (!fullyParallel) test.describe.configure({ mode: 'serial' }); +const { initializePages } = require('../core/helpers'); test.describe('Learning Dashboard', async () => { const learningDashboard = new LearningDashboard(); - test.beforeAll(async ({ browser }) => { - const context = await browser.newContext(); - const page = await context.newPage(); - await learningDashboard.initModPage(page, true, { createParameter: c.recordMeeting }); + test.describe.configure({ mode: fullyParallel ? 'parallel' : 'serial' }); + test[fullyParallel ? 'beforeEach' : 'beforeAll'](async ({ browser }) => { + const { context } = await initializePages(learningDashboard, browser, { createParameter: c.recordMeeting }); await learningDashboard.getDashboardPage(context); }); diff --git a/bigbluebutton-tests/playwright/notifications/notifications.spec.js b/bigbluebutton-tests/playwright/notifications/notifications.spec.js index 97159bf62f..05d93cc3ad 100644 --- a/bigbluebutton-tests/playwright/notifications/notifications.spec.js +++ b/bigbluebutton-tests/playwright/notifications/notifications.spec.js @@ -31,13 +31,14 @@ test.describe.parallel('Notifications', () => { }); test.describe.parallel('Chat', () => { + // both tests are flaky due to missing refactor to get data from GraphQL test('Public Chat notification @ci @flaky', async ({ browser, context, page }) => { const chatNotifications = new ChatNotifications(browser, context); await chatNotifications.initPages(page, true); await chatNotifications.publicChatNotification(); }); - test('Private Chat notification', async ({ browser, context, page }) => { + test('Private Chat notification @flaky', async ({ browser, context, page }) => { const chatNotifications = new ChatNotifications(browser, context); await chatNotifications.initPages(page, true); await chatNotifications.privateChatNotification(); diff --git a/bigbluebutton-tests/playwright/options/options.spec.js b/bigbluebutton-tests/playwright/options/options.spec.js index 9bd12c0196..f73dee0056 100644 --- a/bigbluebutton-tests/playwright/options/options.spec.js +++ b/bigbluebutton-tests/playwright/options/options.spec.js @@ -1,16 +1,16 @@ const { test } = require('@playwright/test'); const { fullyParallel } = require('../playwright.config'); const { Options } = require('./options'); - -if (!fullyParallel) test.describe.configure({ mode: 'serial' }); +const { initializePages } = require('../core/helpers'); test.describe('Options', () => { const options = new Options(); let context; - test.beforeAll(async ({ browser }) => { - context = await browser.newContext(); - const page = await context.newPage(); - await options.initModPage(page, true); + + test.describe.configure({ mode: fullyParallel ? 'parallel' : 'serial' }); + test[fullyParallel ? 'beforeEach' : 'beforeAll'](async ({ browser }) => { + const { context: innerContext } = await initializePages(options, browser); + context = innerContext; }); test('Open about modal', async () => { diff --git a/bigbluebutton-tests/playwright/parameters/constants.js b/bigbluebutton-tests/playwright/parameters/constants.js index bdd2fdb70a..1004cead70 100644 --- a/bigbluebutton-tests/playwright/parameters/constants.js +++ b/bigbluebutton-tests/playwright/parameters/constants.js @@ -1,10 +1,10 @@ const e = require('../core/elements'); // Create Parameters -exports.bannerText = 'bannerText=some text'; -const color = 'FFFF00' +exports.bannerText = 'bannerText=some+text'; +const color = '#FFFF00' exports.color = color; -exports.bannerColor = `bannerColor=%23${color}`; +exports.bannerColor = `bannerColor=${color}`; exports.maxParticipants = 'maxParticipants=2'; exports.duration = 'duration=2'; const messageModerator = 'Test'; @@ -35,13 +35,13 @@ exports.logo = 'logo=https://bigbluebutton.org/wp-content/uploads/2021/01/BigBlu exports.enableVideo = 'userdata-bbb_enable_video=false'; exports.autoShareWebcam = 'userdata-bbb_auto_share_webcam=true'; exports.multiUserPenOnly = 'userdata-bbb_multi_user_pen_only=true'; -exports.presenterTools = 'userdata-bbb_presenter_tools=["pencil", "hand"]'; -exports.multiUserTools = 'userdata-bbb_multi_user_tools=["pencil", "hand"]'; +exports.presenterTools = 'userdata-bbb_presenter_tools=["pencil","hand"]'; +exports.multiUserTools = 'userdata-bbb_multi_user_tools=["pencil","hand"]'; const cssCode = `${e.presentationTitle}{display: none;}`; exports.customStyle = `userdata-bbb_custom_style=${cssCode}`; exports.customStyleUrl = 'userdata-bbb_custom_style_url=https://develop.bigbluebutton.org/css-test-file.css'; exports.autoSwapLayout = 'userdata-bbb_auto_swap_layout=true'; -exports.hidePresentationOnJoin = 'userdata-bbb_hide_presentation_on_join="true"'; +exports.hidePresentationOnJoin = 'userdata-bbb_hide_presentation_on_join=true'; exports.outsideToggleSelfVoice = 'userdata-bbb_outside_toggle_self_voice=true'; exports.outsideToggleRecording = 'userdata-bbb_outside_toggle_recording=true'; exports.showPublicChatOnLogin = 'userdata-bbb_show_public_chat_on_login=false'; diff --git a/bigbluebutton-tests/playwright/parameters/customparameters.js b/bigbluebutton-tests/playwright/parameters/customparameters.js index acada0c773..7e641eac03 100644 --- a/bigbluebutton-tests/playwright/parameters/customparameters.js +++ b/bigbluebutton-tests/playwright/parameters/customparameters.js @@ -26,7 +26,7 @@ class CustomParameters extends MultiUsers { async clientTitle() { const pageTitle = await this.modPage.page.title(); - await expect(pageTitle).toContain(`${c.docTitle} - `); + expect(pageTitle).toContain(`${c.docTitle} - `); } async askForFeedbackOnLogout() { @@ -59,7 +59,7 @@ class CustomParameters extends MultiUsers { const resp = await this.modPage.page.evaluate((elem) => { return document.querySelectorAll(elem)[0].offsetHeight == 0; }, e.presentationTitle); - await expect(resp).toBeTruthy(); + expect(resp).toBeTruthy(); } async autoSwapLayout() { @@ -68,7 +68,7 @@ class CustomParameters extends MultiUsers { const resp = await this.modPage.page.evaluate((elem) => { return document.querySelectorAll(elem)[0].offsetHeight !== 0; }, e.restorePresentation); - await expect(resp).toBeTruthy(); + expect(resp).toBeTruthy(); } async autoJoin() { @@ -121,13 +121,14 @@ class CustomParameters extends MultiUsers { const notificationBarColor = await notificationLocator.evaluate((elem) => { return getComputedStyle(elem).backgroundColor; }, e.notificationBannerBar); - await expect(notificationBarColor).toBe(colorToRGB); + expect(notificationBarColor).toBe(colorToRGB); } async hidePresentationOnJoin() { await this.modPage.waitForSelector(e.actions); await this.modPage.hasElement(e.restorePresentation); - await this.modPage.wasRemoved(e.presentationPlaceholder); + await this.userPage.hasElement(e.restorePresentation); + await this.userPage.wasRemoved(e.whiteboard); } async forceRestorePresentationOnNewEvents(joinParameter) { @@ -135,9 +136,9 @@ class CustomParameters extends MultiUsers { const { presentationHidden, pollEnabled } = getSettings(); if (!presentationHidden) await this.userPage.waitAndClick(e.minimizePresentation); const zoomInCase = await util.zoomIn(this.modPage); - await expect(zoomInCase).toBeTruthy(); + expect(zoomInCase).toBeTruthy(); const zoomOutCase = await util.zoomOut(this.modPage); - await expect(zoomOutCase).toBeTruthy(); + expect(zoomOutCase).toBeTruthy(); if (pollEnabled) await util.poll(this.modPage, this.userPage); await util.nextSlide(this.modPage); await util.previousSlide(this.modPage); @@ -183,7 +184,7 @@ class CustomParameters extends MultiUsers { const resp = await this.userPage.page.evaluate((toolsElement) => { return document.querySelectorAll(toolsElement)[0].parentElement.childElementCount === 1; }, e.wbToolbar); - await expect(resp).toBeTruthy(); + expect(resp).toBeTruthy(); } async presenterTools() { @@ -192,7 +193,7 @@ class CustomParameters extends MultiUsers { const resp = await this.modPage.page.evaluate(([toolsElement, toolbarListSelector]) => { return document.querySelectorAll(toolsElement)[0].parentElement.querySelector(toolbarListSelector).childElementCount === 2; }, [e.wbToolbar, e.toolbarToolsList]); - await expect(resp).toBeTruthy(); + expect(resp).toBeTruthy(); } async multiUserTools() { @@ -201,7 +202,7 @@ class CustomParameters extends MultiUsers { const resp = await this.userPage.page.evaluate(([toolsElement, toolbarListSelector]) => { return document.querySelectorAll(toolsElement)[0].parentElement.querySelector(toolbarListSelector).childElementCount === 2; }, [e.wbToolbar, e.toolbarToolsList]); - await expect(resp).toBeTruthy(); + expect(resp).toBeTruthy(); } async autoShareWebcam() { diff --git a/bigbluebutton-tests/playwright/parameters/parameters.spec.js b/bigbluebutton-tests/playwright/parameters/parameters.spec.js index 0570ade66f..79a7f1bfff 100644 --- a/bigbluebutton-tests/playwright/parameters/parameters.spec.js +++ b/bigbluebutton-tests/playwright/parameters/parameters.spec.js @@ -15,26 +15,28 @@ test.describe.parallel('Create Parameters', () => { test.describe.parallel('Banner', () => { test('Banner Text @ci', async ({ browser, context, page }) => { const createParam = new CreateParameters(browser, context); - await createParam.initModPage(page, true, { createParameter: encodeCustomParams(c.bannerText) }); + await createParam.initModPage(page, true, { createParameter: c.bannerText }); await createParam.bannerText(); }); test('Banner Color @ci', async ({ browser, context, page }) => { const createParam = new CreateParameters(browser, context); - const colorToRGB = hexToRgb(c.color); - await createParam.initModPage(page, true, { createParameter: `${c.bannerColor}&${encodeCustomParams(c.bannerText)}` }); + const colorToRGB = hexToRgb(c.color.substring(1)); + await createParam.initModPage(page, true, { createParameter: `${encodeCustomParams(c.bannerColor)}&${c.bannerText}` }); await createParam.bannerColor(colorToRGB); }); }); - test('Max Participants', async ({ browser, context, page }) => { + // see https://github.com/bigbluebutton/bigbluebutton/issues/19426 + test('Max Participants @flaky', async ({ browser, context, page }) => { const createParam = new CreateParameters(browser, context); await createParam.initModPage(page, true, { createParameter: c.maxParticipants }); await createParam.initModPage2(true, context); await createParam.maxParticipants(context); }); - test('Meeting Duration', async ({ browser, context, page }) => { + // Not working due to missing data provided by GraphQL + test('Meeting Duration @flaky', async ({ browser, context, page }) => { const createParam = new CreateParameters(browser, context); await createParam.initModPage(page, true, { createParameter: c.duration }); await createParam.duration(); @@ -369,7 +371,7 @@ test.describe.parallel('Custom Parameters', () => { await customParam.displayBrandingArea(); }); - test('Shortcuts', async ({ browser, context, page }) => { + test('Shortcuts @ci', async ({ browser, context, page }) => { const customParam = new CustomParameters(browser, context); const shortcutParam = getAllShortcutParams(); await customParam.initModPage(page, true, { joinParameter: encodeCustomParams(shortcutParam) }); @@ -421,7 +423,8 @@ test.describe.parallel('Custom Parameters', () => { }); test.describe.parallel('Audio', () => { - test('Auto join @ci', async ({ browser, context, page }) => { + // see https://github.com/bigbluebutton/bigbluebutton/issues/19427 + test('Auto join @ci @flaky', async ({ browser, context, page }) => { const customParam = new CustomParameters(browser, context); await customParam.initModPage(page, false, { joinParameter: c.autoJoin }); await customParam.autoJoin(); @@ -433,7 +436,8 @@ test.describe.parallel('Custom Parameters', () => { await customParam.listenOnlyMode(); }); - test('Force Listen Only @ci', async ({ browser, context, page }) => { + // see https://github.com/bigbluebutton/bigbluebutton/issues/19428 + test('Force Listen Only @ci @flaky', async ({ browser, context, page }) => { const customParam = new CustomParameters(browser, context); await customParam.initUserPage(false, context, { useModMeetingId: false, joinParameter: c.forceListenOnly }); await customParam.forceListenOnly(page); @@ -453,9 +457,11 @@ test.describe.parallel('Custom Parameters', () => { }); test.describe.parallel('Presentation', () => { - test('Hide Presentation on join @ci', async ({ browser, context, page }) => { + // see https://github.com/bigbluebutton/bigbluebutton/issues/19456 + test('Hide Presentation on join @ci @flaky', async ({ browser, context, page }) => { const customParam = new CustomParameters(browser, context); - await customParam.initModPage(page, true, { joinParameter: encodeCustomParams(c.hidePresentationOnJoin) }); + await customParam.initModPage(page, true, { joinParameter: c.hidePresentationOnJoin }); + await customParam.initUserPage(true, context, { useModMeetingId: true, joinParameter: c.hidePresentationOnJoin }); await customParam.hidePresentationOnJoin(); }); diff --git a/bigbluebutton-tests/playwright/parameters/util.js b/bigbluebutton-tests/playwright/parameters/util.js index 07d90a9ba7..50a2eb0ee8 100644 --- a/bigbluebutton-tests/playwright/parameters/util.js +++ b/bigbluebutton-tests/playwright/parameters/util.js @@ -1,7 +1,7 @@ const { expect } = require('@playwright/test'); const e = require('../core/elements'); const c = require('./constants'); -const { ELEMENT_WAIT_TIME, ELEMENT_WAIT_LONGER_TIME } = require('../core/constants'); +const { ELEMENT_WAIT_LONGER_TIME } = require('../core/constants'); async function forceListenOnly(test) { await test.wasRemoved(e.echoYesButton); @@ -71,14 +71,14 @@ async function annotation(test) { function encodeCustomParams(param) { try { - let splited = param.split('='); - if (splited.length > 2) { - const aux = splited.shift(); - splited[1] = splited.join('='); - splited[0] = aux; + let splitted = param.split('='); + if (splitted.length > 2) { + const aux = splitted.shift(); + splitted[1] = splitted.join('='); + splitted[0] = aux; } - splited[1] = encodeURIComponent(splited[1]).replace(/%20/g, '+'); - return splited.join('='); + splitted[1] = encodeURIComponent(splitted[1]).replace(); + return splitted.join('='); } catch (err) { console.log(err); } diff --git a/bigbluebutton-tests/playwright/polling/poll.js b/bigbluebutton-tests/playwright/polling/poll.js index ddf3283071..2cce8f2272 100644 --- a/bigbluebutton-tests/playwright/polling/poll.js +++ b/bigbluebutton-tests/playwright/polling/poll.js @@ -2,10 +2,11 @@ const { expect, test } = require('@playwright/test'); const { MultiUsers } = require('../user/multiusers'); const e = require('../core/elements'); const util = require('./util.js'); -const utilPresentation = require('../presentation/util'); const { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } = require('../core/constants'); const { getSettings } = require('../core/settings'); const { waitAndClearDefaultPresentationNotification } = require('../notifications/util'); +const { uploadSinglePresentation, skipSlide } = require('../presentation/util'); +const { sleep } = require('../core/helpers.js'); class Polling extends MultiUsers { constructor(browser, context) { @@ -33,7 +34,7 @@ class Polling extends MultiUsers { } async quickPoll() { - await utilPresentation.uploadSinglePresentation(this.modPage, e.questionSlideFileName); + await uploadSinglePresentation(this.modPage, e.questionSlideFileName); // The slide needs to be uploaded and converted, so wait a bit longer for this step await this.modPage.waitAndClick(e.quickPoll, ELEMENT_WAIT_LONGER_TIME); @@ -117,6 +118,7 @@ class Polling extends MultiUsers { } async notAbleStartNewPollWithoutPresentation() { + await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME); await this.modPage.waitAndClick(e.actions); await this.modPage.waitAndClick(e.managePresentations); await this.modPage.waitAndClick(e.removePresentation); @@ -128,7 +130,7 @@ class Polling extends MultiUsers { } async customInput() { - await utilPresentation.uploadSinglePresentation(this.modPage, e.questionSlideFileName); + await uploadSinglePresentation(this.modPage, e.questionSlideFileName); await this.modPage.waitAndClick(e.actions); await this.modPage.waitAndClick(e.polling); @@ -180,7 +182,7 @@ class Polling extends MultiUsers { async smartSlidesQuestions() { await this.modPage.hasElement(e.whiteboard, ELEMENT_WAIT_LONGER_TIME); - await utilPresentation.uploadSinglePresentation(this.modPage, e.smartSlides1, ELEMENT_WAIT_LONGER_TIME); + await uploadSinglePresentation(this.modPage, e.smartSlides1, ELEMENT_WAIT_LONGER_TIME); await this.userPage.hasElement(e.userListItem); // Type Response @@ -196,7 +198,8 @@ class Polling extends MultiUsers { await this.modPage.wasRemoved(e.closePollingBtn); // Multiple Choices - await this.modPage.waitAndClick(e.nextSlide); + await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift + await skipSlide(this.modPage); await this.modPage.waitAndClick(e.quickPoll); await this.userPage.waitAndClick(e.firstPollAnswerDescOption); await this.userPage.waitAndClick(e.secondPollAnswerDescOption); @@ -209,7 +212,8 @@ class Polling extends MultiUsers { await this.modPage.wasRemoved(e.closePollingBtn); // One option answer - await this.modPage.waitAndClick(e.nextSlide); + await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift + await skipSlide(this.modPage); await this.modPage.waitAndClick(e.quickPoll); await this.userPage.waitAndClick(e.pollAnswerOptionE); await this.modPage.hasText(e.answerE, '1'); @@ -219,7 +223,8 @@ class Polling extends MultiUsers { await this.modPage.wasRemoved(e.closePollingBtn); // Yes/No/Abstention - await this.modPage.waitAndClick(e.nextSlide); + await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift + await skipSlide(this.modPage); await this.modPage.waitAndClick(e.yesNoOption); await this.modPage.waitAndClick(e.yesNoAbstentionOption) await this.userPage.waitAndClick(e.pollAnswerOptionBtn); @@ -230,7 +235,8 @@ class Polling extends MultiUsers { await this.modPage.wasRemoved(e.closePollingBtn); // True/False - await this.modPage.waitAndClick(e.nextSlide); + await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift + await skipSlide(this.modPage); await this.modPage.waitAndClick(e.quickPoll); await this.userPage.waitAndClick(e.pollAnswerOptionBtn); await this.modPage.hasText(e.answer1, '1'); @@ -293,7 +299,7 @@ class Polling extends MultiUsers { await waitAndClearDefaultPresentationNotification(this.modPage); - await utilPresentation.uploadSinglePresentation(this.modPage, e.questionSlideFileName); + await uploadSinglePresentation(this.modPage, e.questionSlideFileName); await util.startPoll(this.modPage); await this.modPage.waitAndClick(e.publishPollingLabel); diff --git a/bigbluebutton-tests/playwright/polling/polling.spec.js b/bigbluebutton-tests/playwright/polling/polling.spec.js index bcf1b0cab1..e69f8fa332 100644 --- a/bigbluebutton-tests/playwright/polling/polling.spec.js +++ b/bigbluebutton-tests/playwright/polling/polling.spec.js @@ -1,17 +1,14 @@ const { test } = require('@playwright/test'); const { fullyParallel } = require('../playwright.config'); const { Polling } = require('./poll'); +const { initializePages } = require('../core/helpers'); -if (!fullyParallel) test.describe.configure({ mode: 'serial' }); - -test.describe('Polling', () => { +test.describe('Polling', async () => { const polling = new Polling(); - test.beforeAll(async ({ browser }) => { - const context = await browser.newContext(); - const page = await context.newPage(); - await polling.initModPage(page, true); - await polling.initUserPage(true, context); + test.describe.configure({ mode: fullyParallel ? 'parallel' : 'serial' }); + test[fullyParallel ? 'beforeEach' : 'beforeAll'](async ({ browser }) => { + await initializePages(polling, browser, { isMultiUser: true }); }); // Manage diff --git a/bigbluebutton-tests/playwright/presentation/presentation.js b/bigbluebutton-tests/playwright/presentation/presentation.js index f0137f6668..b5869b4dfb 100644 --- a/bigbluebutton-tests/playwright/presentation/presentation.js +++ b/bigbluebutton-tests/playwright/presentation/presentation.js @@ -6,7 +6,6 @@ const { checkSvgIndex, getSlideOuterHtml, uploadSinglePresentation, uploadMultip const { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_EXTRA_LONG_TIME, UPLOAD_PDF_WAIT_TIME, ELEMENT_WAIT_TIME } = require('../core/constants'); const { sleep } = require('../core/helpers'); const { getSettings } = require('../core/settings'); -const { waitAndClearDefaultPresentationNotification, waitAndClearNotification } = require('../notifications/util'); const defaultZoomLevel = '100%'; diff --git a/bigbluebutton-tests/playwright/presentation/util.js b/bigbluebutton-tests/playwright/presentation/util.js index 106bb855e7..9152a85ab4 100644 --- a/bigbluebutton-tests/playwright/presentation/util.js +++ b/bigbluebutton-tests/playwright/presentation/util.js @@ -23,18 +23,23 @@ async function getCurrentPresentationHeight(locator) { } async function uploadSinglePresentation(test, fileName, uploadTimeout = UPLOAD_PDF_WAIT_TIME) { - const firstSlideSrc = await test.page.evaluate(selector => document.querySelector(selector).src, [e.currentSlideImg]); + const firstSlideSrc = await test.page.evaluate(selector => document.querySelector(selector) + .style + .backgroundImage + .split('"')[1], + [e.currentSlideImg]); await test.waitAndClick(e.actions); await test.waitAndClick(e.managePresentations); - await test.waitForSelector(e.fileUpload); + await test.waitForSelector(e.presentationFileUpload); - await test.page.setInputFiles(e.fileUpload, path.join(__dirname, `../core/media/${fileName}`)); + await test.page.setInputFiles(e.presentationFileUpload, path.join(__dirname, `../core/media/${fileName}`)); await test.hasText('body', e.statingUploadPresentationToast); await test.waitAndClick(e.confirmManagePresentation); await test.hasElement(e.presentationUploadProgressToast, ELEMENT_WAIT_EXTRA_LONG_TIME); await test.page.waitForFunction(([selector, firstSlideSrc]) => { - const currentSrc = document.querySelector(selector).src; + const currentSrc = document.querySelector(selector) + ?.style?.backgroundImage?.split('"')[1]; return currentSrc != firstSlideSrc; }, [e.currentSlideImg, firstSlideSrc], { timeout: uploadTimeout, @@ -44,9 +49,9 @@ async function uploadSinglePresentation(test, fileName, uploadTimeout = UPLOAD_P async function uploadMultiplePresentations(test, fileNames, uploadTimeout = ELEMENT_WAIT_EXTRA_LONG_TIME) { await test.waitAndClick(e.actions); await test.waitAndClick(e.managePresentations); - await test.waitForSelector(e.fileUpload); + await test.waitForSelector(e.presentationFileUpload); - await test.page.setInputFiles(e.fileUpload, fileNames.map((fileName) => path.join(__dirname, `../core/media/${fileName}`))); + await test.page.setInputFiles(e.presentationFileUpload, fileNames.map((fileName) => path.join(__dirname, `../core/media/${fileName}`))); await test.hasText('body', e.statingUploadPresentationToast); await test.waitAndClick(e.confirmManagePresentation); @@ -54,8 +59,16 @@ async function uploadMultiplePresentations(test, fileNames, uploadTimeout = ELEM await test.hasText(e.smallToastMsg, e.presentationUploadedToast, uploadTimeout); } +async function skipSlide(page) { + const selectSlideLocator = page.getLocator(e.skipSlide); + const currentSlideNumber = await selectSlideLocator.inputValue(); + await page.waitAndClick(e.nextSlide); + await expect(selectSlideLocator).not.toHaveValue(currentSlideNumber); +} + exports.checkSvgIndex = checkSvgIndex; exports.getSlideOuterHtml = getSlideOuterHtml; exports.uploadSinglePresentation = uploadSinglePresentation; exports.uploadMultiplePresentations = uploadMultiplePresentations; exports.getCurrentPresentationHeight = getCurrentPresentationHeight; +exports.skipSlide = skipSlide; diff --git a/bigbluebutton-tests/playwright/reconnection/reconnection.spec.js b/bigbluebutton-tests/playwright/reconnection/reconnection.spec.js index 039426f396..2d328e0853 100644 --- a/bigbluebutton-tests/playwright/reconnection/reconnection.spec.js +++ b/bigbluebutton-tests/playwright/reconnection/reconnection.spec.js @@ -1,11 +1,8 @@ const { test } = require('@playwright/test'); -const { fullyParallel } = require('../playwright.config'); const { Reconnection } = require('./reconnection'); const { checkRootPermission } = require('../core/helpers'); -if (!fullyParallel) test.describe.configure({ mode: 'serial' }); - -test.describe('Reconnection', () => { +test.describe.parallel('Reconnection', () => { test('Chat', async ({ browser, context, page }) => { await checkRootPermission(); // check sudo permission before starting test const reconnection = new Reconnection(browser, context); diff --git a/bigbluebutton-tests/playwright/screenshare/screenshare.spec.js b/bigbluebutton-tests/playwright/screenshare/screenshare.spec.js index fc60476009..e92e104107 100644 --- a/bigbluebutton-tests/playwright/screenshare/screenshare.spec.js +++ b/bigbluebutton-tests/playwright/screenshare/screenshare.spec.js @@ -5,7 +5,8 @@ test.describe.parallel('Screenshare', () => { // https://docs.bigbluebutton.org/2.6/release-tests.html#sharing-screen-in-full-screen-mode-automated test('Share screen @ci', async ({ browser, browserName, page }) => { test.skip(browserName === 'firefox' && process.env.DISPLAY === undefined, - "Screenshare tests not able in Firefox browser without desktop"); + 'Screenshare tests not able in Firefox browser without desktop' + ); const screenshare = new ScreenShare(browser, page); await screenshare.init(true, true); await screenshare.startSharing(); diff --git a/bigbluebutton-tests/playwright/sharednotes/sharednotes.spec.js b/bigbluebutton-tests/playwright/sharednotes/sharednotes.spec.js index 5d1f00da6f..198928ef34 100644 --- a/bigbluebutton-tests/playwright/sharednotes/sharednotes.spec.js +++ b/bigbluebutton-tests/playwright/sharednotes/sharednotes.spec.js @@ -1,15 +1,16 @@ const { test } = require('@playwright/test'); const { SharedNotes } = require('./sharednotes'); +const { initializePages } = require('../core/helpers'); +const { fullyParallel } = require('../playwright.config'); -test.describe.parallel('Shared Notes', () => { +test.describe('Shared Notes', () => { const sharedNotes = new SharedNotes(); - test.beforeAll(async ({ browser }) => { - const context = await browser.newContext(); - const page = await context.newPage(); - await sharedNotes.initModPage(page, true); - await sharedNotes.initUserPage(true, context); + test.describe.configure({ mode: fullyParallel ? 'parallel' : 'serial' }); + test[fullyParallel ? 'beforeEach' : 'beforeAll'](async ({ browser }) => { + await initializePages(sharedNotes, browser, { isMultiUser: true }); }); + test('Open shared notes @ci', async () => { await sharedNotes.openSharedNotes(); }); From 2f67417b4b02c8e15a1008be876a630e7176ad1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 19 Jan 2024 15:40:27 -0300 Subject: [PATCH 104/512] migrate stopWatchingExternalVideo action --- .../api/external-videos/server/index.js | 1 - .../api/external-videos/server/methods.js | 6 - .../methods/stopWatchingExternalVideo.js | 25 ---- .../ui/components/actions-bar/container.jsx | 8 +- .../actions-bar/screenshare/component.jsx | 6 +- .../modal/service.ts | 7 +- .../external-video-player/mutations.tsx | 8 +- .../external-video-player/service.js | 6 - .../notes/notes-dropdown/component.jsx | 3 +- .../notes/notes-dropdown/container.jsx | 19 +++- .../notes/notes-dropdown/service.js | 2 +- .../imports/ui/components/notes/service.js | 4 +- .../imports/ui/components/pads/service.js | 5 +- .../ui/components/screenshare/component.jsx | 3 +- .../ui/components/screenshare/container.jsx | 4 + .../ui/components/screenshare/service.js | 3 +- .../ui/components/video-preview/container.jsx | 107 ++++++++++-------- bigbluebutton-html5/server/main.js | 1 - 18 files changed, 105 insertions(+), 113 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/external-videos/server/index.js delete mode 100644 bigbluebutton-html5/imports/api/external-videos/server/methods.js delete mode 100644 bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js diff --git a/bigbluebutton-html5/imports/api/external-videos/server/index.js b/bigbluebutton-html5/imports/api/external-videos/server/index.js deleted file mode 100644 index ba55c4a16e..0000000000 --- a/bigbluebutton-html5/imports/api/external-videos/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './methods'; diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods.js b/bigbluebutton-html5/imports/api/external-videos/server/methods.js deleted file mode 100644 index 710daa890b..0000000000 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods.js +++ /dev/null @@ -1,6 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import stopWatchingExternalVideo from './methods/stopWatchingExternalVideo'; - -Meteor.methods({ - stopWatchingExternalVideo, -}); diff --git a/bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js b/bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js deleted file mode 100644 index 59bc829d54..0000000000 --- a/bigbluebutton-html5/imports/api/external-videos/server/methods/stopWatchingExternalVideo.js +++ /dev/null @@ -1,25 +0,0 @@ -import { check } from 'meteor/check'; -import Logger from '/imports/startup/server/logger'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import RedisPubSub from '/imports/startup/server/redis'; - -export default function stopWatchingExternalVideo() { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'StopExternalVideoPubMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - - const payload = {}; - - Logger.info(`User ${requesterUserId} stoping an external video for meeting ${meetingId}`); - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (error) { - Logger.error(`Error on stoping an external video for meeting ${meetingId}: ${error}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index 62a1dc5052..9baac5016f 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -2,12 +2,11 @@ import React, { useContext } from 'react'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; import { injectIntl } from 'react-intl'; -import { useSubscription } from '@apollo/client'; +import { useSubscription, useMutation } from '@apollo/client'; import getFromUserSettings from '/imports/ui/services/users-settings'; import Auth from '/imports/ui/services/auth'; import ActionsBar from './component'; import Service from './service'; -import ExternalVideoService from '/imports/ui/components/external-video-player/service'; import CaptionsService from '/imports/ui/components/captions/service'; import TimerService from '/imports/ui/components/timer/service'; import { layoutSelectOutput, layoutDispatch } from '../layout/context'; @@ -20,6 +19,7 @@ import { import MediaService from '../media/service'; import useMeeting from '/imports/ui/core/hooks/useMeeting'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; +import { EXTERNAL_VIDEO_STOP } from '../external-video-player/mutations'; const ActionsBarContainer = (props) => { const actionsBarStyle = layoutSelectOutput((i) => i.actionBar); @@ -50,6 +50,8 @@ const ActionsBarContainer = (props) => { emoji: user.emoji, isModerator: user.isModerator, })); + + const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP); const currentUser = { userId: Auth.userID, emoji: currentUserData?.emoji }; const amIPresenter = currentUserData?.presenter; const amIModerator = currentUserData?.isModerator; @@ -68,6 +70,7 @@ const ActionsBarContainer = (props) => { actionBarItems, isThereCurrentPresentation, isSharingVideo, + stopExternalVideoShare, } } /> @@ -86,7 +89,6 @@ const isReactionsButtonEnabled = () => { }; export default withTracker(() => ({ - stopExternalVideoShare: ExternalVideoService.stopWatching, enableVideo: getFromUserSettings('bbb_enable_video', Meteor.settings.public.kurento.enableVideo), setPresentationIsOpen: MediaService.setPresentationIsOpen, isSharedNotesPinned: Service.isSharedNotesPinned(), diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx index ec210fe463..7820f1bcf0 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/screenshare/component.jsx @@ -5,6 +5,7 @@ import deviceInfo from '/imports/utils/deviceInfo'; import browserInfo from '/imports/utils/browserInfo'; import logger from '/imports/startup/client/logger'; import { notify } from '/imports/ui/services/notification'; +import { useMutation } from '@apollo/client'; import Styled from './styles'; import ScreenshareBridgeService from '/imports/api/screenshare/client/bridge/service'; import { @@ -14,6 +15,7 @@ import { import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors'; import Button from '/imports/ui/components/common/button/component'; import { parsePayloads } from 'sdp-transform'; +import { EXTERNAL_VIDEO_STOP } from '../../external-video-player/mutations'; const { isMobile } = deviceInfo; const { isSafari, isTabletApp } = browserInfo; @@ -118,6 +120,8 @@ const ScreenshareButton = ({ amIPresenter, isMeteorConnected, }) => { + const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP); + // This is the failure callback that will be passed to the /api/screenshare/kurento.js // script on the presenter's call const handleFailure = (error) => { @@ -189,7 +193,7 @@ const ScreenshareButton = ({ if (isSafari && !ScreenshareBridgeService.HAS_DISPLAY_MEDIA) { setScreenshareUnavailableModalIsOpen(true); } else { - shareScreen(amIPresenter, handleFailure); + shareScreen(stopExternalVideoShare, amIPresenter, handleFailure); } }} id={amIBroadcasting ? 'unshare-screen-button' : 'share-screen-button'} diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/service.ts b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/service.ts index a980a5b3b8..13a9290557 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/service.ts +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/external-video-player-graphql/modal/service.ts @@ -1,13 +1,8 @@ import ReactPlayer from 'react-player'; -import { makeCall } from '/imports/ui/services/api'; const YOUTUBE_SHORTS_REGEX = new RegExp(/^(?:https?:\/\/)?(?:www\.)?(youtube\.com\/shorts)\/.+$/); const PANOPTO_MATCH_URL = /https?:\/\/([^/]+\/Panopto)(\/Pages\/Viewer\.aspx\?id=)([-a-zA-Z0-9]+)/; -export const stopWatching = () => { - makeCall('stopWatchingExternalVideo'); -}; - export const isUrlValid = (url: string) => { if (YOUTUBE_SHORTS_REGEX.test(url)) { const shortsUrl = url.replace('shorts/', 'watch?v='); @@ -19,5 +14,5 @@ export const isUrlValid = (url: string) => { }; export default { - stopWatching, + isUrlValid, }; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx b/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx index 78d44fbc59..9a13f1d00f 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/mutations.tsx @@ -24,4 +24,10 @@ export const EXTERNAL_VIDEO_UPDATE = gql` } `; -export default { EXTERNAL_VIDEO_START, EXTERNAL_VIDEO_UPDATE }; +export const EXTERNAL_VIDEO_STOP = gql` + mutation ExternalVideoStop { + externalVideoStop + } +`; + +export default { EXTERNAL_VIDEO_START, EXTERNAL_VIDEO_UPDATE, EXTERNAL_VIDEO_STOP }; diff --git a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js index 7d23437391..5f4dfbdf91 100644 --- a/bigbluebutton-html5/imports/ui/components/external-video-player/service.js +++ b/bigbluebutton-html5/imports/ui/components/external-video-player/service.js @@ -1,7 +1,6 @@ import Auth from '/imports/ui/services/auth'; import { getStreamer } from '/imports/api/external-videos'; -import { makeCall } from '/imports/ui/services/api'; import ReactPlayer from 'react-player'; import Panopto from './custom-players/panopto'; @@ -18,10 +17,6 @@ const isUrlValid = (url) => { return /^https.*$/.test(url) && (ReactPlayer.canPlay(url) || Panopto.canPlay(url)); }; -const stopWatching = () => { - makeCall('stopWatchingExternalVideo'); -}; - const onMessage = (message, func) => { const streamer = getStreamer(Auth.meetingID); streamer.on(message, func); @@ -43,6 +38,5 @@ export { onMessage, removeAllListeners, isUrlValid, - stopWatching, getPlayingState, }; diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx index 263744f597..f7d8d8a254 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx @@ -51,6 +51,7 @@ class NotesDropdown extends PureComponent { intl, amIPresenter, presentations, + stopExternalVideoShare, } = this.props; const { converterButtonDisabled } = this.state; @@ -85,7 +86,7 @@ class NotesDropdown extends PureComponent { dataTest: 'pinNotes', label: intl.formatMessage(intlMessages.pinNotes), onClick: () => { - Service.pinSharedNotes(); + Service.pinSharedNotes(stopExternalVideoShare); }, }, ); diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx index e736b07c63..a401850e22 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx @@ -1,11 +1,12 @@ import React from 'react'; import NotesDropdown from './component'; import { layoutSelect } from '/imports/ui/components/layout/context'; -import { useSubscription } from '@apollo/client'; +import { useSubscription, useMutation } from '@apollo/client'; import { PROCESSED_PRESENTATIONS_SUBSCRIPTION, } from '/imports/ui/components/whiteboard/queries'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; +import { EXTERNAL_VIDEO_STOP } from '../../external-video-player/mutations'; const NotesDropdownContainer = ({ ...props }) => { const { data: currentUserData } = useCurrentUser((user) => ({ @@ -17,7 +18,21 @@ const NotesDropdownContainer = ({ ...props }) => { const { data: presentationData } = useSubscription(PROCESSED_PRESENTATIONS_SUBSCRIPTION); const presentations = presentationData?.pres_presentation || []; - return ; + const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP); + + return ( + + ); }; export default NotesDropdownContainer; diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js index afc7d7b1bb..6b3f9cd8b7 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js @@ -58,5 +58,5 @@ async function convertAndUpload(presentations) { export default { convertAndUpload, - pinSharedNotes: () => NotesService.pinSharedNotes(true), + pinSharedNotes: (stopWatching) => NotesService.pinSharedNotes(true, stopWatching), }; diff --git a/bigbluebutton-html5/imports/ui/components/notes/service.js b/bigbluebutton-html5/imports/ui/components/notes/service.js index fd56687766..f8f32e7e28 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/service.js +++ b/bigbluebutton-html5/imports/ui/components/notes/service.js @@ -58,8 +58,8 @@ const toggleNotesPanel = (sidebarContentPanel, layoutContextDispatch) => { }); }; -const pinSharedNotes = (pinned) => { - PadsService.pinPad(NOTES_CONFIG.id, pinned); +const pinSharedNotes = (pinned, stopWatching) => { + PadsService.pinPad(NOTES_CONFIG.id, pinned, stopWatching); }; const isSharedNotesPinned = () => { diff --git a/bigbluebutton-html5/imports/ui/components/pads/service.js b/bigbluebutton-html5/imports/ui/components/pads/service.js index 6cf2fab99f..87b5eca4f8 100644 --- a/bigbluebutton-html5/imports/ui/components/pads/service.js +++ b/bigbluebutton-html5/imports/ui/components/pads/service.js @@ -3,9 +3,6 @@ import Pads, { PadsSessions, PadsUpdates } from '/imports/api/pads'; import { makeCall } from '/imports/ui/services/api'; import Auth from '/imports/ui/services/auth'; import Settings from '/imports/ui/services/settings'; -import { - stopWatching, -} from '/imports/ui/components/external-video-player/service'; import { screenshareHasEnded, isScreenBroadcasting, @@ -113,7 +110,7 @@ const getPinnedPad = () => { return pad; }; -const pinPad = (externalId, pinned) => { +const pinPad = (externalId, pinned, stopWatching) => { if (pinned) { // Stop external video sharing if it's running. stopWatching(); diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx index 94af6b95a3..aa46dd43cb 100755 --- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx @@ -141,6 +141,7 @@ class ScreenshareComponent extends React.Component { layoutContextDispatch, toggleSwapLayout, pinSharedNotes, + stopExternalVideoShare, } = this.props; screenshareHasEnded(); window.removeEventListener('screensharePlayFailed', this.handlePlayElementFailed); @@ -173,7 +174,7 @@ class ScreenshareComponent extends React.Component { value: Session.get('presentationLastState'), }); - pinSharedNotes(Session.get('pinnedNotesLastState')); + pinSharedNotes(Session.get('pinnedNotesLastState'), stopExternalVideoShare); } clearMediaFlowingMonitor() { diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx index 23831a7456..9d212b6cef 100755 --- a/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/screenshare/container.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; +import { useMutation } from '@apollo/client'; import { getSharingContentType, getBroadcastContentType, @@ -16,6 +17,7 @@ import AudioService from '/imports/ui/components/audio/service'; import MediaService from '/imports/ui/components/media/service'; import { defineMessages } from 'react-intl'; import NotesService from '/imports/ui/components/notes/service'; +import { EXTERNAL_VIDEO_STOP } from '../external-video-player/mutations'; const screenshareIntlMessages = defineMessages({ // SCREENSHARE @@ -95,6 +97,7 @@ const ScreenshareContainer = (props) => { const { element } = fullscreen; const fullscreenElementId = 'Screenshare'; const fullscreenContext = (element === fullscreenElementId); + const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP); const { isPresenter } = props; @@ -130,6 +133,7 @@ const ScreenshareContainer = (props) => { ...screenShare, fullscreenContext, fullscreenElementId, + stopExternalVideoShare, ...selectedInfo, } } diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js index 8f8ae5b7b2..8d0ba3b977 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js +++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js @@ -3,7 +3,6 @@ import KurentoBridge from '/imports/api/screenshare/client/bridge'; import BridgeService from '/imports/api/screenshare/client/bridge/service'; import Settings from '/imports/ui/services/settings'; import logger from '/imports/startup/client/logger'; -import { stopWatching } from '/imports/ui/components/external-video-player/service'; import Meetings from '/imports/api/meetings'; import Auth from '/imports/ui/services/auth'; import AudioService from '/imports/ui/components/audio/service'; @@ -281,7 +280,7 @@ const screenshareHasStarted = (isPresenter, options = {}) => { } }; -const shareScreen = async (isPresenter, onFail, options = {}) => { +const shareScreen = async (stopWatching, isPresenter, onFail, options = {}) => { if (isCameraAsContentBroadcasting()) { screenshareHasEnded(); } diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx index 74cd63b593..6c90090887 100755 --- a/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx @@ -1,64 +1,71 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import Service from './service'; +import { useMutation } from '@apollo/client'; import VideoPreview from './component'; import VideoService from '../video-provider/service'; import ScreenShareService from '/imports/ui/components/screenshare/service'; import logger from '/imports/startup/client/logger'; import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors'; +import { EXTERNAL_VIDEO_STOP } from '../external-video-player/mutations'; const VideoPreviewContainer = (props) => ; -export default withTracker(({ setIsOpen, callbackToClose }) => ({ - startSharing: (deviceId) => { - callbackToClose(); - setIsOpen(false); - VideoService.joinVideo(deviceId); - }, - startSharingCameraAsContent: (deviceId) => { - callbackToClose(); - setIsOpen(false); - const handleFailure = (error) => { - const { - errorCode = SCREENSHARING_ERRORS.UNKNOWN_ERROR.errorCode, - errorMessage = error.message, - } = error; +export default withTracker(({ setIsOpen, callbackToClose }) => { + const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP); - logger.error({ - logCode: 'camera_as_content_failed', - extraInfo: { errorCode, errorMessage }, - }, `Sharing camera as content failed: ${errorMessage} (code=${errorCode})`); + return { + startSharing: (deviceId) => { + callbackToClose(); + setIsOpen(false); + VideoService.joinVideo(deviceId); + }, + startSharingCameraAsContent: (deviceId) => { + callbackToClose(); + setIsOpen(false); + const handleFailure = (error) => { + const { + errorCode = SCREENSHARING_ERRORS.UNKNOWN_ERROR.errorCode, + errorMessage = error.message, + } = error; + logger.error({ + logCode: 'camera_as_content_failed', + extraInfo: { errorCode, errorMessage }, + }, `Sharing camera as content failed: ${errorMessage} (code=${errorCode})`); + + ScreenShareService.screenshareHasEnded(); + }; + ScreenShareService.shareScreen( + stopExternalVideoShare, + true, handleFailure, { stream: Service.getStream(deviceId)._mediaStream } + ); + ScreenShareService.setCameraAsContentDeviceId(deviceId); + }, + stopSharing: (deviceId) => { + callbackToClose(); + setIsOpen(false); + if (deviceId) { + const streamId = VideoService.getMyStreamId(deviceId); + if (streamId) VideoService.stopVideo(streamId); + } else { + VideoService.exitVideo(); + } + }, + stopSharingCameraAsContent: () => { + callbackToClose(); + setIsOpen(false); ScreenShareService.screenshareHasEnded(); - }; - ScreenShareService.shareScreen( - true, handleFailure, { stream: Service.getStream(deviceId)._mediaStream } - ); - ScreenShareService.setCameraAsContentDeviceId(deviceId); - }, - stopSharing: (deviceId) => { - callbackToClose(); - setIsOpen(false); - if (deviceId) { - const streamId = VideoService.getMyStreamId(deviceId); - if (streamId) VideoService.stopVideo(streamId); - } else { - VideoService.exitVideo(); - } - }, - stopSharingCameraAsContent: () => { - callbackToClose(); - setIsOpen(false); - ScreenShareService.screenshareHasEnded(); - }, - sharedDevices: VideoService.getSharedDevices(), - cameraAsContentDeviceId: ScreenShareService.getCameraAsContentDeviceId(), - isCamLocked: VideoService.isUserLocked(), - camCapReached: VideoService.hasCapReached(), - closeModal: () => { - callbackToClose(); - setIsOpen(false); - }, - webcamDeviceId: Service.webcamDeviceId(), - hasVideoStream: VideoService.hasVideoStream(), -}))(VideoPreviewContainer); + }, + sharedDevices: VideoService.getSharedDevices(), + cameraAsContentDeviceId: ScreenShareService.getCameraAsContentDeviceId(), + isCamLocked: VideoService.isUserLocked(), + camCapReached: VideoService.hasCapReached(), + closeModal: () => { + callbackToClose(); + setIsOpen(false); + }, + webcamDeviceId: Service.webcamDeviceId(), + hasVideoStream: VideoService.hasVideoStream(), + }; +})(VideoPreviewContainer); diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js index a5c41244a6..602a09fa57 100755 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -22,7 +22,6 @@ import '/imports/api/users-persistent-data/server'; import '/imports/api/connection-status/server'; import '/imports/api/timer/server'; import '/imports/api/audio-captions/server'; -import '/imports/api/external-videos/server'; import '/imports/api/pads/server'; import '/imports/api/local-settings/server'; import '/imports/api/voice-call-states/server'; From 9e5d678df4471ee7aa4a0c9d83b6c6e53fd7778b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 19 Jan 2024 15:47:10 -0300 Subject: [PATCH 105/512] fix stop external video on screenshare --- .../imports/ui/components/screenshare/service.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js index 8d0ba3b977..a8f2788beb 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js +++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js @@ -3,7 +3,6 @@ import KurentoBridge from '/imports/api/screenshare/client/bridge'; import BridgeService from '/imports/api/screenshare/client/bridge/service'; import Settings from '/imports/ui/services/settings'; import logger from '/imports/startup/client/logger'; -import Meetings from '/imports/api/meetings'; import Auth from '/imports/ui/services/auth'; import AudioService from '/imports/ui/components/audio/service'; import { Meteor } from "meteor/meteor"; @@ -284,12 +283,6 @@ const shareScreen = async (stopWatching, isPresenter, onFail, options = {}) => { if (isCameraAsContentBroadcasting()) { screenshareHasEnded(); } - // stop external video share if running - const meeting = Meetings.findOne({ meetingId: Auth.meetingID }); - - if (meeting && meeting.externalVideoUrl) { - stopWatching(); - } try { let stream; @@ -318,6 +311,8 @@ const shareScreen = async (stopWatching, isPresenter, onFail, options = {}) => { // Close Shared Notes if open. NotesService.pinSharedNotes(false); + // stop external video share if running + stopWatching(); setSharingContentType(contentType); setIsSharing(true); From fcb048e1fefcaec0ab38a1e8d6519265257e87b0 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Fri, 19 Jan 2024 15:30:33 -0500 Subject: [PATCH 106/512] chore(export-ann): bump axios etc --- bbb-export-annotations/package-lock.json | 60 +++++++++++++++--------- bbb-export-annotations/package.json | 6 +-- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/bbb-export-annotations/package-lock.json b/bbb-export-annotations/package-lock.json index ab6424497d..b78a6ad9f5 100644 --- a/bbb-export-annotations/package-lock.json +++ b/bbb-export-annotations/package-lock.json @@ -8,7 +8,7 @@ "name": "bbb-export-annotations", "version": "0.0.1", "dependencies": { - "axios": "^0.26.0", + "axios": "^1.6.5", "form-data": "^4.0.0", "perfect-freehand": "^1.0.16", "probe-image-size": "^7.2.3", @@ -21,8 +21,8 @@ "eslint-config-google": "^0.14.0" }, "engines": { - "node": ">=16", - "npm": ">=8.5" + "node": ">=18.16.0", + "npm": ">=9.5.0" } }, "node_modules/@eslint/eslintrc": { @@ -287,11 +287,13 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/axios": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz", - "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dependencies": { - "follow-redirects": "^1.14.8" + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/balanced-match": { @@ -692,9 +694,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -1067,6 +1069,11 @@ "stream-parser": "~0.3.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -1319,9 +1326,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -1573,11 +1580,13 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "axios": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz", - "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "requires": { - "follow-redirects": "^1.14.8" + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "balanced-match": { @@ -1881,9 +1890,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" }, "form-data": { "version": "4.0.0", @@ -2161,6 +2170,11 @@ "stream-parser": "~0.3.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -2358,9 +2372,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, "wrappy": { diff --git a/bbb-export-annotations/package.json b/bbb-export-annotations/package.json index b32bc07158..67a3bdb2f2 100644 --- a/bbb-export-annotations/package.json +++ b/bbb-export-annotations/package.json @@ -7,7 +7,7 @@ "lint:fix": "eslint --fix **/*.js" }, "dependencies": { - "axios": "^0.26.0", + "axios": "^1.6.5", "form-data": "^4.0.0", "perfect-freehand": "^1.0.16", "probe-image-size": "^7.2.3", @@ -20,7 +20,7 @@ "eslint-config-google": "^0.14.0" }, "engines": { - "node": "^18.16.0", - "npm": "^9.5.0" + "node": ">=18.16.0", + "npm": ">=9.5.0" } } From 6a887c442f96b3aff1a29f56d06043a99b4e611f Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Thu, 4 Jan 2024 16:08:44 -0500 Subject: [PATCH 107/512] fix: Bump spring-boot-starter-validation to 2.7.17 to match bbb-web --- bbb-common-web/build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-common-web/build.sbt b/bbb-common-web/build.sbt index e7d24da7b7..8d713533f9 100755 --- a/bbb-common-web/build.sbt +++ b/bbb-common-web/build.sbt @@ -103,7 +103,7 @@ homepage := Some(url("http://www.bigbluebutton.org")) libraryDependencies ++= Seq( "javax.validation" % "validation-api" % "2.0.1.Final", - "org.springframework.boot" % "spring-boot-starter-validation" % "2.7.12", + "org.springframework.boot" % "spring-boot-starter-validation" % "2.7.17", "org.springframework.data" % "spring-data-commons" % "2.7.6", "org.apache.httpcomponents" % "httpclient" % "4.5.13", "org.postgresql" % "postgresql" % "42.4.3", From 02f1fe686d031aec0c55f30ef106c0369ee9e590 Mon Sep 17 00:00:00 2001 From: Paul Trudel Date: Tue, 21 Nov 2023 16:26:39 +0000 Subject: [PATCH 108/512] Upgrade Grails to 6.1 --- bigbluebutton-web/build.gradle | 16 +++++++++------- bigbluebutton-web/gradle.properties | 6 +++--- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/bigbluebutton-web/build.gradle b/bigbluebutton-web/build.gradle index 38aa753703..778aeb1ab3 100755 --- a/bigbluebutton-web/build.gradle +++ b/bigbluebutton-web/build.gradle @@ -9,7 +9,7 @@ buildscript { dependencies { classpath "org.grails:grails-gradle-plugin:${grailsGradlePluginVersion}" classpath "org.grails.plugins:hibernate5:${gormVersion}" - classpath "com.bertramlabs.plugins:asset-pipeline-gradle:4.0.0" + classpath "com.bertramlabs.plugins:asset-pipeline-gradle:4.3.0" classpath "gradle.plugin.com.github.erdi.webdriver-binaries:webdriver-binaries-gradle-plugin:2.6" classpath "org.grails.plugins:views-gradle:2.1.1" classpath "org.grails.plugins:views-json:2.1.1" @@ -53,10 +53,10 @@ repositories { } dependencies { - runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails:4.0.0" + runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails:4.3.0" - implementation "org.springframework:spring-core:5.3.21" - implementation "org.springframework:spring-context:5.3.27" + implementation "org.springframework:spring-core:5.3.31" + implementation "org.springframework:spring-context:5.3.31" implementation "org.springframework.boot:spring-boot:${springVersion}" implementation "org.springframework.boot:spring-boot-starter-logging:${springVersion}" implementation "org.springframework.boot:spring-boot-autoconfigure:${springVersion}" @@ -65,7 +65,7 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-tomcat:${springVersion}" implementation "org.grails:grails-web-boot:5.2.5" - implementation "org.springframework:spring-webmvc:5.3.27" + implementation "org.springframework:spring-webmvc:5.3.31" implementation "org.grails:grails-logging" implementation "org.grails:grails-plugin-rest:5.2.5" @@ -79,7 +79,7 @@ dependencies { implementation "org.grails.plugins:views-json:2.1.1" implementation "org.grails.plugins:cache" implementation "org.apache.xmlbeans:xmlbeans:5.0.3" - implementation "org.grails:grails-gradle-plugin:5.1.4" + implementation "org.grails:grails-gradle-plugin:${grailsGradlePluginVersion}" implementation "org.grails.plugins:async" implementation "org.grails.plugins:scaffolding" implementation "org.grails.plugins:events" @@ -109,7 +109,7 @@ dependencies { //--- BigBlueButton Dependencies End console "org.grails:grails-console:5.2.0" profile "org.grails.profiles:web" - runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails:4.0.0" + runtimeOnly "com.bertramlabs.plugins:asset-pipeline-grails:4.3.0" testImplementation "org.grails:grails-gorm-testing-support" testImplementation "org.grails.plugins:geb" testImplementation "org.grails:grails-web-testing-support" @@ -127,6 +127,8 @@ configurations.implementation { exclude group: 'io.micronaut', module: 'micronaut-aop' exclude group: 'com.h2database', module: 'h2' exclude group: 'org.graalvm.sdk', module: 'graal-sdk' + exclude group: 'io.github.gradle-nexus', module: 'publish-plugin' + exclude group: 'org.grails', module: 'grails-shell' } configurations { diff --git a/bigbluebutton-web/gradle.properties b/bigbluebutton-web/gradle.properties index 8212141475..0baceadce8 100644 --- a/bigbluebutton-web/gradle.properties +++ b/bigbluebutton-web/gradle.properties @@ -1,7 +1,7 @@ -grailsVersion=5.3.3 +grailsVersion=6.1.0 gormVersion=7.3.1 -gradleWrapperVersion=7.3.1 -grailsGradlePluginVersion=5.0.0 +gradleWrapperVersion=7.6.3 +grailsGradlePluginVersion=6.1.0 groovyVersion=3.0.19 tomcatEmbedVersion=9.0.82 springVersion=2.7.17 \ No newline at end of file diff --git a/bigbluebutton-web/gradle/wrapper/gradle-wrapper.properties b/bigbluebutton-web/gradle/wrapper/gradle-wrapper.properties index fd636e6c98..3cb5fe56a3 100644 --- a/bigbluebutton-web/gradle/wrapper/gradle-wrapper.properties +++ b/bigbluebutton-web/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip \ No newline at end of file +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-bin.zip \ No newline at end of file From 7c10f49f4a11b4b45b37daaf2c49ff93312873a3 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Wed, 10 Jan 2024 14:15:56 -0500 Subject: [PATCH 109/512] fix(sec): filter tags in presentation name --- .../src/main/java/org/bigbluebutton/api/util/ParamsUtil.java | 4 ++++ bigbluebutton-html5/imports/ui/components/chat/service.js | 3 ++- .../web/controllers/PresentationController.groovy | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/util/ParamsUtil.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/util/ParamsUtil.java index 6e6697ad23..3f5c07aad6 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/util/ParamsUtil.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/util/ParamsUtil.java @@ -21,6 +21,10 @@ public class ParamsUtil { return text.replaceAll("\\p{Cc}", "").trim(); } + public static String stripTags(String text) { + return text.replaceAll("<[^>]*>", ""); +} + public static String escapeHTMLTags(String value) { return StringEscapeUtils.escapeHtml4(value); } diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js index 8c1836cebc..609607b9d7 100755 --- a/bigbluebutton-html5/imports/ui/components/chat/service.js +++ b/bigbluebutton-html5/imports/ui/components/chat/service.js @@ -291,13 +291,14 @@ const removePackagedClassAttribute = (classnames, attribute) => { }; const getExportedPresentationString = (fileURI, filename, intl, fileStateType) => { + const sanitizedFilename = stripTags(filename); const intlFileStateType = fileStateType === 'Original' ? intlMessages.original : intlMessages.withWhiteboardAnnotations; const href = `${APP.bbbWebBase}/${fileURI}`; const warningIcon = ''; const label = `${intl.formatMessage(intlMessages.download)}`; const notAccessibleWarning = `${warningIcon}`; const link = `${label} ${notAccessibleWarning}`; - const name = `${filename} (${intl.formatMessage(intlFileStateType)})`; + const name = `${sanitizedFilename} (${intl.formatMessage(intlFileStateType)})`; return `${name}
${link}`; }; diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy index 84a799e40d..1a4696a9d5 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy @@ -30,6 +30,7 @@ import org.apache.commons.io.FilenameUtils; import org.bigbluebutton.web.services.PresentationService import org.bigbluebutton.presentation.UploadedPresentation import org.bigbluebutton.api.MeetingService; +import org.bigbluebutton.api.util.ParamsUtil; import org.bigbluebutton.api.Util; class PresentationController { @@ -164,6 +165,7 @@ class PresentationController { // Gets the name minus the path from a full fileName. // a/b/c.txt --> c.txt presFilename = FilenameUtils.getName(presOrigFilename) + presFilename = ParamsUtil.stripTags(presFilename) filenameExt = FilenameUtils.getExtension(presFilename) } else { log.warn "Upload failed. File Empty." From 554f4f2e2ac097c06c7fc00f1f8838ba70e308a6 Mon Sep 17 00:00:00 2001 From: GuiLeme Date: Thu, 9 Nov 2023 09:50:38 -0300 Subject: [PATCH 110/512] [GHSA-j42p-fh2w-24q6] - validate URL for external upload of presentation. --- .../api/service/ValidationService.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java index 7971c456c3..b26b367adb 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/service/ValidationService.java @@ -14,6 +14,9 @@ import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.*; @@ -76,6 +79,11 @@ public class ValidationService { if(request == null) { violations.put("validationError", "Request not recognized"); + } else if(params.containsKey("presentationUploadExternalUrl")) { + String urlToValidate = params.get("presentationUploadExternalUrl")[0]; + if(!this.isValidURL(urlToValidate)) { + violations.put("validationError", "Param 'presentationUploadExternalUrl' is not a valid URL"); + } } else { request.populateFromParamsMap(params); violations = performValidation(request); @@ -84,6 +92,15 @@ public class ValidationService { return violations; } + boolean isValidURL(String url) { + try { + new URL(url).toURI(); + return true; + } catch (MalformedURLException | URISyntaxException e) { + return false; + } + } + private Request initializeRequest(ApiCall apiCall, Map params, String queryString) { Request request = null; Checksum checksum; From 7e313ed92ae1b453409b879c9ecec0f219e4e1b4 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Fri, 19 Jan 2024 16:10:40 -0500 Subject: [PATCH 111/512] [Snyk] Fix for 2 vulnerabilities (recording-imex) --- bbb-recording-imex/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bbb-recording-imex/pom.xml b/bbb-recording-imex/pom.xml index d3818141a3..bf7809a5c0 100755 --- a/bbb-recording-imex/pom.xml +++ b/bbb-recording-imex/pom.xml @@ -75,7 +75,7 @@ ch.qos.logback logback-core - 1.2.11 + 1.4.14 org.slf4j @@ -85,7 +85,7 @@ ch.qos.logback logback-classic - 1.2.11 + 1.4.14 From 4d64464029ab86efeac79cc00c8f1e00edb0c6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Sat, 20 Jan 2024 11:01:20 -0300 Subject: [PATCH 112/512] migrate switchSlide action --- .../imports/api/slides/server/index.js | 1 - .../imports/api/slides/server/methods.js | 6 -- .../api/slides/server/methods/switchSlide.js | 30 -------- .../ui/components/presentation/mutations.jsx | 10 +++ .../presentation-toolbar/component.jsx | 20 ++---- .../presentation-toolbar/container.jsx | 40 +++++++++-- .../presentation-toolbar/service.js | 25 ------- .../ui/components/whiteboard/component.jsx | 70 +++++++++---------- .../ui/components/whiteboard/container.jsx | 6 -- .../ui/components/whiteboard/service.js | 5 -- bigbluebutton-html5/server/main.js | 1 - 11 files changed, 84 insertions(+), 130 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/slides/server/index.js delete mode 100644 bigbluebutton-html5/imports/api/slides/server/methods.js delete mode 100755 bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js delete mode 100755 bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js diff --git a/bigbluebutton-html5/imports/api/slides/server/index.js b/bigbluebutton-html5/imports/api/slides/server/index.js deleted file mode 100644 index ba55c4a16e..0000000000 --- a/bigbluebutton-html5/imports/api/slides/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './methods'; diff --git a/bigbluebutton-html5/imports/api/slides/server/methods.js b/bigbluebutton-html5/imports/api/slides/server/methods.js deleted file mode 100644 index 211d15fe65..0000000000 --- a/bigbluebutton-html5/imports/api/slides/server/methods.js +++ /dev/null @@ -1,6 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import switchSlide from './methods/switchSlide'; - -Meteor.methods({ - switchSlide, -}); diff --git a/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js b/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js deleted file mode 100755 index 76d4bdd2b0..0000000000 --- a/bigbluebutton-html5/imports/api/slides/server/methods/switchSlide.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import RedisPubSub from '/imports/startup/server/redis'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import Logger from '/imports/startup/server/logger'; - -export default async function switchSlide(slideNumber, podId, presentationId) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'SetCurrentPagePubMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(slideNumber, Number); - check(podId, String); - - const payload = { - podId, - presentationId, - pageId: `${presentationId}/${slideNumber}`, - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method switchSlide ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index 39b4d4490a..9afed2f79f 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -23,7 +23,17 @@ export const PRESENTATION_SET_WRITERS = gql` } `; +export const PRESENTATION_SET_PAGE = gql` + mutation PresentationSetPage($presentationId: String!, $pageId: String!) { + presentationSetPage( + presentationId: $presentationId, + pageId: $pageId, + ) + } +`; + export default { PRESENTATION_SET_ZOOM, PRESENTATION_SET_WRITERS, + PRESENTATION_SET_PAGE, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx index 8748bdf4b2..06333f46b8 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/component.jsx @@ -150,12 +150,12 @@ class PresentationToolbar extends PureComponent { } handleSkipToSlideChange(event) { - const { skipToSlide, presentationId } = this.props; + const { skipToSlide } = this.props; const requestedSlideNum = Number.parseInt(event.target.value, 10); this.handleFTWSlideChange(); if (event) event.currentTarget.blur(); - skipToSlide(requestedSlideNum, presentationId); + skipToSlide(requestedSlideNum); } handleSwitchWhiteboardMode() { @@ -194,29 +194,21 @@ class PresentationToolbar extends PureComponent { } nextSlideHandler(event) { - const { - nextSlide, - currentSlideNum, - numberOfSlides, - endCurrentPoll, - presentationId, - } = this.props; + const { nextSlide, endCurrentPoll } = this.props; this.handleFTWSlideChange(); if (event) event.currentTarget.blur(); endCurrentPoll(); - nextSlide(currentSlideNum, numberOfSlides, presentationId); + nextSlide(); } previousSlideHandler(event) { - const { - previousSlide, currentSlideNum, endCurrentPoll, presentationId - } = this.props; + const { previousSlide, endCurrentPoll } = this.props; this.handleFTWSlideChange(); if (event) event.currentTarget.blur(); endCurrentPoll(); - previousSlide(currentSlideNum, presentationId); + previousSlide(); } switchSlide(event) { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx index bb9eb4af95..041e55cb8f 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/container.jsx @@ -2,19 +2,24 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { withTracker } from 'meteor/react-meteor-data'; import PresentationToolbar from './component'; -import PresentationToolbarService from './service'; import FullscreenService from '/imports/ui/components/common/fullscreen-button/service'; import { isPollingEnabled } from '/imports/ui/services/features'; import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context'; import { useSubscription, useMutation } from '@apollo/client'; import POLL_SUBSCRIPTION from '/imports/ui/core/graphql/queries/pollSubscription'; import { POLL_CANCEL, POLL_CREATE } from '/imports/ui/components/poll/mutations'; +import { PRESENTATION_SET_PAGE } from '../mutations'; const PresentationToolbarContainer = (props) => { const pluginsContext = useContext(PluginsContext); const { pluginsExtensibleAreasAggregatedState } = pluginsContext; - const { userIsPresenter, layoutSwapped } = props; + const { + userIsPresenter, + layoutSwapped, + currentSlideNum, + presentationId, + } = props; const { data: pollData } = useSubscription(POLL_SUBSCRIPTION); const hasPoll = pollData?.poll?.length > 0; @@ -23,11 +28,36 @@ const PresentationToolbarContainer = (props) => { const [stopPoll] = useMutation(POLL_CANCEL); const [createPoll] = useMutation(POLL_CREATE); + const [presentationSetPage] = useMutation(PRESENTATION_SET_PAGE); const endCurrentPoll = () => { if (hasPoll) stopPoll(); }; + const setPresentationPage = (pageId) => { + presentationSetPage({ + variables: { + presentationId, + pageId, + }, + }); + }; + + const skipToSlide = (slideNum) => { + const slideId = `${presentationId}/${slideNum}`; + setPresentationPage(slideId); + }; + + const previousSlide = () => { + const prevSlideNum = currentSlideNum - 1; + skipToSlide(prevSlideNum); + }; + + const nextSlide = () => { + const nextSlideNum = currentSlideNum + 1; + skipToSlide(nextSlideNum); + }; + const startPoll = (pollType, pollId, answers = [], question, isMultipleResponse = false) => { Session.set('openPanel', 'poll'); Session.set('forcePollOpen', true); @@ -60,6 +90,9 @@ const PresentationToolbarContainer = (props) => { pluginProvidedPresentationToolbarItems, handleToggleFullScreen, startPoll, + previousSlide, + nextSlide, + skipToSlide, }} /> ); @@ -69,9 +102,6 @@ const PresentationToolbarContainer = (props) => { export default withTracker(() => { return { - nextSlide: PresentationToolbarService.nextSlide, - previousSlide: PresentationToolbarService.previousSlide, - skipToSlide: PresentationToolbarService.skipToSlide, isMeteorConnected: Meteor.status().connected, isPollingEnabled: isPollingEnabled(), }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js deleted file mode 100755 index 2c843f4eee..0000000000 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-toolbar/service.js +++ /dev/null @@ -1,25 +0,0 @@ -import { makeCall } from '/imports/ui/services/api'; - -const POD_ID = 'DEFAULT_PRESENTATION_POD'; - -const previousSlide = (currentSlideNum, presentationId) => { - if (currentSlideNum > 1) { - makeCall('switchSlide', currentSlideNum - 1, POD_ID, presentationId); - } -}; - -const nextSlide = (currentSlideNum, numberOfSlides, presentationId) => { - if (currentSlideNum < numberOfSlides) { - makeCall('switchSlide', currentSlideNum + 1, POD_ID, presentationId); - } -}; - -const skipToSlide = (requestedSlideNum, presentationId) => { - makeCall('switchSlide', requestedSlideNum, POD_ID, presentationId); -}; - -export default { - nextSlide, - previousSlide, - skipToSlide, -}; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 80b473b137..9cddcda5c7 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -106,7 +106,6 @@ export default Whiteboard = React.memo(function Whiteboard(props) { currentUser, whiteboardId, zoomSlide, - skipToSlide, curPageId, zoomChanger, isMultiUserActive, @@ -117,11 +116,11 @@ export default Whiteboard = React.memo(function Whiteboard(props) { svgUri, maxStickyNoteLength, fontFamily, - colorStyle, - dashStyle, - fillStyle, - fontStyle, - sizeStyle, + colorStyle, + dashStyle, + fillStyle, + fontStyle, + sizeStyle, hasShapeAccess, presentationAreaHeight, presentationAreaWidth, @@ -670,29 +669,29 @@ export default Whiteboard = React.memo(function Whiteboard(props) { console.log("EDITOR : ", editor); const debouncePersistShape = debounce({ delay: 0 }, persistShape); - - const colorStyles = ['black', 'blue', 'green', 'grey', 'light-blue', 'light-green', 'light-red', 'light-violet', 'orange', 'red', 'violet', 'yellow']; - const dashStyles = ['dashed', 'dotted', 'draw', 'solid']; - const fillStyles = ['none', 'pattern', 'semi', 'solid']; - const fontStyles = ['draw','mono','sans', 'serif']; - const sizeStyles = ['l', 'm', 's', 'xl']; - - if ( colorStyles.includes(colorStyle) ) { - editor.setStyleForNextShapes(DefaultColorStyle, colorStyle); - } - if ( dashStyles.includes(dashStyle) ) { - editor.setStyleForNextShapes(DefaultDashStyle, dashStyle); - } - if ( fillStyles.includes(fillStyle) ) { - editor.setStyleForNextShapes(DefaultFillStyle, fillStyle); - } - if ( fontStyles.includes(fontStyle)) { - editor.setStyleForNextShapes(DefaultFontStyle, fontStyle); - } - if ( sizeStyles.includes(sizeStyle) ) { - editor.setStyleForNextShapes(DefaultSizeStyle, sizeStyle); - } - + + const colorStyles = ['black', 'blue', 'green', 'grey', 'light-blue', 'light-green', 'light-red', 'light-violet', 'orange', 'red', 'violet', 'yellow']; + const dashStyles = ['dashed', 'dotted', 'draw', 'solid']; + const fillStyles = ['none', 'pattern', 'semi', 'solid']; + const fontStyles = ['draw','mono','sans', 'serif']; + const sizeStyles = ['l', 'm', 's', 'xl']; + + if ( colorStyles.includes(colorStyle) ) { + editor.setStyleForNextShapes(DefaultColorStyle, colorStyle); + } + if ( dashStyles.includes(dashStyle) ) { + editor.setStyleForNextShapes(DefaultDashStyle, dashStyle); + } + if ( fillStyles.includes(fillStyle) ) { + editor.setStyleForNextShapes(DefaultFillStyle, fillStyle); + } + if ( fontStyles.includes(fontStyle)) { + editor.setStyleForNextShapes(DefaultFontStyle, fontStyle); + } + if ( sizeStyles.includes(sizeStyle) ) { + editor.setStyleForNextShapes(DefaultSizeStyle, sizeStyle); + } + editor.store.listen( (entry) => { const { changes } = entry; @@ -900,7 +899,6 @@ Whiteboard.propTypes = { }).isRequired, whiteboardId: PropTypes.string, zoomSlide: PropTypes.func.isRequired, - skipToSlide: PropTypes.func.isRequired, curPageId: PropTypes.string.isRequired, presentationWidth: PropTypes.number.isRequired, presentationHeight: PropTypes.number.isRequired, @@ -914,11 +912,11 @@ Whiteboard.propTypes = { svgUri: PropTypes.string, maxStickyNoteLength: PropTypes.number.isRequired, fontFamily: PropTypes.string.isRequired, - colorStyle: PropTypes.string.isRequired, - dashStyle: PropTypes.string.isRequired, - fillStyle: PropTypes.string.isRequired, - fontStyle: PropTypes.string.isRequired, - sizeStyle: PropTypes.string.isRequired, + colorStyle: PropTypes.string.isRequired, + dashStyle: PropTypes.string.isRequired, + fillStyle: PropTypes.string.isRequired, + fontStyle: PropTypes.string.isRequired, + sizeStyle: PropTypes.string.isRequired, hasShapeAccess: PropTypes.func.isRequired, presentationAreaHeight: PropTypes.number.isRequired, presentationAreaWidth: PropTypes.number.isRequired, @@ -934,9 +932,7 @@ Whiteboard.propTypes = { fullscreenAction: PropTypes.string.isRequired, fullscreenRef: PropTypes.instanceOf(Element), handleToggleFullScreen: PropTypes.func.isRequired, - nextSlide: PropTypes.func.isRequired, numberOfSlides: PropTypes.number.isRequired, - previousSlide: PropTypes.func.isRequired, sidebarNavigationWidth: PropTypes.number, presentationId: PropTypes.string, }; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index 3cdb96951c..2266f8507b 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -11,14 +11,12 @@ import { initDefaultPages, persistShape, removeShapes, - changeCurrentSlide, notifyNotAllowedChange, notifyShapeNumberExceeded, toggleToolsAnimations, formatAnnotations, } from './service'; import CursorService from './cursors/service'; -import PresentationToolbarService from '../presentation/presentation-toolbar/service'; import SettingsService from '/imports/ui/services/settings'; import Auth from '/imports/ui/services/auth'; import { @@ -235,15 +233,11 @@ const WhiteboardContainer = (props) => { initDefaultPages, persistShape, isMultiUserActive, - changeCurrentSlide, shapes, bgShape, assets, removeShapes, zoomSlide, - skipToSlide: PresentationToolbarService.skipToSlide, - nextSlide: PresentationToolbarService.nextSlide, - previousSlide: PresentationToolbarService.previousSlide, numberOfSlides: currentPresentationPage?.totalPages, notifyNotAllowedChange, notifyShapeNumberExceeded, diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js index 2c91fec606..6043a7f426 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js @@ -100,10 +100,6 @@ const persistShape = (shape, whiteboardId, isModerator) => { const removeShapes = (shapes, whiteboardId) => makeCall('deleteAnnotations', shapes, whiteboardId); -const changeCurrentSlide = (s) => { - makeCall('changeCurrentSlide', s); -}; - const initDefaultPages = (count = 1) => { const pages = {}; const pageStates = {}; @@ -284,7 +280,6 @@ export { getMultiUser, persistShape, removeShapes, - changeCurrentSlide, notifyNotAllowedChange, notifyShapeNumberExceeded, toggleToolsAnimations, diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js index a5c41244a6..739d7f3c09 100755 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -9,7 +9,6 @@ import '/imports/api/polls/server'; import '/imports/api/captions/server'; import '/imports/api/presentations/server'; import '/imports/api/presentation-upload-token/server'; -import '/imports/api/slides/server'; import '/imports/api/breakouts/server'; import '/imports/api/breakouts-history/server'; import '/imports/api/screenshare/server'; From 0319fb7d27a3bc52f75d2a6dc0768a64955ef348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Sat, 20 Jan 2024 11:43:34 -0300 Subject: [PATCH 113/512] migrate exportPresentation action --- .../api/presentations/server/methods.js | 2 -- .../server/methods/exportPresentation.js | 29 ------------------- .../ui/components/presentation/mutations.jsx | 14 +++++++++ .../presentation-uploader/container.jsx | 14 +++++++-- 4 files changed, 25 insertions(+), 34 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentation.js diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods.js b/bigbluebutton-html5/imports/api/presentations/server/methods.js index f02acab509..ab965e9787 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods.js @@ -2,11 +2,9 @@ import { Meteor } from 'meteor/meteor'; import removePresentation from './methods/removePresentation'; import setPresentation from './methods/setPresentation'; import setPresentationDownloadable from './methods/setPresentationDownloadable'; -import exportPresentation from './methods/exportPresentation'; Meteor.methods({ removePresentation, setPresentation, setPresentationDownloadable, - exportPresentation, }); diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentation.js b/bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentation.js deleted file mode 100644 index 0eb8ce9cfa..0000000000 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/exportPresentation.js +++ /dev/null @@ -1,29 +0,0 @@ -import RedisPubSub from '/imports/startup/server/redis'; -import { check } from 'meteor/check'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import Logger from '/imports/startup/server/logger'; - -export default async function exportPresentation(presentationId, fileStateType) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'MakePresentationDownloadReqMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(presentationId, String); - - const payload = { - presId: presentationId, - allPages: true, - fileStateType, - pages: [], - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method exportPresentation ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index 9afed2f79f..78a5a95549 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -32,8 +32,22 @@ export const PRESENTATION_SET_PAGE = gql` } `; +export const PRESENTATION_SET_DOWNLOADABLE = gql` + mutation PresentationSetDownloadable( + $presentationId: String!, + $downloadable: Boolean!, + $fileStateType: String!,) { + presentationSetDownloadable( + presentationId: $presentationId, + downloadable: $downloadable, + fileStateType: $fileStateType, + ) + } +`; + export default { PRESENTATION_SET_ZOOM, PRESENTATION_SET_WRITERS, PRESENTATION_SET_PAGE, + PRESENTATION_SET_DOWNLOADABLE, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx index cbc9adb22e..d2ca8dbcd6 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx @@ -1,9 +1,9 @@ import React from 'react'; import { Meteor } from 'meteor/meteor'; import { withTracker } from 'meteor/react-meteor-data'; -import { makeCall } from '/imports/ui/services/api'; import ErrorBoundary from '/imports/ui/components/common/error-boundary/component'; import FallbackModal from '/imports/ui/components/common/fallback-errors/fallback-modal/component'; +import { useSubscription, useMutation } from '@apollo/client'; import Service from './service'; import PresUploaderToast from '/imports/ui/components/presentation/presentation-toast/presentation-uploader-toast/component'; import PresentationUploader from './component'; @@ -13,11 +13,11 @@ import { isDownloadPresentationConvertedToPdfEnabled, isPresentationEnabled, } from '/imports/ui/services/features'; -import { useSubscription } from '@apollo/client'; import { PRESENTATIONS_SUBSCRIPTION, } from '/imports/ui/components/whiteboard/queries'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; +import { PRESENTATION_SET_DOWNLOADABLE } from '../mutations'; const PRESENTATION_CONFIG = Meteor.settings.public.presentation; @@ -31,8 +31,16 @@ const PresentationUploaderContainer = (props) => { const presentations = presentationData?.pres_presentation || []; const currentPresentation = presentations.find((p) => p.current)?.presentationId || ''; + const [presentationSetDownloadable] = useMutation(PRESENTATION_SET_DOWNLOADABLE); + const exportPresentation = (presentationId, fileStateType) => { - makeCall('exportPresentation', presentationId, fileStateType); + presentationSetDownloadable({ + variables: { + presentationId, + downloadable: true, + fileStateType, + }, + }); }; return userIsPresenter && ( From c3b3d6c3901c02b3f46b38d1742115b78fccbdc2 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Sat, 20 Jan 2024 13:41:14 -0300 Subject: [PATCH 114/512] Stop logging as Error when it's a normal WS conn closing --- .../internal/hascli/conn/reader/reader.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go index 486f76b641..f7daf1691a 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go +++ b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go @@ -1,6 +1,8 @@ package reader import ( + "context" + "errors" "github.com/iMDT/bbb-graphql-middleware/internal/common" "github.com/iMDT/bbb-graphql-middleware/internal/hascli/retransmiter" "github.com/iMDT/bbb-graphql-middleware/internal/msgpatch" @@ -23,7 +25,11 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan var message interface{} err := wsjson.Read(hc.Context, hc.Websocket, &message) if err != nil { - log.Errorf("Error: %v", err) + if errors.Is(err, context.Canceled) { + log.Debugf("Closing ws connection as Context was cancelled!") + } else { + log.Errorf("Error reading message from Hasura: %v", err) + } return } @@ -59,6 +65,7 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan subscription.Type == common.Subscription { msgpatch.PatchMessage(&messageAsMap, hc.Browserconn) } + } // Write the message to browser From 38132045e28035056771bc9b8ee4f6a70932e203 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Sat, 20 Jan 2024 13:42:24 -0300 Subject: [PATCH 115/512] Make middleware store last cursor for streaming subscriptions (to improve reconnection) --- .../internal/common/StreamCursorUtils.go | 128 ++++++++++++++++++ .../internal/common/types.go | 4 + .../internal/hascli/conn/reader/reader.go | 12 ++ .../internal/hascli/conn/writer/writer.go | 9 ++ .../hascli/retransmiter/retransmiter.go | 8 +- 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 bbb-graphql-middleware/internal/common/StreamCursorUtils.go diff --git a/bbb-graphql-middleware/internal/common/StreamCursorUtils.go b/bbb-graphql-middleware/internal/common/StreamCursorUtils.go new file mode 100644 index 0000000000..88353307a8 --- /dev/null +++ b/bbb-graphql-middleware/internal/common/StreamCursorUtils.go @@ -0,0 +1,128 @@ +package common + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +func GetStreamCursorPropsFromQuery(payload map[string]interface{}, query string) (string, string, interface{}) { + streamCursorKey := "" + streamCursorVariable := "" + var streamCursorInitialValue interface{} + + regexPattern := `cursor:\s*\{\s*initial_value\s*:\s*\{\s*([^\:]+):\s*([^}]+)\s*\}\s*\}` + re := regexp.MustCompile(regexPattern) + matches := re.FindStringSubmatch(query) + if matches != nil { + streamCursorKey = matches[1] + if strings.HasPrefix(matches[2], "$") { + //Variable + streamCursorVariable, _ = strings.CutPrefix(matches[2], "$") + variables, ok := payload["variables"].(map[string]interface{}) + if ok { + for varKey, varValue := range variables { + if varKey == streamCursorVariable { + streamCursorInitialValue = varValue + } + } + } + } else { + streamCursorInitialValue = matches[2] + } + } + + return streamCursorKey, streamCursorVariable, streamCursorInitialValue +} + +func GetLastStreamCursorValueFromReceivedMessage(messageAsMap map[string]interface{}, streamCursorKey string) interface{} { + var lastStreamCursorValue interface{} + + if payload, okPayload := messageAsMap["payload"].(map[string]interface{}); okPayload { + if data, okData := payload["data"].(map[string]interface{}); okData { + //Data will have only one prop, `range` because its name is unknown + for _, dataItem := range data { + currentDataProp, okCurrentDataProp := dataItem.([]interface{}) + if okCurrentDataProp && len(currentDataProp) > 0 { + // Get the last item directly (once it will contain the last cursor value) + lastItemOfMessage := currentDataProp[len(currentDataProp)-1] + if lastItemOfMessageAsMap, currDataOk := lastItemOfMessage.(map[string]interface{}); currDataOk { + if lastItemValue, okLastItemValue := lastItemOfMessageAsMap[streamCursorKey]; okLastItemValue { + lastStreamCursorValue = lastItemValue + //fmt.Println("Descobriu ultimo valor: " + lastStreamCursorValue.(string)) + } + } + } + } + } + } + + return lastStreamCursorValue +} + +func ReplaceMessageWithLastCursorValue(subscription GraphQlSubscription) interface{} { + var message = subscription.Message.(map[string]interface{}) + payload, okPayload := message["payload"].(map[string]interface{}) + + if okPayload { + if subscription.StreamCursorVariable != "" { + /**** This stream has its cursor value set through variables ****/ + if variables, okVariables := payload["variables"].(map[string]interface{}); okVariables { + if variables[subscription.StreamCursorVariable] != subscription.StreamCursorCurrValue { + variables[subscription.StreamCursorVariable] = subscription.StreamCursorCurrValue + payload["variables"] = variables + message["payload"] = payload + } + } + } else { + /**** This stream has its cursor value set through inline value (not variables) ****/ + query, okQuery := payload["query"].(string) + if okQuery { + pattern := `cursor:\s*\{\s*initial_value\s*:\s*\{\s*([^\:]+:\s*[^}]+)\s*\}\s*\}` + re := regexp.MustCompile(pattern) + + newValue := "" + + replaceInitialValueFunc := func(match string) string { + switch v := subscription.StreamCursorCurrValue.(type) { + case string: + newValue = v + + //Append quotes if it is missing + if !strings.HasPrefix(v, "\"") { + newValue = "\"" + newValue + } + if !strings.HasSuffix(v, "\"") { + newValue = newValue + "\"" + } + case int: + newValue = strconv.Itoa(v) + case float32: + myFloat64 := float64(v) + newValue = strconv.FormatFloat(myFloat64, 'f', -1, 32) + case float64: + newValue = strconv.FormatFloat(v, 'f', -1, 64) + default: + newValue = "" + } + + if newValue != "" { + replacement := subscription.StreamCursorKey + ": " + newValue + return fmt.Sprintf("cursor: {initial_value: {%s}}", replacement) + } else { + return match + } + } + + newQuery := re.ReplaceAllStringFunc(query, replaceInitialValueFunc) + if query != newQuery { + payload["query"] = newQuery + message["payload"] = payload + } + } + } + } + + return message +} diff --git a/bbb-graphql-middleware/internal/common/types.go b/bbb-graphql-middleware/internal/common/types.go index 666ccc8932..93ee15affe 100644 --- a/bbb-graphql-middleware/internal/common/types.go +++ b/bbb-graphql-middleware/internal/common/types.go @@ -21,6 +21,10 @@ type GraphQlSubscription struct { Id string Message interface{} Type QueryType + OperationName string + StreamCursorKey string + StreamCursorVariable string + StreamCursorCurrValue interface{} JsonPatchSupported bool // indicate if client support Json Patch for this subscription LastSeenOnHasuraConnetion string // id of the hasura connection that this query was active } diff --git a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go index f7daf1691a..0c4c213c83 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go +++ b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go @@ -66,6 +66,18 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan msgpatch.PatchMessage(&messageAsMap, hc.Browserconn) } + //Set last cursor value for stream + if subscription.Type == common.Streaming { + lastCursor := common.GetLastStreamCursorValueFromReceivedMessage(messageAsMap, subscription.StreamCursorKey) + if lastCursor != nil && subscription.StreamCursorCurrValue != lastCursor { + subscription.StreamCursorCurrValue = lastCursor + + hc.Browserconn.ActiveSubscriptionsMutex.Lock() + hc.Browserconn.ActiveSubscriptions[queryId] = subscription + hc.Browserconn.ActiveSubscriptionsMutex.Unlock() + } + + } } // Write the message to browser diff --git a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go index 8b34d03609..3f10f51e60 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go +++ b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go @@ -41,7 +41,11 @@ RangeLoop: //Identify type based on query string messageType := common.Query + streamCursorKey := "" + streamCursorVariable := "" + var streamCursorInitialValue interface{} payload := fromBrowserMessageAsMap["payload"].(map[string]interface{}) + query, ok := payload["query"].(string) if ok { if strings.HasPrefix(query, "subscription") { @@ -49,6 +53,7 @@ RangeLoop: if strings.Contains(query, "_stream(") && strings.Contains(query, "cursor: {") { messageType = common.Streaming + streamCursorKey, streamCursorVariable, streamCursorInitialValue = common.GetStreamCursorPropsFromQuery(payload, query) } if strings.Contains(query, "_aggregate") && strings.Contains(query, "aggregate {") { @@ -73,6 +78,10 @@ RangeLoop: browserConnection.ActiveSubscriptions[queryId] = common.GraphQlSubscription{ Id: queryId, Message: fromBrowserMessage, + OperationName: operationName, + StreamCursorKey: streamCursorKey, + StreamCursorVariable: streamCursorVariable, + StreamCursorCurrValue: streamCursorInitialValue, LastSeenOnHasuraConnetion: hc.Id, JsonPatchSupported: jsonPatchSupported, Type: messageType, diff --git a/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go b/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go index 5cb1bb7cd9..30afeb2c77 100644 --- a/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go +++ b/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go @@ -11,8 +11,14 @@ func RetransmitSubscriptionStartMessages(hc *common.HasuraConnection, fromBrowse hc.Browserconn.ActiveSubscriptionsMutex.RLock() for _, subscription := range hc.Browserconn.ActiveSubscriptions { if subscription.LastSeenOnHasuraConnetion != hc.Id { + log.Tracef("retransmiting subscription start: %v", subscription.Message) - fromBrowserToHasuraChannel.Send(subscription.Message) + + if subscription.Type == common.Streaming && subscription.StreamCursorCurrValue != nil { + fromBrowserToHasuraChannel.Send(common.ReplaceMessageWithLastCursorValue(subscription)) + } else { + fromBrowserToHasuraChannel.Send(subscription.Message) + } } } hc.Browserconn.ActiveSubscriptionsMutex.RUnlock() From 9de2c484be29fe7b709f798bb173971d07f607fb Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Sat, 20 Jan 2024 13:56:46 -0300 Subject: [PATCH 116/512] Code cleansing --- .../internal/common/StreamCursorUtils.go | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/bbb-graphql-middleware/internal/common/StreamCursorUtils.go b/bbb-graphql-middleware/internal/common/StreamCursorUtils.go index 88353307a8..76f9341503 100644 --- a/bbb-graphql-middleware/internal/common/StreamCursorUtils.go +++ b/bbb-graphql-middleware/internal/common/StreamCursorUtils.go @@ -12,20 +12,15 @@ func GetStreamCursorPropsFromQuery(payload map[string]interface{}, query string) streamCursorVariable := "" var streamCursorInitialValue interface{} - regexPattern := `cursor:\s*\{\s*initial_value\s*:\s*\{\s*([^\:]+):\s*([^}]+)\s*\}\s*\}` - re := regexp.MustCompile(regexPattern) - matches := re.FindStringSubmatch(query) + cursorInitialValueRePattern := regexp.MustCompile(`cursor:\s*\{\s*initial_value\s*:\s*\{\s*([^:]+):\s*([^}]+)\s*}\s*}`) + matches := cursorInitialValueRePattern.FindStringSubmatch(query) if matches != nil { streamCursorKey = matches[1] if strings.HasPrefix(matches[2], "$") { - //Variable streamCursorVariable, _ = strings.CutPrefix(matches[2], "$") - variables, ok := payload["variables"].(map[string]interface{}) - if ok { - for varKey, varValue := range variables { - if varKey == streamCursorVariable { - streamCursorInitialValue = varValue - } + if variables, okVariables := payload["variables"].(map[string]interface{}); okVariables { + if targetVariableValue, okTargetVariableValue := variables[streamCursorVariable]; okTargetVariableValue { + streamCursorInitialValue = targetVariableValue } } } else { @@ -50,7 +45,6 @@ func GetLastStreamCursorValueFromReceivedMessage(messageAsMap map[string]interfa if lastItemOfMessageAsMap, currDataOk := lastItemOfMessage.(map[string]interface{}); currDataOk { if lastItemValue, okLastItemValue := lastItemOfMessageAsMap[streamCursorKey]; okLastItemValue { lastStreamCursorValue = lastItemValue - //fmt.Println("Descobriu ultimo valor: " + lastStreamCursorValue.(string)) } } } @@ -79,9 +73,7 @@ func ReplaceMessageWithLastCursorValue(subscription GraphQlSubscription) interfa /**** This stream has its cursor value set through inline value (not variables) ****/ query, okQuery := payload["query"].(string) if okQuery { - pattern := `cursor:\s*\{\s*initial_value\s*:\s*\{\s*([^\:]+:\s*[^}]+)\s*\}\s*\}` - re := regexp.MustCompile(pattern) - + cursorInitialValueRePattern := regexp.MustCompile(`cursor:\s*\{\s*initial_value\s*:\s*\{\s*([^:]+:\s*[^}]+)\s*}\s*}`) newValue := "" replaceInitialValueFunc := func(match string) string { @@ -89,7 +81,7 @@ func ReplaceMessageWithLastCursorValue(subscription GraphQlSubscription) interfa case string: newValue = v - //Append quotes if it is missing + //Append quotes if it is missing, it will be necessary when appending to the query if !strings.HasPrefix(v, "\"") { newValue = "\"" + newValue } @@ -115,7 +107,7 @@ func ReplaceMessageWithLastCursorValue(subscription GraphQlSubscription) interfa } } - newQuery := re.ReplaceAllStringFunc(query, replaceInitialValueFunc) + newQuery := cursorInitialValueRePattern.ReplaceAllStringFunc(query, replaceInitialValueFunc) if query != newQuery { payload["query"] = newQuery message["payload"] = payload From f01c55680e375be39eaaeb54f87cb28d0fa52f55 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Mon, 22 Jan 2024 17:15:41 +0000 Subject: [PATCH 117/512] add checks for prevShape existence and remoteShape id in shape sync --- .../imports/ui/components/whiteboard/component.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 2113565227..e8d2e23e90 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -222,8 +222,9 @@ export default Whiteboard = React.memo(function Whiteboard(props) { }); Object.values(prevShapesRef.current).forEach((remoteShape) => { + if (!remoteShape.id) return; const localShape = localLookup.get(remoteShape.id); - + const prevShape = prevShapesRef.current[remoteShape.id]; // Create a deep clone of remoteShape and remove the isModerator property const comparisonRemoteShape = deepCloneUsingShallow(remoteShape); delete comparisonRemoteShape.isModerator; @@ -234,7 +235,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) { // If the shape does not exist in local, add it to toAdd toAdd.push(remoteShape); } - } else if (!isEqual(localShape, comparisonRemoteShape)) { + } else if (!isEqual(localShape, comparisonRemoteShape) && prevShape) { // Capture the differences const diff = { id: remoteShape.id, @@ -242,7 +243,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) { typeName: remoteShape.typeName, }; - if (!selectedShapeIds.includes(diff?.id) && prevShapesRef.current[`${diff?.id}`].meta?.updatedBy !== currentUser?.userId) { + if (!selectedShapeIds.includes(remoteShape.id) && prevShape?.meta?.updatedBy !== currentUser?.userId) { // Compare each property Object.keys(remoteShape).forEach((key) => { if ( From 22b74ed18c728e39b2df459a86036de36d454209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Mon, 22 Jan 2024 14:47:46 -0300 Subject: [PATCH 118/512] migrate setPresentationDownloadable action --- .../api/presentations/server/methods.js | 2 -- .../methods/setPresentationDownloadable.js | 31 ------------------- .../presentation-uploader/container.jsx | 13 ++++++-- .../presentation-uploader/service.js | 5 --- 4 files changed, 11 insertions(+), 40 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods.js b/bigbluebutton-html5/imports/api/presentations/server/methods.js index ab965e9787..f772ab0b5b 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods.js @@ -1,10 +1,8 @@ import { Meteor } from 'meteor/meteor'; import removePresentation from './methods/removePresentation'; import setPresentation from './methods/setPresentation'; -import setPresentationDownloadable from './methods/setPresentationDownloadable'; Meteor.methods({ removePresentation, setPresentation, - setPresentationDownloadable, }); diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js deleted file mode 100644 index 1ad147ca9c..0000000000 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentationDownloadable.js +++ /dev/null @@ -1,31 +0,0 @@ -import RedisPubSub from '/imports/startup/server/redis'; -import { check } from 'meteor/check'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import Logger from '/imports/startup/server/logger'; - -export default function setPresentationDownloadable(presentationId, downloadable, fileStateType) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'SetPresentationDownloadablePubMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(downloadable, Match.Maybe(Boolean)); - check(presentationId, String); - check(fileStateType, Match.Maybe(String)); - - const payload = { - presentationId, - podId: 'DEFAULT_PRESENTATION_POD', - downloadable, - fileStateType, - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method setPresentationDownloadable ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx index d2ca8dbcd6..1364e528aa 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx @@ -43,6 +43,16 @@ const PresentationUploaderContainer = (props) => { }); }; + const dispatchChangePresentationDownloadable = (presentationId, downloadable, fileStateType) => { + presentationSetDownloadable({ + variables: { + presentationId, + downloadable, + fileStateType, + }, + }); + }; + return userIsPresenter && ( { presentations={presentations} currentPresentation={currentPresentation} exportPresentation={exportPresentation} + dispatchChangePresentationDownloadable={dispatchChangePresentationDownloadable} {...props} /> @@ -60,7 +71,6 @@ export default withTracker(() => { const { dispatchDisableDownloadable, dispatchEnableDownloadable, - dispatchChangePresentationDownloadable, } = Service; const isOpen = isPresentationEnabled() && (Session.get('showUploadPresentationView') || false); @@ -78,7 +88,6 @@ export default withTracker(() => { renderPresentationItemStatus: PresUploaderToast.renderPresentationItemStatus, dispatchDisableDownloadable, dispatchEnableDownloadable, - dispatchChangePresentationDownloadable, isOpen, selectedToBeNextCurrent: Session.get('selectedToBeNextCurrent') || null, externalUploadData: Service.getExternalUploadData(), diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js index ac1188285a..2719e2c52c 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js @@ -41,10 +41,6 @@ const futch = (url, opts = {}, onProgress) => new Promise((res, rej) => { xhr.send(opts.body); }); -const dispatchChangePresentationDownloadable = (presentation, newState, fileStateType) => { - makeCall('setPresentationDownloadable', presentation.presentationId, newState, fileStateType); -}; - const requestPresentationUploadToken = ( temporaryPresentationId, meetingId, @@ -352,7 +348,6 @@ function handleFiledrop(files, files2, that, intl, intlMessages) { export default { handleSavePresentation, persistPresentationChanges, - dispatchChangePresentationDownloadable, setPresentation, requestPresentationUploadToken, getExternalUploadData, From 1757c0b2f1ea11f20fc77bd4ac894d4e7e543d32 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Mon, 22 Jan 2024 15:51:33 -0300 Subject: [PATCH 119/512] Add a flag to inform if current user responded to the Poll --- bbb-graphql-server/bbb_schema.sql | 7 +++++++ .../BigBlueButton/tables/public_v_poll.yaml | 10 ++++++++++ .../tables/public_v_poll_user_current.yaml | 17 +++++++++++++++++ .../databases/BigBlueButton/tables/tables.yaml | 1 + 4 files changed, 35 insertions(+) create mode 100644 bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_poll_user_current.yaml diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index 0450e87f8f..9e59dc9520 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -1310,6 +1310,13 @@ FROM poll_option o JOIN poll using("pollId") WHERE poll."type" != 'R-'; +create view "v_poll_user_current" as +select "user"."userId", "poll"."pollId", case when count(pr.*) > 0 then true else false end as responded +from "user" +join "poll" on "poll"."meetingId" = "user"."meetingId" +left join "poll_response" pr on pr."userId" = "user"."userId" and pr."pollId" = "poll"."pollId" +group by "user"."userId", "poll"."pollId"; + -------------------------------- ----External video diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_poll.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_poll.yaml index e6c689b5ba..15d9468584 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_poll.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_poll.yaml @@ -6,6 +6,16 @@ configuration: custom_column_names: {} custom_name: poll custom_root_fields: {} +object_relationships: + - name: userCurrent + using: + manual_configuration: + column_mapping: + pollId: pollId + insertion_order: null + remote_table: + name: v_poll_user_current + schema: public array_relationships: - name: options using: diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_poll_user_current.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_poll_user_current.yaml new file mode 100644 index 0000000000..9be764473a --- /dev/null +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_poll_user_current.yaml @@ -0,0 +1,17 @@ +table: + name: v_poll_user_current + schema: public +configuration: + column_config: {} + custom_column_names: {} + custom_name: pollUserCurrent + custom_root_fields: {} +select_permissions: + - role: bbb_client + permission: + columns: + - responded + filter: + userId: + _eq: X-Hasura-UserId + comment: "" diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml index 27edd90911..6a0321df91 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/tables.yaml @@ -26,6 +26,7 @@ - "!include public_v_poll_option.yaml" - "!include public_v_poll_response.yaml" - "!include public_v_poll_user.yaml" +- "!include public_v_poll_user_current.yaml" - "!include public_v_pres_annotation_curr.yaml" - "!include public_v_pres_annotation_history_curr.yaml" - "!include public_v_pres_page.yaml" From d69bbe95287d09ec359b23e53b5d30e78de239a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Thu, 11 Jan 2024 13:51:51 -0300 Subject: [PATCH 120/512] Refactor: migrate poll answer gathering --- .../ui/components/polling/container.jsx | 5 +- .../polling/polling-graphql/component.tsx | 401 ++++++++++++++++++ .../polling/polling-graphql/queries.ts | 52 +++ .../polling/polling-graphql/service.ts | 5 + .../polling/polling-graphql/styles.ts | 244 +++++++++++ 5 files changed, 706 insertions(+), 1 deletion(-) create mode 100644 bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx create mode 100644 bigbluebutton-html5/imports/ui/components/polling/polling-graphql/queries.ts create mode 100644 bigbluebutton-html5/imports/ui/components/polling/polling-graphql/service.ts create mode 100644 bigbluebutton-html5/imports/ui/components/polling/polling-graphql/styles.ts diff --git a/bigbluebutton-html5/imports/ui/components/polling/container.jsx b/bigbluebutton-html5/imports/ui/components/polling/container.jsx index 43fb81a4f6..fbec196ade 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/polling/container.jsx @@ -8,6 +8,7 @@ import PollingComponent from './component'; import { isPollingEnabled } from '/imports/ui/services/features'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; import { POLL_SUBMIT_TYPED_VOTE, POLL_SUBMIT_VOTE } from '/imports/ui/components/poll/mutations'; +import PollingGraphqlContainer from './polling-graphql/component'; const propTypes = { pollExists: PropTypes.bool.isRequired, @@ -50,7 +51,7 @@ const PollingContainer = ({ pollExists, ...props }) => { PollingContainer.propTypes = propTypes; -export default withTracker(() => { +withTracker(() => { const { pollExists, poll, } = PollingService.mapPolls(); @@ -70,3 +71,5 @@ export default withTracker(() => { isMeteorConnected: Meteor.status().connected, }); })(PollingContainer); + +export default PollingGraphqlContainer; diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx new file mode 100644 index 0000000000..a26e9bc313 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx @@ -0,0 +1,401 @@ +import React, { + ElementRef, useEffect, useMemo, useRef, useState, +} from 'react'; +import { useMutation, useSubscription } from '@apollo/client'; +import { defineMessages, useIntl } from 'react-intl'; +import { Meteor } from 'meteor/meteor'; +import AudioService from '/imports/ui/components/audio/service'; +import Checkbox from '/imports/ui/components/common/checkbox/component'; +import PollService from '/imports/ui/components/poll/service'; +import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; +import { isPollingEnabled } from '/imports/ui/services/features'; +import { + POLL_SUBMIT_TYPED_VOTE, + POLL_SUBMIT_VOTE, +} from '/imports/ui/components/poll/mutations'; +import { + hasPendingPoll, + HasPendingPollResponse, +} from './queries'; +import { shouldStackOptions } from './service'; +import Styled from './styles'; + +const MAX_INPUT_CHARS = Meteor.settings.public.poll.maxTypedAnswerLength; + +const intlMessages = defineMessages({ + pollingTitleLabel: { + id: 'app.polling.pollingTitle', + }, + pollAnswerLabel: { + id: 'app.polling.pollAnswerLabel', + }, + pollAnswerDesc: { + id: 'app.polling.pollAnswerDesc', + }, + pollQuestionTitle: { + id: 'app.polling.pollQuestionTitle', + }, + responseIsSecret: { + id: 'app.polling.responseSecret', + }, + responseNotSecret: { + id: 'app.polling.responseNotSecret', + }, + submitLabel: { + id: 'app.polling.submitLabel', + }, + submitAriaLabel: { + id: 'app.polling.submitAriaLabel', + }, + responsePlaceholder: { + id: 'app.polling.responsePlaceholder', + }, +}); + +const validateInput = (i: string) => { + let input = i; + if (/^\s/.test(input)) input = ''; + return input; +}; + +interface PollingGraphqlContainerProps {} + +interface PollingGraphqlProps { + handleTypedVote: (pollId: string, answer: string) => void; + handleVote: (pollId: string, answerIds: Array) => void; + pollAnswerIds: Record; + pollTypes: Record; + isDefaultPoll: (pollType: string) => boolean; + poll: { + pollId: string; + multipleResponses: boolean; + type: string; + stackOptions: boolean; + questionText: string; + secret: boolean; + options: Array<{ + optionDesc: string; + optionId: number; + pollId: string; + }>; + }; +} + +const PollingGraphql: React.FC = (props) => { + const { + handleTypedVote, + handleVote, + poll, + pollAnswerIds, + pollTypes, + isDefaultPoll, + } = props; + + const [typedAns, setTypedAns] = useState(''); + const [checkedAnswers, setCheckedAnswers] = useState>([]); + const intl = useIntl(); + const responseInput = useRef>(null); + const pollingContainer = useRef>(null); + + useEffect(() => { + play(); + if (pollingContainer.current) { + pollingContainer.current.focus(); + } + }, []); + + const play = () => { + AudioService.playAlertSound( + `${ + Meteor.settings.public.app.cdn + + Meteor.settings.public.app.basename + + Meteor.settings.public.app.instanceId + }/resources/sounds/Poll.mp3`, + ); + }; + + const handleUpdateResponseInput = (e: React.ChangeEvent) => { + if (responseInput.current) { + responseInput.current.value = validateInput(e.target.value); + setTypedAns(responseInput.current.value); + } + }; + + const handleSubmit = (pollId: string) => { + handleVote(pollId, checkedAnswers); + }; + + const handleCheckboxChange = (answerId: number) => { + if (checkedAnswers.includes(answerId)) { + checkedAnswers.splice(checkedAnswers.indexOf(answerId), 1); + } else { + checkedAnswers.push(answerId); + } + checkedAnswers.sort(); + setCheckedAnswers([...checkedAnswers]); + }; + + const handleMessageKeyDown = (e: React.KeyboardEvent) => { + if (e.keyCode === 13 && typedAns.length > 0) { + handleTypedVote(poll.pollId, typedAns); + } + }; + + const renderButtonAnswers = () => { + const { + stackOptions, + options, + questionText, + type, + } = poll; + const defaultPoll = isDefaultPoll(type); + + return ( +
+ {poll.type !== pollTypes.Response && ( + + {questionText.length === 0 && ( + + {intl.formatMessage(intlMessages.pollingTitleLabel)} + + )} + + {options.map((option) => { + const formattedMessageIndex = option.optionDesc.toLowerCase(); + let label = option.optionDesc; + if ( + (defaultPoll || type.includes('CUSTOM')) + && pollAnswerIds[formattedMessageIndex] + ) { + label = intl.formatMessage( + pollAnswerIds[formattedMessageIndex], + ); + } + + return ( + + handleVote(poll.pollId, [option.optionId])} + aria-labelledby={`pollAnswerLabel${option.optionDesc}`} + aria-describedby={`pollAnswerDesc${option.optionDesc}`} + data-test="pollAnswerOption" + /> + + {intl.formatMessage(intlMessages.pollAnswerLabel, { + 0: label, + })} + + + {intl.formatMessage(intlMessages.pollAnswerDesc, { + 0: label, + })} + + + ); + })} + + + )} + {poll.type === pollTypes.Response && ( + + { + handleUpdateResponseInput(e); + }} + onKeyDown={(e) => { + handleMessageKeyDown(e); + }} + type="text" + placeholder={intl.formatMessage(intlMessages.responsePlaceholder)} + maxLength={MAX_INPUT_CHARS} + ref={responseInput} + onPaste={(e) => { + e.stopPropagation(); + }} + onCut={(e) => { + e.stopPropagation(); + }} + onCopy={(e) => { + e.stopPropagation(); + }} + /> + { + handleTypedVote(poll.pollId, typedAns); + }} + /> + + )} + + {intl.formatMessage( + poll.secret + ? intlMessages.responseIsSecret + : intlMessages.responseNotSecret, + )} + +
+ ); + }; + + const renderCheckboxAnswers = () => { + return ( +
+ {poll.questionText.length === 0 && ( + + {intl.formatMessage(intlMessages.pollingTitleLabel)} + + )} + + {poll.options.map((option) => { + const formattedMessageIndex = option.optionDesc.toLowerCase(); + let label = option.optionDesc; + if (pollAnswerIds[formattedMessageIndex]) { + label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]); + } + + return ( + +
+ + + + {intl.formatMessage(intlMessages.pollAnswerDesc, { + 0: label, + })} + + + + ); + })} + +
+ handleSubmit(poll.pollId)} + data-test="submitAnswersMultiple" + /> +
+ + ); + }; + + return ( + + + {poll.questionText.length > 0 && ( + + + {intl.formatMessage(intlMessages.pollQuestionTitle)} + + + {poll.questionText} + + + )} + {poll.multipleResponses + ? renderCheckboxAnswers() + : renderButtonAnswers()} + + + ); +}; + +const PollingGraphqlContainer: React.FC = () => { + const { data: currentUserData } = useCurrentUser((u) => ({ + userId: u.userId, + presenter: u.presenter, + })); + const { data: hasPendingPollData, error, loading } = useSubscription( + hasPendingPoll, + { + variables: { userId: currentUserData?.userId }, + }, + ); + const [pollSubmitUserTypedVote] = useMutation(POLL_SUBMIT_TYPED_VOTE); + const [pollSubmitUserVote] = useMutation(POLL_SUBMIT_VOTE); + + const meetingData = hasPendingPollData && hasPendingPollData.meeting[0]; + const pollData = meetingData && meetingData.polls[0]; + const userData = pollData && pollData.users[0]; + const pollExists = !!userData; + const showPolling = pollExists && !currentUserData?.presenter && isPollingEnabled(); + const stackOptions = useMemo( + () => !!pollData && shouldStackOptions(pollData.options.map((o) => o.optionDesc)), + [pollData], + ); + + const handleTypedVote = (pollId: string, answer: string) => { + pollSubmitUserTypedVote({ + variables: { + pollId, + answer, + }, + }); + }; + + const handleVote = (pollId: string, answerIds: Array) => { + pollSubmitUserVote({ + variables: { + pollId, + answerIds, + }, + }); + }; + + if (!showPolling || error || loading) return null; + + return ( + + ); +}; + +export default PollingGraphqlContainer; diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/queries.ts b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/queries.ts new file mode 100644 index 0000000000..6dd88f4bbd --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/queries.ts @@ -0,0 +1,52 @@ +import { gql } from '@apollo/client'; + +export interface HasPendingPollResponse { + meeting: Array<{ + polls: Array<{ + users: Array<{ + userId: string; + responded: boolean; + }>; + options: Array<{ + optionDesc: string; + optionId: number; + pollId: string; + }>; + multipleResponses: boolean; + pollId: string; + questionText: string; + secret: boolean; + type: string; + }>; + }>; +} + +export const hasPendingPoll = gql` + subscription hasPendingPoll($userId: String!) { + meeting { + polls( + where: { + ended: { _eq: false } + users: { responded: { _eq: false }, userId: { _eq: $userId } } + } + ) { + users { + responded + userId + } + options { + optionDesc + optionId + pollId + } + multipleResponses + pollId + questionText + secret + type + } + } + } +`; + +export default { hasPendingPoll }; diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/service.ts b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/service.ts new file mode 100644 index 0000000000..848179b395 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/service.ts @@ -0,0 +1,5 @@ +const MAX_CHAR_LENGTH = 5; + +export const shouldStackOptions = (keys: Array) => keys.some((k) => k.length > MAX_CHAR_LENGTH); + +export default { shouldStackOptions }; diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/styles.ts b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/styles.ts new file mode 100644 index 0000000000..52fbffb7cb --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/styles.ts @@ -0,0 +1,244 @@ +import styled from 'styled-components'; +import { + mdPaddingY, + smPaddingY, + jumboPaddingY, + smPaddingX, + borderRadius, + pollWidth, + pollSmMargin, + overlayIndex, + overlayOpacity, + pollIndex, + lgPaddingY, + pollBottomOffset, + jumboPaddingX, + pollColAmount, + borderSize, +} from '/imports/ui/stylesheets/styled-components/general'; +import { + fontSizeSmall, + fontSizeBase, + fontSizeLarge, +} from '/imports/ui/stylesheets/styled-components/typography'; +import { + colorText, + colorBlueLight, + colorGrayLighter, + colorOffWhite, + colorGrayDark, + colorWhite, + colorPrimary, +} from '/imports/ui/stylesheets/styled-components/palette'; +import { hasPhoneDimentions } from '/imports/ui/stylesheets/styled-components/breakpoints'; +import Button from '/imports/ui/components/common/button/component'; + +const PollingTitle = styled.div` + white-space: nowrap; + padding-bottom: ${mdPaddingY}; + padding-top: ${mdPaddingY}; + font-size: ${fontSizeSmall}; +`; + +const PollButtonWrapper = styled.div` + text-align: center; + padding: ${smPaddingY}; + width: 100%; +`; + +// @ts-ignore Until everything in Typescript +const PollingButton = styled(Button)` + width: 100%; + max-width: 9em; + + @media ${hasPhoneDimentions} { + max-width: none; + } + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`; + +const Hidden = styled.div` + display: none; +`; + +const TypedResponseWrapper = styled.div` + margin: ${jumboPaddingY} 0.5rem 0.5rem 0.5rem; + display: flex; + flex-flow: column; +`; + +const TypedResponseInput = styled.input` + &:focus { + outline: none; + border-radius: ${borderSize}; + box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, + inset 0 0 0 1px ${colorPrimary}; + } + + color: ${colorText}; + -webkit-appearance: none; + padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25); + border-radius: ${borderRadius}; + font-size: ${fontSizeBase}; + border: 1px solid ${colorGrayLighter}; + box-shadow: 0 0 0 1px ${colorGrayLighter}; + margin-bottom: 1rem; +`; + +// @ts-ignore Until everything in Typescript +const SubmitVoteButton = styled(Button)` + font-size: ${fontSizeBase}; +`; + +const PollingSecret = styled.div` + font-size: ${fontSizeSmall}; + max-width: ${pollWidth}; +`; + +const MultipleResponseAnswersTable = styled.table` + margin-left: auto; + margin-right: auto; +`; + +const PollingCheckbox = styled.div` + display: inline-block; + margin-right: ${pollSmMargin}; +`; + +const CheckboxContainer = styled.tr` + margin-bottom: ${pollSmMargin}; +`; + +const MultipleResponseAnswersTableAnswerText = styled.td` + text-align: left; +`; + +const Overlay = styled.div` + position: absolute; + height: 100vh; + width: 100vw; + z-index: ${overlayIndex}; + pointer-events: none; + + @media ${hasPhoneDimentions} { + pointer-events: auto; + background-color: rgba(0, 0, 0, ${overlayOpacity}); + } +`; + +const QHeader = styled.span` + text-align: left; + position: relative; + left: ${smPaddingY}; +`; + +const QTitle = styled.div` + font-size: ${fontSizeSmall}; +`; + +const QText = styled.div` + color: ${colorText}; + word-break: break-word; + white-space: pre-wrap; + font-size: ${fontSizeLarge}; + max-width: ${pollWidth}; + padding-right: ${smPaddingX}; +`; + +const PollingContainer = styled.aside<{ autoWidth: boolean }>` + pointer-events: auto; + min-width: ${pollWidth}; + position: absolute; + + z-index: ${pollIndex}; + border: 1px solid ${colorOffWhite}; + border-radius: ${borderRadius}; + box-shadow: ${colorGrayDark} 0px 0px ${lgPaddingY}; + align-items: center; + text-align: center; + font-weight: 600; + padding: ${mdPaddingY}; + background-color: ${colorWhite}; + bottom: ${pollBottomOffset}; + right: ${jumboPaddingX}; + + &:focus { + border: 1px solid ${colorPrimary}; + } + + [dir="rtl"] & { + left: ${jumboPaddingX}; + right: auto; + } + + @media ${hasPhoneDimentions} { + bottom: auto; + right: auto; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + max-height: 95%; + overflow-y: auto; + + [dir="rtl"] & { + left: 50%; + } + } + + ${({ autoWidth }) => autoWidth + && ` + width: auto; + `} +`; + +const PollingAnswers = styled.div<{ removeColumns: boolean; stacked: boolean }>` + display: grid; + grid-template-columns: repeat(${pollColAmount}, 1fr); + + @media ${hasPhoneDimentions} { + grid-template-columns: repeat(1, 1fr); + + & div button { + grid-column: 1; + } + } + + z-index: 1; + + ${({ removeColumns }) => removeColumns + && ` + grid-template-columns: auto; + `} + + ${({ stacked }) => stacked + && ` + grid-template-columns: repeat(1, 1fr); + + & div button { + max-width: none !important; + } + `} +`; + +export default { + PollingTitle, + PollButtonWrapper, + PollingButton, + Hidden, + TypedResponseWrapper, + TypedResponseInput, + SubmitVoteButton, + PollingSecret, + MultipleResponseAnswersTable, + PollingCheckbox, + CheckboxContainer, + MultipleResponseAnswersTableAnswerText, + Overlay, + QHeader, + QTitle, + QText, + PollingContainer, + PollingAnswers, +}; From b7fac7bfc048d58be86dbbacbbf82b59de494e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Mon, 22 Jan 2024 16:59:09 -0300 Subject: [PATCH 121/512] Fix secret poll getting stuck --- .../ui/components/polling/polling-graphql/component.tsx | 1 + .../imports/ui/components/polling/polling-graphql/queries.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx index a26e9bc313..c0a0d421b3 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx @@ -350,6 +350,7 @@ const PollingGraphqlContainer: React.FC = () => { variables: { userId: currentUserData?.userId }, }, ); + console.log(hasPendingPollData, error); const [pollSubmitUserTypedVote] = useMutation(POLL_SUBMIT_TYPED_VOTE); const [pollSubmitUserVote] = useMutation(POLL_SUBMIT_VOTE); diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/queries.ts b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/queries.ts index 6dd88f4bbd..9ad89850bb 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/queries.ts +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/queries.ts @@ -27,7 +27,8 @@ export const hasPendingPoll = gql` polls( where: { ended: { _eq: false } - users: { responded: { _eq: false }, userId: { _eq: $userId } } + users: { responded: { _eq: false }, userId: { _eq: $userId } }, + userCurrent: { responded: { _eq: false } } } ) { users { From 5c668aafa1bd0e153f9f2d61cde932c2ea9537bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Mon, 22 Jan 2024 17:15:37 -0300 Subject: [PATCH 122/512] Remove log --- .../imports/ui/components/polling/polling-graphql/component.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx index c0a0d421b3..a26e9bc313 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx @@ -350,7 +350,6 @@ const PollingGraphqlContainer: React.FC = () => { variables: { userId: currentUserData?.userId }, }, ); - console.log(hasPendingPollData, error); const [pollSubmitUserTypedVote] = useMutation(POLL_SUBMIT_TYPED_VOTE); const [pollSubmitUserVote] = useMutation(POLL_SUBMIT_VOTE); From 082996a15ece01d575e2598a36003fccfc8919be Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 22 Jan 2024 15:58:28 -0500 Subject: [PATCH 123/512] chore: Bump release to 3.0.0-alpha.2 --- bigbluebutton-config/bigbluebutton-release | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-config/bigbluebutton-release b/bigbluebutton-config/bigbluebutton-release index a0b5197f82..89c096ec73 100644 --- a/bigbluebutton-config/bigbluebutton-release +++ b/bigbluebutton-config/bigbluebutton-release @@ -1 +1 @@ -BIGBLUEBUTTON_RELEASE=3.0.0-alpha.1 +BIGBLUEBUTTON_RELEASE=3.0.0-alpha.2 From 380287a4c862bd9a1c9e0bf3f3131d330f420dcd Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Wed, 8 Nov 2023 00:20:59 +0000 Subject: [PATCH 124/512] add transitions to cursors for smoothing --- .../ui/components/whiteboard/cursors/cursor/component.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/cursor/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/cursor/component.jsx index a4711d4d45..7db9283d3d 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/cursor/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/cursors/cursor/component.jsx @@ -25,19 +25,22 @@ const Cursor = (props) => { _y = (y + tldrawCamera?.point[1]) * tldrawCamera?.zoom; } + const transitionStyle = owner ? { transition: 'left 0.3s ease-out, top 0.3s ease-out' } : {}; + return ( <>
@@ -57,6 +60,7 @@ const Cursor = (props) => { color: '#FFF', backgroundColor: color, border: `1px solid ${color}`, + ...transitionStyle, }} data-test="whiteboardCursorIndicator" > From 957dd56ade22291cf968a4ddb3ee7eaced26651d Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 22 Jan 2024 16:33:58 -0500 Subject: [PATCH 125/512] feat: join param for default animations setting value --- .../api/users-settings/server/methods/addUserSettings.js | 1 + bigbluebutton-html5/imports/startup/client/base.jsx | 8 ++++++++ docs/docs/administration/customize.md | 1 + 3 files changed, 10 insertions(+) diff --git a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js index 68e9682bb7..5d9bad360f 100644 --- a/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js +++ b/bigbluebutton-html5/imports/api/users-settings/server/methods/addUserSettings.js @@ -36,6 +36,7 @@ const currentParameters = [ 'bbb_skip_check_audio_on_first_join', 'bbb_fullaudio_bridge', 'bbb_transparent_listen_only', + 'bbb_show_animations_default', // BRANDING 'bbb_display_branding_area', // SHORTCUTS diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index 2ce86e3d57..bd52ad9d9b 100755 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -177,6 +177,14 @@ class Base extends Component { if (Session.equals('layoutReady', true) && (sidebarContentPanel === PANELS.NONE || Session.equals('subscriptionsReady', true))) { if (!checkedUserSettings) { + const showAnimationsDefault = getFromUserSettings( + 'bbb_show_animations_default', + Meteor.settings.public.app.defaultSettings.application.animations + ); + + Settings.application.animations = showAnimationsDefault; + Settings.save(); + if (getFromUserSettings('bbb_show_participants_on_login', Meteor.settings.public.layout.showParticipantsOnLogin) && !deviceInfo.isPhone) { if (isChatEnabled() && getFromUserSettings('bbb_show_public_chat_on_login', !Meteor.settings.public.chat.startClosed)) { layoutContextDispatch({ diff --git a/docs/docs/administration/customize.md b/docs/docs/administration/customize.md index bf98a79667..0cd2fd98dd 100644 --- a/docs/docs/administration/customize.md +++ b/docs/docs/administration/customize.md @@ -1441,6 +1441,7 @@ Useful tools for development: | `userdata-bbb_skip_check_audio_on_first_join=` | (Introduced in BigBlueButton 2.3) If set to `true`, the user will not see the "echo test" when sharing audio for the first time in the session. If the user stops sharing, next time they try to share audio the echo test window will be displayed, allowing for configuration changes to be made prior to sharing audio again | `false` | | `userdata-bbb_override_default_locale=` | (Introduced in BigBlueButton 2.3) If set to `de`, the user's browser preference will be ignored - the client will be shown in 'de' (i.e. German) regardless of the otherwise preferred locale 'en' (or other) | `null` | | `userdata-bbb_hide_presentation_on_join` | (Introduced in BigBlueButton 2.6) If set to `true` it will make the user enter the meeting with presentation minimized, not permanent. | `false` | +| `userdata-bbb_show_animations_default` | (Introduced in BigBlueButton 2.7.4) If set to `false` the default value for the Animations toggle in Settings will be 'off' | `true` | #### Branding parameters From 02f86c8e0aebaf32ea76dac815c3ca1f481cc952 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Mon, 22 Jan 2024 22:43:32 -0300 Subject: [PATCH 126/512] Assure that the query will request the cursor field value in its list of fields --- .../internal/common/StreamCursorUtils.go | 43 +++++++++++++------ .../internal/common/types.go | 8 ++-- .../internal/hascli/conn/reader/reader.go | 2 +- .../internal/hascli/conn/writer/writer.go | 29 +++++++++---- .../hascli/retransmiter/retransmiter.go | 2 +- 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/bbb-graphql-middleware/internal/common/StreamCursorUtils.go b/bbb-graphql-middleware/internal/common/StreamCursorUtils.go index 76f9341503..872a13dc32 100644 --- a/bbb-graphql-middleware/internal/common/StreamCursorUtils.go +++ b/bbb-graphql-middleware/internal/common/StreamCursorUtils.go @@ -8,18 +8,18 @@ import ( ) func GetStreamCursorPropsFromQuery(payload map[string]interface{}, query string) (string, string, interface{}) { - streamCursorKey := "" - streamCursorVariable := "" + streamCursorField := "" + streamCursorVariableName := "" var streamCursorInitialValue interface{} cursorInitialValueRePattern := regexp.MustCompile(`cursor:\s*\{\s*initial_value\s*:\s*\{\s*([^:]+):\s*([^}]+)\s*}\s*}`) matches := cursorInitialValueRePattern.FindStringSubmatch(query) if matches != nil { - streamCursorKey = matches[1] + streamCursorField = matches[1] if strings.HasPrefix(matches[2], "$") { - streamCursorVariable, _ = strings.CutPrefix(matches[2], "$") + streamCursorVariableName, _ = strings.CutPrefix(matches[2], "$") if variables, okVariables := payload["variables"].(map[string]interface{}); okVariables { - if targetVariableValue, okTargetVariableValue := variables[streamCursorVariable]; okTargetVariableValue { + if targetVariableValue, okTargetVariableValue := variables[streamCursorVariableName]; okTargetVariableValue { streamCursorInitialValue = targetVariableValue } } @@ -28,10 +28,10 @@ func GetStreamCursorPropsFromQuery(payload map[string]interface{}, query string) } } - return streamCursorKey, streamCursorVariable, streamCursorInitialValue + return streamCursorField, streamCursorVariableName, streamCursorInitialValue } -func GetLastStreamCursorValueFromReceivedMessage(messageAsMap map[string]interface{}, streamCursorKey string) interface{} { +func GetLastStreamCursorValueFromReceivedMessage(messageAsMap map[string]interface{}, streamCursorField string) interface{} { var lastStreamCursorValue interface{} if payload, okPayload := messageAsMap["payload"].(map[string]interface{}); okPayload { @@ -43,7 +43,7 @@ func GetLastStreamCursorValueFromReceivedMessage(messageAsMap map[string]interfa // Get the last item directly (once it will contain the last cursor value) lastItemOfMessage := currentDataProp[len(currentDataProp)-1] if lastItemOfMessageAsMap, currDataOk := lastItemOfMessage.(map[string]interface{}); currDataOk { - if lastItemValue, okLastItemValue := lastItemOfMessageAsMap[streamCursorKey]; okLastItemValue { + if lastItemValue, okLastItemValue := lastItemOfMessageAsMap[streamCursorField]; okLastItemValue { lastStreamCursorValue = lastItemValue } } @@ -55,16 +55,31 @@ func GetLastStreamCursorValueFromReceivedMessage(messageAsMap map[string]interfa return lastStreamCursorValue } -func ReplaceMessageWithLastCursorValue(subscription GraphQlSubscription) interface{} { - var message = subscription.Message.(map[string]interface{}) +func PatchQueryIncludingCursorField(originalQuery string, cursorField string) string { + if cursorField == "" { + return originalQuery + } + + lastIndex := strings.LastIndex(originalQuery, "{") + if lastIndex == -1 { + return originalQuery + } + + // It will include the cursorField at the beginning of the list of fields + // It's not a problem if the field be duplicated in the list, Hasura just ignore the second occurrence + return originalQuery[:lastIndex+1] + "\n " + cursorField + originalQuery[lastIndex+1:] +} + +func PatchQuerySettingLastCursorValue(subscription GraphQlSubscription) interface{} { + message := subscription.Message payload, okPayload := message["payload"].(map[string]interface{}) if okPayload { - if subscription.StreamCursorVariable != "" { + if subscription.StreamCursorVariableName != "" { /**** This stream has its cursor value set through variables ****/ if variables, okVariables := payload["variables"].(map[string]interface{}); okVariables { - if variables[subscription.StreamCursorVariable] != subscription.StreamCursorCurrValue { - variables[subscription.StreamCursorVariable] = subscription.StreamCursorCurrValue + if variables[subscription.StreamCursorVariableName] != subscription.StreamCursorCurrValue { + variables[subscription.StreamCursorVariableName] = subscription.StreamCursorCurrValue payload["variables"] = variables message["payload"] = payload } @@ -100,7 +115,7 @@ func ReplaceMessageWithLastCursorValue(subscription GraphQlSubscription) interfa } if newValue != "" { - replacement := subscription.StreamCursorKey + ": " + newValue + replacement := subscription.StreamCursorField + ": " + newValue return fmt.Sprintf("cursor: {initial_value: {%s}}", replacement) } else { return match diff --git a/bbb-graphql-middleware/internal/common/types.go b/bbb-graphql-middleware/internal/common/types.go index 93ee15affe..15bc7e5db5 100644 --- a/bbb-graphql-middleware/internal/common/types.go +++ b/bbb-graphql-middleware/internal/common/types.go @@ -19,11 +19,11 @@ const ( type GraphQlSubscription struct { Id string - Message interface{} + Message map[string]interface{} Type QueryType OperationName string - StreamCursorKey string - StreamCursorVariable string + StreamCursorField string + StreamCursorVariableName string StreamCursorCurrValue interface{} JsonPatchSupported bool // indicate if client support Json Patch for this subscription LastSeenOnHasuraConnetion string // id of the hasura connection that this query was active @@ -35,7 +35,7 @@ type BrowserConnection struct { Context context.Context // browser connection context ActiveSubscriptions map[string]GraphQlSubscription // active subscriptions of this connection (start, but no stop) ActiveSubscriptionsMutex sync.RWMutex // mutex to control the map usage - ConnectionInitMessage interface{} // init message received in this connection (to be used on hasura reconnect) + ConnectionInitMessage map[string]interface{} // init message received in this connection (to be used on hasura reconnect) HasuraConnection *HasuraConnection // associated hasura connection Disconnected bool // indicate if the connection is gone } diff --git a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go index 0c4c213c83..c66a299c35 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go +++ b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go @@ -68,7 +68,7 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan //Set last cursor value for stream if subscription.Type == common.Streaming { - lastCursor := common.GetLastStreamCursorValueFromReceivedMessage(messageAsMap, subscription.StreamCursorKey) + lastCursor := common.GetLastStreamCursorValueFromReceivedMessage(messageAsMap, subscription.StreamCursorField) if lastCursor != nil && subscription.StreamCursorCurrValue != lastCursor { subscription.StreamCursorCurrValue = lastCursor diff --git a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go index 3f10f51e60..334bc3c01a 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go +++ b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go @@ -41,8 +41,8 @@ RangeLoop: //Identify type based on query string messageType := common.Query - streamCursorKey := "" - streamCursorVariable := "" + streamCursorField := "" + streamCursorVariableName := "" var streamCursorInitialValue interface{} payload := fromBrowserMessageAsMap["payload"].(map[string]interface{}) @@ -53,7 +53,18 @@ RangeLoop: if strings.Contains(query, "_stream(") && strings.Contains(query, "cursor: {") { messageType = common.Streaming - streamCursorKey, streamCursorVariable, streamCursorInitialValue = common.GetStreamCursorPropsFromQuery(payload, query) + + browserConnection.ActiveSubscriptionsMutex.RLock() + _, queryIdExists := browserConnection.ActiveSubscriptions[queryId] + browserConnection.ActiveSubscriptionsMutex.RUnlock() + if !queryIdExists { + streamCursorField, streamCursorVariableName, streamCursorInitialValue = common.GetStreamCursorPropsFromQuery(payload, query) + + //It's necessary to assure the cursor field will return in the result of the query + //To be able to store the last received cursor value + payload["query"] = common.PatchQueryIncludingCursorField(query, streamCursorField) + fromBrowserMessageAsMap["payload"] = payload + } } if strings.Contains(query, "_aggregate") && strings.Contains(query, "aggregate {") { @@ -77,10 +88,10 @@ RangeLoop: browserConnection.ActiveSubscriptionsMutex.Lock() browserConnection.ActiveSubscriptions[queryId] = common.GraphQlSubscription{ Id: queryId, - Message: fromBrowserMessage, + Message: fromBrowserMessageAsMap, OperationName: operationName, - StreamCursorKey: streamCursorKey, - StreamCursorVariable: streamCursorVariable, + StreamCursorField: streamCursorField, + StreamCursorVariableName: streamCursorVariableName, StreamCursorCurrValue: streamCursorInitialValue, LastSeenOnHasuraConnetion: hc.Id, JsonPatchSupported: jsonPatchSupported, @@ -105,11 +116,11 @@ RangeLoop: } if fromBrowserMessageAsMap["type"] == "connection_init" { - browserConnection.ConnectionInitMessage = fromBrowserMessage + browserConnection.ConnectionInitMessage = fromBrowserMessageAsMap } - log.Tracef("sending to hasura: %v", fromBrowserMessage) - err := wsjson.Write(hc.Context, hc.Websocket, fromBrowserMessage) + log.Tracef("sending to hasura: %v", fromBrowserMessageAsMap) + err := wsjson.Write(hc.Context, hc.Websocket, fromBrowserMessageAsMap) if err != nil { log.Errorf("error on write (we're disconnected from hasura): %v", err) return diff --git a/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go b/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go index 30afeb2c77..530d167e42 100644 --- a/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go +++ b/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go @@ -15,7 +15,7 @@ func RetransmitSubscriptionStartMessages(hc *common.HasuraConnection, fromBrowse log.Tracef("retransmiting subscription start: %v", subscription.Message) if subscription.Type == common.Streaming && subscription.StreamCursorCurrValue != nil { - fromBrowserToHasuraChannel.Send(common.ReplaceMessageWithLastCursorValue(subscription)) + fromBrowserToHasuraChannel.Send(common.PatchQuerySettingLastCursorValue(subscription)) } else { fromBrowserToHasuraChannel.Send(subscription.Message) } From 9083d1a2918cd18fa944b07d5192f674567f3951 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Tue, 23 Jan 2024 09:12:04 -0300 Subject: [PATCH 127/512] Allow to select voice props along with userCamera in Graphql --- .../BigBlueButton/tables/public_v_user_camera.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_camera.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_camera.yaml index 66086faada..732a58f20c 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_camera.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_camera.yaml @@ -16,6 +16,15 @@ object_relationships: remote_table: name: v_user_ref schema: public + - name: voice + using: + manual_configuration: + column_mapping: + userId: userId + insertion_order: null + remote_table: + name: v_user_voice + schema: public select_permissions: - role: bbb_client permission: From ddfce55213eaad2fd14dca4144f2f80543561e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Tue, 23 Jan 2024 09:33:59 -0300 Subject: [PATCH 128/512] remove clearWhiteboard action --- .../imports/api/annotations/server/methods.js | 2 -- .../server/methods/clearWhiteboard.js | 27 ------------------- 2 files changed, 29 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods.js b/bigbluebutton-html5/imports/api/annotations/server/methods.js index 6a42fbf0bb..cafab18911 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods.js @@ -1,11 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import clearWhiteboard from './methods/clearWhiteboard'; import sendAnnotations from './methods/sendAnnotations'; import sendBulkAnnotations from './methods/sendBulkAnnotations'; import deleteAnnotations from './methods/deleteAnnotations'; Meteor.methods({ - clearWhiteboard, sendAnnotations, sendBulkAnnotations, deleteAnnotations, diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js b/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js deleted file mode 100644 index a95d775dba..0000000000 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/clearWhiteboard.js +++ /dev/null @@ -1,27 +0,0 @@ -import RedisPubSub from '/imports/startup/server/redis'; -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import Logger from '/imports/startup/server/logger'; - -export default function clearWhiteboard(whiteboardId) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'ClearWhiteboardPubMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(whiteboardId, String); - - const payload = { - whiteboardId, - }; - - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method clearWhiteboard ${err.stack}`); - } -} From b674d8a9121ef63d7dae1ace5a695d4dd9b1d6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Tue, 23 Jan 2024 09:51:14 -0300 Subject: [PATCH 129/512] migrate setPresentation action --- .../api/presentations/server/methods.js | 2 -- .../server/methods/setPresentation.js | 28 ------------------- .../actions-dropdown/container.jsx | 9 ++++-- .../notes/notes-dropdown/component.jsx | 3 +- .../notes/notes-dropdown/container.jsx | 11 ++++++-- .../notes/notes-dropdown/service.js | 5 ++-- .../ui/components/presentation/mutations.jsx | 9 ++++++ .../presentation-uploader/component.jsx | 3 +- .../presentation-uploader/container.jsx | 8 +++++- .../presentation-uploader/service.js | 15 +++++----- 10 files changed, 45 insertions(+), 48 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods.js b/bigbluebutton-html5/imports/api/presentations/server/methods.js index f772ab0b5b..1134b35779 100644 --- a/bigbluebutton-html5/imports/api/presentations/server/methods.js +++ b/bigbluebutton-html5/imports/api/presentations/server/methods.js @@ -1,8 +1,6 @@ import { Meteor } from 'meteor/meteor'; import removePresentation from './methods/removePresentation'; -import setPresentation from './methods/setPresentation'; Meteor.methods({ removePresentation, - setPresentation, }); diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js b/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js deleted file mode 100644 index 3ff16aee82..0000000000 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/setPresentation.js +++ /dev/null @@ -1,28 +0,0 @@ -import RedisPubSub from '/imports/startup/server/redis'; -import { check } from 'meteor/check'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import Logger from '/imports/startup/server/logger'; - -export default function setPresentation(presentationId, podId) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'SetCurrentPresentationPubMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(presentationId, String); - check(podId, String); - - const payload = { - presentationId, - podId, - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method setPresentation ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx index 3c39d2b0d0..c7b4c8a25d 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/container.jsx @@ -1,5 +1,4 @@ import React, { useContext } from 'react'; -import PresentationUploaderService from '/imports/ui/components/presentation/presentation-uploader/service'; import ActionsDropdown from './component'; import { layoutSelectInput, layoutDispatch, layoutSelect } from '../../layout/context'; import { SMALL_VIEWPORT_BREAKPOINT, ACTIONS, PANELS } from '../../layout/enums'; @@ -12,6 +11,7 @@ import { import { SET_PRESENTER } from '/imports/ui/core/graphql/mutations/userMutations'; import { TIMER_ACTIVATE, TIMER_DEACTIVATE } from '../../timer/mutations'; import Auth from '/imports/ui/services/auth'; +import { PRESENTATION_SET_CURRENT } from '../../presentation/mutations'; const TIMER_CONFIG = Meteor.settings.public.timer; const MILLI_IN_MINUTE = 60000; @@ -35,11 +35,16 @@ const ActionsDropdownContainer = (props) => { const [setPresenter] = useMutation(SET_PRESENTER); const [timerActivate] = useMutation(TIMER_ACTIVATE); const [timerDeactivate] = useMutation(TIMER_DEACTIVATE); + const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT); const handleTakePresenter = () => { setPresenter({ variables: { userId: Auth.userID } }); }; + const setPresentation = (presentationId) => { + presentationSetCurrent({ variables: { presentationId } }); + }; + const activateTimer = () => { const stopwatch = true; const running = false; @@ -71,7 +76,7 @@ const ActionsDropdownContainer = (props) => { presentations, isTimerFeatureEnabled: isTimerFeatureEnabled(), isDropdownOpen: Session.get('dropdownOpen'), - setPresentation: PresentationUploaderService.setPresentation, + setPresentation, isCameraAsContentEnabled: isCameraAsContentEnabled(), handleTakePresenter, activateTimer, diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx index 263744f597..a836418d60 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx @@ -51,6 +51,7 @@ class NotesDropdown extends PureComponent { intl, amIPresenter, presentations, + setPresentation, } = this.props; const { converterButtonDisabled } = this.state; @@ -71,7 +72,7 @@ class NotesDropdown extends PureComponent { onClick: () => { this.setConverterButtonDisabled(true); setTimeout(() => this.setConverterButtonDisabled(false), DEBOUNCE_TIMEOUT); - return Service.convertAndUpload(presentations); + return Service.convertAndUpload(presentations, setPresentation); }, }, ); diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx index e736b07c63..9e74d8c0dc 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx @@ -1,11 +1,12 @@ import React from 'react'; import NotesDropdown from './component'; import { layoutSelect } from '/imports/ui/components/layout/context'; -import { useSubscription } from '@apollo/client'; +import { useSubscription, useMutation } from '@apollo/client'; import { PROCESSED_PRESENTATIONS_SUBSCRIPTION, } from '/imports/ui/components/whiteboard/queries'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; +import { PRESENTATION_SET_CURRENT } from '../../presentation/mutations'; const NotesDropdownContainer = ({ ...props }) => { const { data: currentUserData } = useCurrentUser((user) => ({ @@ -17,7 +18,13 @@ const NotesDropdownContainer = ({ ...props }) => { const { data: presentationData } = useSubscription(PROCESSED_PRESENTATIONS_SUBSCRIPTION); const presentations = presentationData?.pres_presentation || []; - return ; + const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT); + + const setPresentation = (presentationId) => { + presentationSetCurrent({ variables: { presentationId } }); + }; + + return ; }; export default NotesDropdownContainer; diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js index afc7d7b1bb..a30b9ff057 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js @@ -7,8 +7,7 @@ import { uniqueId } from '/imports/utils/string-utils'; const PADS_CONFIG = Meteor.settings.public.pads; -async function convertAndUpload(presentations) { - +async function convertAndUpload(presentations, setPresentation) { let filename = 'Shared_Notes'; const duplicates = presentations.filter((pres) => pres.filename?.startsWith(filename) || pres.name?.startsWith(filename)).length; @@ -53,7 +52,7 @@ async function convertAndUpload(presentations) { onUpload: () => { }, onProgress: () => { }, onDone: () => { }, - }); + }, setPresentation); } export default { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index 78a5a95549..3442b5ac8d 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -45,9 +45,18 @@ export const PRESENTATION_SET_DOWNLOADABLE = gql` } `; +export const PRESENTATION_SET_CURRENT = gql` + mutation PresentationSetCurrent($presentationId: String!) { + presentationSetCurrent( + presentationId: $presentationId, + ) + } +`; + export default { PRESENTATION_SET_ZOOM, PRESENTATION_SET_WRITERS, PRESENTATION_SET_PAGE, PRESENTATION_SET_DOWNLOADABLE, + PRESENTATION_SET_CURRENT, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx index 0c09d9d775..26d5859083 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx @@ -583,6 +583,7 @@ class PresentationUploader extends Component { selectedToBeNextCurrent, presentations: propPresentations, dispatchChangePresentationDownloadable, + setPresentation, } = this.props; const { disableActions, presentations } = this.state; const presentationsToSave = presentations; @@ -610,7 +611,7 @@ class PresentationUploader extends Component { if (!disableActions) { Session.set('showUploadPresentationView', false); - return handleSave(presentationsToSave, true, {}, propPresentations) + return handleSave(presentationsToSave, true, {}, propPresentations, setPresentation) .then(() => { const hasError = presentations.some((p) => !!p.uploadErrorMsgKey); if (!hasError) { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx index 1364e528aa..20867af21d 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx @@ -17,7 +17,7 @@ import { PRESENTATIONS_SUBSCRIPTION, } from '/imports/ui/components/whiteboard/queries'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; -import { PRESENTATION_SET_DOWNLOADABLE } from '../mutations'; +import { PRESENTATION_SET_DOWNLOADABLE, PRESENTATION_SET_CURRENT } from '../mutations'; const PRESENTATION_CONFIG = Meteor.settings.public.presentation; @@ -32,6 +32,7 @@ const PresentationUploaderContainer = (props) => { const currentPresentation = presentations.find((p) => p.current)?.presentationId || ''; const [presentationSetDownloadable] = useMutation(PRESENTATION_SET_DOWNLOADABLE); + const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT); const exportPresentation = (presentationId, fileStateType) => { presentationSetDownloadable({ @@ -53,6 +54,10 @@ const PresentationUploaderContainer = (props) => { }); }; + const setPresentation = (presentationId) => { + presentationSetCurrent({ variables: { presentationId } }); + }; + return userIsPresenter && ( { currentPresentation={currentPresentation} exportPresentation={exportPresentation} dispatchChangePresentationDownloadable={dispatchChangePresentationDownloadable} + setPresentation={setPresentation} {...props} /> diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js index 2719e2c52c..944fbdb661 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js @@ -179,10 +179,6 @@ const uploadAndConvertPresentations = ( p.onUpload, p.onProgress, p.onConversion, p.current, ))); -const setPresentation = (presentationId) => { - makeCall('setPresentation', presentationId, POD_ID); -}; - const removePresentation = (presentationId) => { makeCall('removePresentation', presentationId, POD_ID); }; @@ -191,7 +187,7 @@ const removePresentations = ( presentationsToRemove, ) => Promise.all(presentationsToRemove.map((p) => removePresentation(p.presentationId, POD_ID))); -const persistPresentationChanges = (oldState, newState, uploadEndpoint) => { +const persistPresentationChanges = (oldState, newState, uploadEndpoint, setPresentation) => { const presentationsToUpload = newState.filter((p) => !p.uploadCompleted); const presentationsToRemove = oldState.filter((p) => !newState.find((u) => { return u.presentationId === p.presentationId })); @@ -231,7 +227,11 @@ const persistPresentationChanges = (oldState, newState, uploadEndpoint) => { }; const handleSavePresentation = ( - presentations = [], isFromPresentationUploaderInterface = true, newPres = {}, currentPresentations = [], + presentations = [], + isFromPresentationUploaderInterface = true, + newPres = {}, + currentPresentations = [], + setPresentation, ) => { if (!isPresentationEnabled()) { return null; @@ -253,7 +253,7 @@ const handleSavePresentation = ( currentPresentations, presentations, PRESENTATION_CONFIG.uploadEndpoint, - 'DEFAULT_PRESENTATION_POD', + setPresentation, ); }; @@ -348,7 +348,6 @@ function handleFiledrop(files, files2, that, intl, intlMessages) { export default { handleSavePresentation, persistPresentationChanges, - setPresentation, requestPresentationUploadToken, getExternalUploadData, uploadAndConvertPresentation, From 1388c74c2e5c50be655ff4a59299eb8d8012a686 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Tue, 23 Jan 2024 09:51:19 -0300 Subject: [PATCH 130/512] Fix duplicating log message --- .../src/main/scala/org/bigbluebutton/ClientSettings.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala index 53000a6507..7254224129 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala @@ -10,6 +10,7 @@ import scala.util.{ Failure, Success, Try } object ClientSettings extends SystemConfiguration { var clientSettingsFromFile: Map[String, Object] = Map("" -> "") val logger = LoggerFactory.getLogger(this.getClass) + val def loadClientSettingsFromFile() = { val clientSettingsFile = scala.io.Source.fromFile(clientSettingsPath, "UTF-8") @@ -56,7 +57,7 @@ object ClientSettings extends SystemConfiguration { getConfigPropertyValueByPath(map, path) match { case Some(configValue: Int) => configValue case _ => - logger.debug("Config `{}` not found.", path) + logger.debug(s"Config `$path` with type Integer not found in clientSettings.") alternativeValue } } @@ -65,7 +66,7 @@ object ClientSettings extends SystemConfiguration { getConfigPropertyValueByPath(map, path) match { case Some(configValue: String) => configValue case _ => - logger.debug("Config `{}` not found.", path) + logger.debug(s"Config `$path` with type String not found in clientSettings.") alternativeValue } } @@ -74,7 +75,7 @@ object ClientSettings extends SystemConfiguration { getConfigPropertyValueByPath(map, path) match { case Some(configValue: Boolean) => configValue case _ => - logger.debug("Config `{}` not found.", path) + logger.debug(s"Config `$path` with type Boolean found in clientSettings.") alternativeValue } } From 23389010fcad63376ba93cb3de6b0d1d71d375b8 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Tue, 23 Jan 2024 09:52:09 -0300 Subject: [PATCH 131/512] Remove wrong line --- .../src/main/scala/org/bigbluebutton/ClientSettings.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala index 7254224129..1b84d040fd 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/ClientSettings.scala @@ -10,7 +10,6 @@ import scala.util.{ Failure, Success, Try } object ClientSettings extends SystemConfiguration { var clientSettingsFromFile: Map[String, Object] = Map("" -> "") val logger = LoggerFactory.getLogger(this.getClass) - val def loadClientSettingsFromFile() = { val clientSettingsFile = scala.io.Source.fromFile(clientSettingsPath, "UTF-8") From 0dcb2bc7a2d0fdf6ce2778794483d991cb096e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Tue, 23 Jan 2024 10:55:40 -0300 Subject: [PATCH 132/512] migrate removePresentation action --- .../imports/api/presentations/server/index.js | 1 - .../api/presentations/server/methods.js | 6 ---- .../server/methods/removePresentation.js | 28 ------------------- .../notes/notes-dropdown/component.jsx | 3 +- .../notes/notes-dropdown/container.jsx | 21 ++++++++++++-- .../notes/notes-dropdown/service.js | 6 ++-- .../ui/components/presentation/mutations.jsx | 9 ++++++ .../presentation-uploader/component.jsx | 10 ++++++- .../presentation-uploader/container.jsx | 8 +++++- .../presentation-uploader/service.js | 23 +++++++++------ bigbluebutton-html5/server/main.js | 1 - 11 files changed, 64 insertions(+), 52 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/presentations/server/index.js delete mode 100644 bigbluebutton-html5/imports/api/presentations/server/methods.js delete mode 100644 bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js diff --git a/bigbluebutton-html5/imports/api/presentations/server/index.js b/bigbluebutton-html5/imports/api/presentations/server/index.js deleted file mode 100644 index ba55c4a16e..0000000000 --- a/bigbluebutton-html5/imports/api/presentations/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './methods'; diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods.js b/bigbluebutton-html5/imports/api/presentations/server/methods.js deleted file mode 100644 index 1134b35779..0000000000 --- a/bigbluebutton-html5/imports/api/presentations/server/methods.js +++ /dev/null @@ -1,6 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import removePresentation from './methods/removePresentation'; - -Meteor.methods({ - removePresentation, -}); diff --git a/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js b/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js deleted file mode 100644 index 8a2ef1e898..0000000000 --- a/bigbluebutton-html5/imports/api/presentations/server/methods/removePresentation.js +++ /dev/null @@ -1,28 +0,0 @@ -import RedisPubSub from '/imports/startup/server/redis'; -import { check } from 'meteor/check'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import Logger from '/imports/startup/server/logger'; - -export default function removePresentation(presentationId, podId) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'RemovePresentationPubMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(presentationId, String); - check(podId, String); - - const payload = { - presentationId, - podId, - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method removePresentation ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx index a836418d60..9e59a9987d 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/component.jsx @@ -52,6 +52,7 @@ class NotesDropdown extends PureComponent { amIPresenter, presentations, setPresentation, + removePresentation, } = this.props; const { converterButtonDisabled } = this.state; @@ -72,7 +73,7 @@ class NotesDropdown extends PureComponent { onClick: () => { this.setConverterButtonDisabled(true); setTimeout(() => this.setConverterButtonDisabled(false), DEBOUNCE_TIMEOUT); - return Service.convertAndUpload(presentations, setPresentation); + return Service.convertAndUpload(presentations, setPresentation, removePresentation); }, }, ); diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx index 9e74d8c0dc..122d11e1cd 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/container.jsx @@ -6,7 +6,7 @@ import { PROCESSED_PRESENTATIONS_SUBSCRIPTION, } from '/imports/ui/components/whiteboard/queries'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; -import { PRESENTATION_SET_CURRENT } from '../../presentation/mutations'; +import { PRESENTATION_SET_CURRENT, PRESENTATION_REMOVE } from '../../presentation/mutations'; const NotesDropdownContainer = ({ ...props }) => { const { data: currentUserData } = useCurrentUser((user) => ({ @@ -19,12 +19,29 @@ const NotesDropdownContainer = ({ ...props }) => { const presentations = presentationData?.pres_presentation || []; const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT); + const [presentationRemove] = useMutation(PRESENTATION_REMOVE); const setPresentation = (presentationId) => { presentationSetCurrent({ variables: { presentationId } }); }; - return ; + const removePresentation = (presentationId) => { + presentationRemove({ variables: { presentationId } }); + }; + + return ( + + ); }; export default NotesDropdownContainer; diff --git a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js index a30b9ff057..c28f3089c4 100644 --- a/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js +++ b/bigbluebutton-html5/imports/ui/components/notes/notes-dropdown/service.js @@ -7,7 +7,7 @@ import { uniqueId } from '/imports/utils/string-utils'; const PADS_CONFIG = Meteor.settings.public.pads; -async function convertAndUpload(presentations, setPresentation) { +async function convertAndUpload(presentations, setPresentation, removePresentation) { let filename = 'Shared_Notes'; const duplicates = presentations.filter((pres) => pres.filename?.startsWith(filename) || pres.name?.startsWith(filename)).length; @@ -52,7 +52,9 @@ async function convertAndUpload(presentations, setPresentation) { onUpload: () => { }, onProgress: () => { }, onDone: () => { }, - }, setPresentation); + }, + setPresentation, + removePresentation); } export default { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index 3442b5ac8d..ee698aae4b 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -53,10 +53,19 @@ export const PRESENTATION_SET_CURRENT = gql` } `; +export const PRESENTATION_REMOVE = gql` + mutation PresentationRemove($presentationId: String!) { + presentationRemove( + presentationId: $presentationId, + ) + } +`; + export default { PRESENTATION_SET_ZOOM, PRESENTATION_SET_WRITERS, PRESENTATION_SET_PAGE, PRESENTATION_SET_DOWNLOADABLE, PRESENTATION_SET_CURRENT, + PRESENTATION_REMOVE, }; diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx index 26d5859083..e09a061567 100755 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/component.jsx @@ -584,6 +584,7 @@ class PresentationUploader extends Component { presentations: propPresentations, dispatchChangePresentationDownloadable, setPresentation, + removePresentation, } = this.props; const { disableActions, presentations } = this.state; const presentationsToSave = presentations; @@ -611,7 +612,14 @@ class PresentationUploader extends Component { if (!disableActions) { Session.set('showUploadPresentationView', false); - return handleSave(presentationsToSave, true, {}, propPresentations, setPresentation) + return handleSave( + presentationsToSave, + true, + {}, + propPresentations, + setPresentation, + removePresentation, + ) .then(() => { const hasError = presentations.some((p) => !!p.uploadErrorMsgKey); if (!hasError) { diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx index 20867af21d..181f09cbf9 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx @@ -17,7 +17,7 @@ import { PRESENTATIONS_SUBSCRIPTION, } from '/imports/ui/components/whiteboard/queries'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; -import { PRESENTATION_SET_DOWNLOADABLE, PRESENTATION_SET_CURRENT } from '../mutations'; +import { PRESENTATION_SET_DOWNLOADABLE, PRESENTATION_SET_CURRENT, PRESENTATION_REMOVE } from '../mutations'; const PRESENTATION_CONFIG = Meteor.settings.public.presentation; @@ -33,6 +33,7 @@ const PresentationUploaderContainer = (props) => { const [presentationSetDownloadable] = useMutation(PRESENTATION_SET_DOWNLOADABLE); const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT); + const [presentationRemove] = useMutation(PRESENTATION_REMOVE); const exportPresentation = (presentationId, fileStateType) => { presentationSetDownloadable({ @@ -58,6 +59,10 @@ const PresentationUploaderContainer = (props) => { presentationSetCurrent({ variables: { presentationId } }); }; + const removePresentation = (presentationId) => { + presentationRemove({ variables: { presentationId } }); + }; + return userIsPresenter && ( { exportPresentation={exportPresentation} dispatchChangePresentationDownloadable={dispatchChangePresentationDownloadable} setPresentation={setPresentation} + removePresentation={removePresentation} {...props} /> diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js index 944fbdb661..459740f021 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/service.js @@ -179,15 +179,18 @@ const uploadAndConvertPresentations = ( p.onUpload, p.onProgress, p.onConversion, p.current, ))); -const removePresentation = (presentationId) => { - makeCall('removePresentation', presentationId, POD_ID); -}; - const removePresentations = ( presentationsToRemove, -) => Promise.all(presentationsToRemove.map((p) => removePresentation(p.presentationId, POD_ID))); + removePresentation, +) => Promise.all(presentationsToRemove.map((p) => removePresentation(p.presentationId))); -const persistPresentationChanges = (oldState, newState, uploadEndpoint, setPresentation) => { +const persistPresentationChanges = ( + oldState, + newState, + uploadEndpoint, + setPresentation, + removePresentation, +) => { const presentationsToUpload = newState.filter((p) => !p.uploadCompleted); const presentationsToRemove = oldState.filter((p) => !newState.find((u) => { return u.presentationId === p.presentationId })); @@ -206,7 +209,7 @@ const persistPresentationChanges = (oldState, newState, uploadEndpoint, setPrese }) .then((presentations) => { if (currentPresentation === undefined) { - setPresentation('', POD_ID); + setPresentation(''); return Promise.resolve(); } @@ -221,9 +224,9 @@ const persistPresentationChanges = (oldState, newState, uploadEndpoint, setPrese return Promise.resolve(); } - return setPresentation(currentPresentation?.presentationId, POD_ID); + return setPresentation(currentPresentation?.presentationId); }) - .then(removePresentations.bind(null, presentationsToRemove, POD_ID)); + .then(removePresentations.bind(null, presentationsToRemove, removePresentation)); }; const handleSavePresentation = ( @@ -232,6 +235,7 @@ const handleSavePresentation = ( newPres = {}, currentPresentations = [], setPresentation, + removePresentation, ) => { if (!isPresentationEnabled()) { return null; @@ -254,6 +258,7 @@ const handleSavePresentation = ( presentations, PRESENTATION_CONFIG.uploadEndpoint, setPresentation, + removePresentation, ); }; diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js index 739d7f3c09..1a33749f2f 100755 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -7,7 +7,6 @@ import '/imports/api/annotations/server'; import '/imports/api/cursor/server'; import '/imports/api/polls/server'; import '/imports/api/captions/server'; -import '/imports/api/presentations/server'; import '/imports/api/presentation-upload-token/server'; import '/imports/api/breakouts/server'; import '/imports/api/breakouts-history/server'; From 00e881dfd662c97eb7878ea904e5d4b794cb6b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Tue, 23 Jan 2024 11:31:39 -0300 Subject: [PATCH 133/512] migrate deleteAnnotations action --- .../imports/api/annotations/server/methods.js | 2 - .../server/methods/deleteAnnotations.js | 29 ----- .../ui/components/presentation/mutations.jsx | 10 ++ .../ui/components/whiteboard/component.jsx | 3 +- .../ui/components/whiteboard/container.jsx | 13 +- .../ui/components/whiteboard/service.js | 3 - .../imports/ui/components/whiteboard/utils.js | 117 +----------------- 7 files changed, 24 insertions(+), 153 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/annotations/server/methods/deleteAnnotations.js diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods.js b/bigbluebutton-html5/imports/api/annotations/server/methods.js index cafab18911..a2721ec9e9 100644 --- a/bigbluebutton-html5/imports/api/annotations/server/methods.js +++ b/bigbluebutton-html5/imports/api/annotations/server/methods.js @@ -1,10 +1,8 @@ import { Meteor } from 'meteor/meteor'; import sendAnnotations from './methods/sendAnnotations'; import sendBulkAnnotations from './methods/sendBulkAnnotations'; -import deleteAnnotations from './methods/deleteAnnotations'; Meteor.methods({ sendAnnotations, sendBulkAnnotations, - deleteAnnotations, }); diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/deleteAnnotations.js b/bigbluebutton-html5/imports/api/annotations/server/methods/deleteAnnotations.js deleted file mode 100644 index a255ab9364..0000000000 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/deleteAnnotations.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import RedisPubSub from '/imports/startup/server/redis'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import { check } from 'meteor/check'; -import Logger from '/imports/startup/server/logger'; - -export default function deleteAnnotations(annotations, whiteboardId) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'DeleteWhiteboardAnnotationsPubMsg'; - - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(whiteboardId, String); - check(annotations, Array); - - const payload = { - whiteboardId, - annotationsIds: annotations, - }; - - return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method deleteAnnotation ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index ee698aae4b..4a6d9aecfb 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -61,6 +61,15 @@ export const PRESENTATION_REMOVE = gql` } `; +export const PRES_ANNOTATION_DELETE = gql` + mutation PresAnnotationDelete($pageId: String!, $annotationsIds: [String]!) { + presAnnotationDelete( + pageId: $pageId, + annotationsIds: $annotationsIds, + ) + } +`; + export default { PRESENTATION_SET_ZOOM, PRESENTATION_SET_WRITERS, @@ -68,4 +77,5 @@ export default { PRESENTATION_SET_DOWNLOADABLE, PRESENTATION_SET_CURRENT, PRESENTATION_REMOVE, + PRES_ANNOTATION_DELETE, }; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 9cddcda5c7..a06223b32f 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -20,7 +20,6 @@ import { findRemoved, filterInvalidShapes, mapLanguage, - sendShapeChanges, usePrevious, } from "./utils"; // import { throttle } from "/imports/utils/throttle"; @@ -720,7 +719,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) { }); Object.values(removed).forEach((record) => { - removeShapes([record.id], whiteboardId); + removeShapes([record.id]); }); }, { source: "user", scope: "document" } diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index 2266f8507b..cf3b3fa3f9 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -10,7 +10,6 @@ import { CURSOR_SUBSCRIPTION } from './cursors/queries'; import { initDefaultPages, persistShape, - removeShapes, notifyNotAllowedChange, notifyShapeNumberExceeded, toggleToolsAnimations, @@ -33,7 +32,7 @@ import useMeeting from '/imports/ui/core/hooks/useMeeting'; import { AssetRecordType, } from "@tldraw/tldraw"; -import { PRESENTATION_SET_ZOOM } from '../presentation/mutations'; +import { PRESENTATION_SET_ZOOM, PRES_ANNOTATION_DELETE } from '../presentation/mutations'; const WHITEBOARD_CONFIG = Meteor.settings.public.whiteboard; @@ -62,6 +61,16 @@ const WhiteboardContainer = (props) => { const hasWBAccess = whiteboardWriters?.some((writer) => writer.userId === Auth.userID); const [presentationSetZoom] = useMutation(PRESENTATION_SET_ZOOM); + const [presentationDeleteAnnotations] = useMutation(PRES_ANNOTATION_DELETE); + + const removeShapes = (shapeIds) => { + presentationDeleteAnnotations({ + variables: { + pageId: currentPresentationPage?.pageId, + annotationsIds: shapeIds, + }, + }); + }; const zoomSlide = (widthRatio, heightRatio, xOffset, yOffset) => { const { pageId, num } = currentPresentationPage; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js index 6043a7f426..1be7fb562f 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js @@ -98,8 +98,6 @@ const persistShape = (shape, whiteboardId, isModerator) => { sendAnnotation(annotation); }; -const removeShapes = (shapes, whiteboardId) => makeCall('deleteAnnotations', shapes, whiteboardId); - const initDefaultPages = (count = 1) => { const pages = {}; const pageStates = {}; @@ -279,7 +277,6 @@ export { sendAnnotation, getMultiUser, persistShape, - removeShapes, notifyNotAllowedChange, notifyShapeNumberExceeded, toggleToolsAnimations, diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js b/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js index 476af5e148..f2fdc45d5c 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js @@ -2,7 +2,6 @@ import React from 'react'; import { isEqual } from 'radash'; import { persistShape, - removeShapes, notifyNotAllowedChange, notifyShapeNumberExceeded, } from './service'; @@ -90,118 +89,6 @@ const isValidShapeType = (shape) => { return !invalidTypes.includes(shape?.type); }; -const sendShapeChanges = ( - app, - changedShapes, - shapes, - prevShapes, - hasShapeAccess, - whiteboardId, - currentUser, - intl, - redo = false, -) => { - let isModerator = currentUser?.role === ROLE_MODERATOR; - - const invalidChange = Object.keys(changedShapes) - .find((id) => !hasShapeAccess(id)); - - const invalidShapeType = Object.keys(changedShapes) - .find((id) => !isValidShapeType(changedShapes[id])); - - const currentShapes = app?.document?.pages[app?.currentPageId]?.shapes; - const { maxNumberOfAnnotations } = WHITEBOARD_CONFIG; - // -1 for background shape - const shapeNumberExceeded = Object.keys(currentShapes).length - 1 > maxNumberOfAnnotations; - - const isInserting = Object.keys(changedShapes) - .filter( - (shape) => typeof changedShapes[shape] === 'object' - && changedShapes[shape].type - && !prevShapes[shape], - ).length !== 0; - - if (invalidChange || invalidShapeType || (shapeNumberExceeded && isInserting)) { - if (shapeNumberExceeded) { - notifyShapeNumberExceeded(intl, maxNumberOfAnnotations); - } else { - notifyNotAllowedChange(intl); - } - const modApp = app; - // undo last command without persisting to not generate the onUndo/onRedo callback - if (!redo) { - const command = app.stack[app.pointer]; - modApp.pointer -= 1; - app.applyPatch(command.before, 'undo'); - return; - // eslint-disable-next-line no-else-return - } else { - modApp.pointer += 1; - const command = app.stack[app.pointer]; - app.applyPatch(command.after, 'redo'); - return; - } - } - const deletedShapes = []; - Object.entries(changedShapes) - .forEach(([id, shape]) => { - if (!shape) deletedShapes.push(id); - else { - // checks to find any bindings assosiated with the changed shapes. - // If any, they may need to be updated as well. - const pageBindings = app.page.bindings; - if (pageBindings) { - Object.entries(pageBindings).forEach(([, b]) => { - if (b.toId.includes(id)) { - const boundShape = app.getShape(b.fromId); - if (shapes[b.fromId] && !isEqual(boundShape, shapes[b.fromId])) { - const shapeBounds = app.getShapeBounds(b.fromId); - boundShape.size = [shapeBounds.width, shapeBounds.height]; - persistShape(boundShape, whiteboardId, isModerator); - } - } - }); - } - let modShape = shape; - if (!shape.id) { - // check it already exists (otherwise we need the full shape) - if (!shapes[id]) { - modShape = app.getShape(id); - } - modShape.id = id; - } - const shapeBounds = app.getShapeBounds(id); - const size = [shapeBounds.width, shapeBounds.height]; - if (!shapes[id] || (shapes[id] && !isEqual(shapes[id].size, size))) { - modShape.size = size; - } - if (!shapes[id] || (shapes[id] && !shapes[id].userId)) { - modShape.userId = currentUser?.userId; - } - // do not change moderator status for existing shapes - if (shapes[id]) { - isModerator = shapes[id].isModerator; - } - persistShape(modShape, whiteboardId, isModerator); - } - }); - - // order the ids of shapes being deleted to prevent crash - // when removing a group shape before its children - const orderedDeletedShapes = []; - deletedShapes.forEach((eid) => { - if (shapes[eid]?.type !== 'group') { - orderedDeletedShapes.unshift(eid); - } else { - orderedDeletedShapes.push(eid); - } - }); - - if (orderedDeletedShapes.length > 0) { - removeShapes(orderedDeletedShapes, whiteboardId); - } -}; - // map different localeCodes from bbb to tldraw const mapLanguage = (language) => { // bbb has xx-xx but in tldraw it's only xx @@ -276,10 +163,10 @@ const getTextSize = (text, style, padding) => { }; const Utils = { - usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize, + usePrevious, findRemoved, filterInvalidShapes, mapLanguage, getTextSize, }; export default Utils; export { - usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize, + usePrevious, findRemoved, filterInvalidShapes, mapLanguage, getTextSize, }; From 68394d4b14375f8f65af050289a25771ecfad162 Mon Sep 17 00:00:00 2001 From: KDSBrowne Date: Tue, 23 Jan 2024 10:36:26 -0500 Subject: [PATCH 134/512] fix: Improve Arrow Shape Handling With Tldraw v2 (#19376) * move cleaning arrow shape to akka --- .../core/apps/WhiteboardModel.scala | 65 +++++++++++--- .../ui/components/whiteboard/component.jsx | 85 +++++++------------ 2 files changed, 85 insertions(+), 65 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala index 7908a30d00..00e1d9ea18 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/WhiteboardModel.scala @@ -47,19 +47,31 @@ class WhiteboardModel extends SystemConfiguration { }).toMap def addAnnotations(wbId: String, userId: String, annotations: Array[AnnotationVO], isPresenter: Boolean, isModerator: Boolean): Array[AnnotationVO] = { - var annotationsAdded = Array[AnnotationVO]() val wb = getWhiteboard(wbId) + + var annotationsAdded = Array[AnnotationVO]() var newAnnotationsMap = wb.annotationsMap + for (annotation <- annotations) { val oldAnnotation = wb.annotationsMap.get(annotation.id) if (!oldAnnotation.isEmpty) { val hasPermission = isPresenter || isModerator || oldAnnotation.get.userId == userId if (hasPermission) { - val newAnnotation = oldAnnotation.get.copy(annotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo)) + // Merge old and new annotation properties + val mergedAnnotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo) + + // Apply cleaning if it's an arrow annotation + val finalAnnotationInfo = if (annotation.annotationInfo.get("type").contains("arrow")) { + cleanArrowAnnotationProps(mergedAnnotationInfo) + } else { + mergedAnnotationInfo + } + + val newAnnotation = oldAnnotation.get.copy(annotationInfo = finalAnnotationInfo) newAnnotationsMap += (annotation.id -> newAnnotation) - annotationsAdded :+= annotation - PresAnnotationDAO.insertOrUpdate(newAnnotation, annotation) - println(s"Updated annotation onpage [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") + annotationsAdded :+= newAnnotation + PresAnnotationDAO.insertOrUpdate(newAnnotation, newAnnotation) + println(s"Updated annotation on page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") } else { println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...") } @@ -69,40 +81,67 @@ class WhiteboardModel extends SystemConfiguration { PresAnnotationDAO.insertOrUpdate(annotation, annotation) println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") } else { - println(s"New annotation [${annotation.id}] with no type, ignoring (probably received a remove message before and now the shape is incomplete, ignoring...") + println(s"New annotation [${annotation.id}] with no type, ignoring...") } } + val newWb = wb.copy(annotationsMap = newAnnotationsMap) saveWhiteboard(newWb) annotationsAdded } + private def cleanArrowAnnotationProps(annotationInfo: Map[String, _]): Map[String, _] = { + annotationInfo.get("props") match { + case Some(props: Map[String, _]) => + val cleanedProps = props.map { + case ("end", endProps: Map[String, _]) => "end" -> cleanEndOrStartProps(endProps) + case ("start", startProps: Map[String, _]) => "start" -> cleanEndOrStartProps(startProps) + case other => other + } + annotationInfo + ("props" -> cleanedProps) + case _ => annotationInfo + } + } + + private def cleanEndOrStartProps(props: Map[String, _]): Map[String, _] = { + props.get("type") match { + case Some("binding") => props - ("x", "y") // Remove 'x' and 'y' for 'binding' type + case Some("point") => props - ("boundShapeId", "normalizedAnchor", "isExact") // Remove unwanted properties for 'point' type + case _ => props + } + } + def getHistory(wbId: String): Array[AnnotationVO] = { val wb = getWhiteboard(wbId) wb.annotationsMap.values.toArray } def deleteAnnotations(wbId: String, userId: String, annotationsIds: Array[String], isPresenter: Boolean, isModerator: Boolean): Array[String] = { - var annotationsIdsRemoved = Array[String]() val wb = getWhiteboard(wbId) + + var annotationsIdsRemoved = Array[String]() var newAnnotationsMap = wb.annotationsMap for (annotationId <- annotationsIds) { val annotation = wb.annotationsMap.get(annotationId) - if (!annotation.isEmpty) { + if (annotation.isDefined) { val hasPermission = isPresenter || isModerator || annotation.get.userId == userId if (hasPermission) { newAnnotationsMap -= annotationId - println("Removing annotation on page [" + wb.id + "]. After numAnnotations=[" + newAnnotationsMap.size + "].") + println(s"Removed annotation $annotationId on page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") annotationsIdsRemoved :+= annotationId } else { - println("User doesn't have permission to remove this annotation, ignoring...") + println(s"User $userId doesn't have permission to remove annotation $annotationId, ignoring...") } + } else { + println(s"Annotation $annotationId not found while trying to delete it.") } } - val newWb = wb.copy(annotationsMap = newAnnotationsMap) - saveWhiteboard(newWb) + + // Update whiteboard and save + val updatedWb = wb.copy(annotationsMap = newAnnotationsMap) + saveWhiteboard(updatedWb) annotationsIdsRemoved.map(PresAnnotationDAO.delete(wbId, userId, _)) @@ -130,4 +169,4 @@ class WhiteboardModel extends SystemConfiguration { } def getChangedModeOn(wbId: String): Long = getWhiteboard(wbId).changedModeOn -} +} \ No newline at end of file diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index ecc7fa1025..0e5db3ec57 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -80,21 +80,6 @@ const determineViewerFitToWidth = (currentPresentationPage) => { ); }; -const cleanArrowShapeProps = (shapeProp) => { - if (!shapeProp) return; - - if (shapeProp.type === "binding") { - delete shapeProp.x; - delete shapeProp.y; - } - - if (shapeProp.type === "point") { - delete shapeProp.boundShapeId; - delete shapeProp.normalizedAnchor; - delete shapeProp.isExact; - } -}; - export default Whiteboard = React.memo(function Whiteboard(props) { const { isPresenter, @@ -117,11 +102,11 @@ export default Whiteboard = React.memo(function Whiteboard(props) { svgUri, maxStickyNoteLength, fontFamily, - colorStyle, - dashStyle, - fillStyle, - fontStyle, - sizeStyle, + colorStyle, + dashStyle, + fillStyle, + fontStyle, + sizeStyle, hasShapeAccess, presentationAreaHeight, presentationAreaWidth, @@ -268,10 +253,6 @@ export default Whiteboard = React.memo(function Whiteboard(props) { }); } - if (diff?.type === "arrow") { - cleanArrowShapeProps(diff?.props?.end); - cleanArrowShapeProps(diff?.props?.start); - } toUpdate.push(diff); } } @@ -700,29 +681,29 @@ export default Whiteboard = React.memo(function Whiteboard(props) { console.log("EDITOR : ", editor); const debouncePersistShape = debounce({ delay: 0 }, persistShape); - - const colorStyles = ['black', 'blue', 'green', 'grey', 'light-blue', 'light-green', 'light-red', 'light-violet', 'orange', 'red', 'violet', 'yellow']; - const dashStyles = ['dashed', 'dotted', 'draw', 'solid']; - const fillStyles = ['none', 'pattern', 'semi', 'solid']; - const fontStyles = ['draw','mono','sans', 'serif']; - const sizeStyles = ['l', 'm', 's', 'xl']; - - if ( colorStyles.includes(colorStyle) ) { - editor.setStyleForNextShapes(DefaultColorStyle, colorStyle); - } - if ( dashStyles.includes(dashStyle) ) { - editor.setStyleForNextShapes(DefaultDashStyle, dashStyle); - } - if ( fillStyles.includes(fillStyle) ) { - editor.setStyleForNextShapes(DefaultFillStyle, fillStyle); - } - if ( fontStyles.includes(fontStyle)) { - editor.setStyleForNextShapes(DefaultFontStyle, fontStyle); - } - if ( sizeStyles.includes(sizeStyle) ) { - editor.setStyleForNextShapes(DefaultSizeStyle, sizeStyle); - } - + + const colorStyles = ['black', 'blue', 'green', 'grey', 'light-blue', 'light-green', 'light-red', 'light-violet', 'orange', 'red', 'violet', 'yellow']; + const dashStyles = ['dashed', 'dotted', 'draw', 'solid']; + const fillStyles = ['none', 'pattern', 'semi', 'solid']; + const fontStyles = ['draw','mono','sans', 'serif']; + const sizeStyles = ['l', 'm', 's', 'xl']; + + if ( colorStyles.includes(colorStyle) ) { + editor.setStyleForNextShapes(DefaultColorStyle, colorStyle); + } + if ( dashStyles.includes(dashStyle) ) { + editor.setStyleForNextShapes(DefaultDashStyle, dashStyle); + } + if ( fillStyles.includes(fillStyle) ) { + editor.setStyleForNextShapes(DefaultFillStyle, fillStyle); + } + if ( fontStyles.includes(fontStyle)) { + editor.setStyleForNextShapes(DefaultFontStyle, fontStyle); + } + if ( sizeStyles.includes(sizeStyle) ) { + editor.setStyleForNextShapes(DefaultSizeStyle, sizeStyle); + } + editor.store.listen( (entry) => { const { changes } = entry; @@ -944,11 +925,11 @@ Whiteboard.propTypes = { svgUri: PropTypes.string, maxStickyNoteLength: PropTypes.number.isRequired, fontFamily: PropTypes.string.isRequired, - colorStyle: PropTypes.string.isRequired, - dashStyle: PropTypes.string.isRequired, - fillStyle: PropTypes.string.isRequired, - fontStyle: PropTypes.string.isRequired, - sizeStyle: PropTypes.string.isRequired, + colorStyle: PropTypes.string.isRequired, + dashStyle: PropTypes.string.isRequired, + fillStyle: PropTypes.string.isRequired, + fontStyle: PropTypes.string.isRequired, + sizeStyle: PropTypes.string.isRequired, hasShapeAccess: PropTypes.func.isRequired, presentationAreaHeight: PropTypes.number.isRequired, presentationAreaWidth: PropTypes.number.isRequired, From f5e65962e5b42e4e47d1f01fb27708c82fd5960b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Tue, 23 Jan 2024 13:14:29 -0300 Subject: [PATCH 135/512] Tweak element types --- .../ui/components/polling/polling-graphql/component.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx index a26e9bc313..d185a7b0db 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx @@ -1,5 +1,5 @@ import React, { - ElementRef, useEffect, useMemo, useRef, useState, + useEffect, useMemo, useRef, useState, } from 'react'; import { useMutation, useSubscription } from '@apollo/client'; import { defineMessages, useIntl } from 'react-intl'; @@ -94,8 +94,8 @@ const PollingGraphql: React.FC = (props) => { const [typedAns, setTypedAns] = useState(''); const [checkedAnswers, setCheckedAnswers] = useState>([]); const intl = useIntl(); - const responseInput = useRef>(null); - const pollingContainer = useRef>(null); + const responseInput = useRef(null); + const pollingContainer = useRef(null); useEffect(() => { play(); From ea463b37b54f9919b205277819547e83695ffaa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Tue, 23 Jan 2024 13:41:15 -0300 Subject: [PATCH 136/512] Move all the necessary services to own service --- .../polling/polling-graphql/component.tsx | 30 +++++++------------ .../polling/polling-graphql/service.ts | 19 +++++++++++- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx index d185a7b0db..a65a79d721 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx @@ -4,11 +4,8 @@ import React, { import { useMutation, useSubscription } from '@apollo/client'; import { defineMessages, useIntl } from 'react-intl'; import { Meteor } from 'meteor/meteor'; -import AudioService from '/imports/ui/components/audio/service'; import Checkbox from '/imports/ui/components/common/checkbox/component'; -import PollService from '/imports/ui/components/poll/service'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; -import { isPollingEnabled } from '/imports/ui/services/features'; import { POLL_SUBMIT_TYPED_VOTE, POLL_SUBMIT_VOTE, @@ -17,7 +14,7 @@ import { hasPendingPoll, HasPendingPollResponse, } from './queries'; -import { shouldStackOptions } from './service'; +import Service from './service'; import Styled from './styles'; const MAX_INPUT_CHARS = Meteor.settings.public.poll.maxTypedAnswerLength; @@ -66,6 +63,7 @@ interface PollingGraphqlProps { pollAnswerIds: Record; pollTypes: Record; isDefaultPoll: (pollType: string) => boolean; + playAlert: () => void; poll: { pollId: string; multipleResponses: boolean; @@ -89,6 +87,7 @@ const PollingGraphql: React.FC = (props) => { pollAnswerIds, pollTypes, isDefaultPoll, + playAlert, } = props; const [typedAns, setTypedAns] = useState(''); @@ -98,22 +97,12 @@ const PollingGraphql: React.FC = (props) => { const pollingContainer = useRef(null); useEffect(() => { - play(); + playAlert(); if (pollingContainer.current) { pollingContainer.current.focus(); } }, []); - const play = () => { - AudioService.playAlertSound( - `${ - Meteor.settings.public.app.cdn - + Meteor.settings.public.app.basename - + Meteor.settings.public.app.instanceId - }/resources/sounds/Poll.mp3`, - ); - }; - const handleUpdateResponseInput = (e: React.ChangeEvent) => { if (responseInput.current) { responseInput.current.value = validateInput(e.target.value); @@ -357,9 +346,9 @@ const PollingGraphqlContainer: React.FC = () => { const pollData = meetingData && meetingData.polls[0]; const userData = pollData && pollData.users[0]; const pollExists = !!userData; - const showPolling = pollExists && !currentUserData?.presenter && isPollingEnabled(); + const showPolling = pollExists && !currentUserData?.presenter && Service.isPollingEnabled(); const stackOptions = useMemo( - () => !!pollData && shouldStackOptions(pollData.options.map((o) => o.optionDesc)), + () => !!pollData && Service.shouldStackOptions(pollData.options.map((o) => o.optionDesc)), [pollData], ); @@ -391,9 +380,10 @@ const PollingGraphqlContainer: React.FC = () => { ...pollData, stackOptions, }} - pollAnswerIds={PollService.pollAnswerIds} - pollTypes={PollService.pollTypes} - isDefaultPoll={PollService.isDefaultPoll} + pollAnswerIds={Service.pollAnswerIds} + isDefaultPoll={Service.isDefaultPoll} + pollTypes={Service.pollTypes} + playAlert={Service.playAlert} /> ); }; diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/service.ts b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/service.ts index 848179b395..8f2a1dfec4 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/service.ts +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/service.ts @@ -1,5 +1,22 @@ +import { Meteor } from 'meteor/meteor'; +import PollService from '/imports/ui/components/poll/service'; +import AudioService from '/imports/ui/components/audio/service'; +import { isPollingEnabled } from '/imports/ui/services/features'; + const MAX_CHAR_LENGTH = 5; +const APP_CONFIG = Meteor.settings.public.app; export const shouldStackOptions = (keys: Array) => keys.some((k) => k.length > MAX_CHAR_LENGTH); -export default { shouldStackOptions }; +const playAlert = () => AudioService.playAlertSound( + `${APP_CONFIG.cdn + APP_CONFIG.basename + APP_CONFIG.instanceId}/resources/sounds/Poll.mp3`, +); + +export default { + shouldStackOptions, + pollAnswerIds: PollService.pollAnswerIds, + pollTypes: PollService.pollTypes, + isDefaultPoll: PollService.isDefaultPoll, + playAlert, + isPollingEnabled, +}; From eb135d86ef01007007aaf4120e30e098914317a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Tue, 23 Jan 2024 13:42:47 -0300 Subject: [PATCH 137/512] Remove unused interface --- .../ui/components/polling/polling-graphql/component.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx index a65a79d721..db63329fcb 100644 --- a/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/polling/polling-graphql/component.tsx @@ -55,8 +55,6 @@ const validateInput = (i: string) => { return input; }; -interface PollingGraphqlContainerProps {} - interface PollingGraphqlProps { handleTypedVote: (pollId: string, answer: string) => void; handleVote: (pollId: string, answerIds: Array) => void; @@ -328,7 +326,7 @@ const PollingGraphql: React.FC = (props) => { ); }; -const PollingGraphqlContainer: React.FC = () => { +const PollingGraphqlContainer: React.FC = () => { const { data: currentUserData } = useCurrentUser((u) => ({ userId: u.userId, presenter: u.presenter, From 6d12508f41728118a1206c6b0e36d11ef3a172fb Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Tue, 23 Jan 2024 15:28:32 -0300 Subject: [PATCH 138/512] feature (graphql-middlware): Enhancing GraphQL Stream Subscriptions: Efficient Handling of Connection Resets and Cursor Management (#19481) --- .../internal/common/StreamCursorUtils.go | 135 ++++++++++++++++++ .../internal/common/types.go | 8 +- .../internal/hascli/conn/reader/reader.go | 21 ++- .../internal/hascli/conn/writer/writer.go | 28 +++- .../hascli/retransmiter/retransmiter.go | 8 +- 5 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 bbb-graphql-middleware/internal/common/StreamCursorUtils.go diff --git a/bbb-graphql-middleware/internal/common/StreamCursorUtils.go b/bbb-graphql-middleware/internal/common/StreamCursorUtils.go new file mode 100644 index 0000000000..872a13dc32 --- /dev/null +++ b/bbb-graphql-middleware/internal/common/StreamCursorUtils.go @@ -0,0 +1,135 @@ +package common + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +func GetStreamCursorPropsFromQuery(payload map[string]interface{}, query string) (string, string, interface{}) { + streamCursorField := "" + streamCursorVariableName := "" + var streamCursorInitialValue interface{} + + cursorInitialValueRePattern := regexp.MustCompile(`cursor:\s*\{\s*initial_value\s*:\s*\{\s*([^:]+):\s*([^}]+)\s*}\s*}`) + matches := cursorInitialValueRePattern.FindStringSubmatch(query) + if matches != nil { + streamCursorField = matches[1] + if strings.HasPrefix(matches[2], "$") { + streamCursorVariableName, _ = strings.CutPrefix(matches[2], "$") + if variables, okVariables := payload["variables"].(map[string]interface{}); okVariables { + if targetVariableValue, okTargetVariableValue := variables[streamCursorVariableName]; okTargetVariableValue { + streamCursorInitialValue = targetVariableValue + } + } + } else { + streamCursorInitialValue = matches[2] + } + } + + return streamCursorField, streamCursorVariableName, streamCursorInitialValue +} + +func GetLastStreamCursorValueFromReceivedMessage(messageAsMap map[string]interface{}, streamCursorField string) interface{} { + var lastStreamCursorValue interface{} + + if payload, okPayload := messageAsMap["payload"].(map[string]interface{}); okPayload { + if data, okData := payload["data"].(map[string]interface{}); okData { + //Data will have only one prop, `range` because its name is unknown + for _, dataItem := range data { + currentDataProp, okCurrentDataProp := dataItem.([]interface{}) + if okCurrentDataProp && len(currentDataProp) > 0 { + // Get the last item directly (once it will contain the last cursor value) + lastItemOfMessage := currentDataProp[len(currentDataProp)-1] + if lastItemOfMessageAsMap, currDataOk := lastItemOfMessage.(map[string]interface{}); currDataOk { + if lastItemValue, okLastItemValue := lastItemOfMessageAsMap[streamCursorField]; okLastItemValue { + lastStreamCursorValue = lastItemValue + } + } + } + } + } + } + + return lastStreamCursorValue +} + +func PatchQueryIncludingCursorField(originalQuery string, cursorField string) string { + if cursorField == "" { + return originalQuery + } + + lastIndex := strings.LastIndex(originalQuery, "{") + if lastIndex == -1 { + return originalQuery + } + + // It will include the cursorField at the beginning of the list of fields + // It's not a problem if the field be duplicated in the list, Hasura just ignore the second occurrence + return originalQuery[:lastIndex+1] + "\n " + cursorField + originalQuery[lastIndex+1:] +} + +func PatchQuerySettingLastCursorValue(subscription GraphQlSubscription) interface{} { + message := subscription.Message + payload, okPayload := message["payload"].(map[string]interface{}) + + if okPayload { + if subscription.StreamCursorVariableName != "" { + /**** This stream has its cursor value set through variables ****/ + if variables, okVariables := payload["variables"].(map[string]interface{}); okVariables { + if variables[subscription.StreamCursorVariableName] != subscription.StreamCursorCurrValue { + variables[subscription.StreamCursorVariableName] = subscription.StreamCursorCurrValue + payload["variables"] = variables + message["payload"] = payload + } + } + } else { + /**** This stream has its cursor value set through inline value (not variables) ****/ + query, okQuery := payload["query"].(string) + if okQuery { + cursorInitialValueRePattern := regexp.MustCompile(`cursor:\s*\{\s*initial_value\s*:\s*\{\s*([^:]+:\s*[^}]+)\s*}\s*}`) + newValue := "" + + replaceInitialValueFunc := func(match string) string { + switch v := subscription.StreamCursorCurrValue.(type) { + case string: + newValue = v + + //Append quotes if it is missing, it will be necessary when appending to the query + if !strings.HasPrefix(v, "\"") { + newValue = "\"" + newValue + } + if !strings.HasSuffix(v, "\"") { + newValue = newValue + "\"" + } + case int: + newValue = strconv.Itoa(v) + case float32: + myFloat64 := float64(v) + newValue = strconv.FormatFloat(myFloat64, 'f', -1, 32) + case float64: + newValue = strconv.FormatFloat(v, 'f', -1, 64) + default: + newValue = "" + } + + if newValue != "" { + replacement := subscription.StreamCursorField + ": " + newValue + return fmt.Sprintf("cursor: {initial_value: {%s}}", replacement) + } else { + return match + } + } + + newQuery := cursorInitialValueRePattern.ReplaceAllStringFunc(query, replaceInitialValueFunc) + if query != newQuery { + payload["query"] = newQuery + message["payload"] = payload + } + } + } + } + + return message +} diff --git a/bbb-graphql-middleware/internal/common/types.go b/bbb-graphql-middleware/internal/common/types.go index 666ccc8932..15bc7e5db5 100644 --- a/bbb-graphql-middleware/internal/common/types.go +++ b/bbb-graphql-middleware/internal/common/types.go @@ -19,8 +19,12 @@ const ( type GraphQlSubscription struct { Id string - Message interface{} + Message map[string]interface{} Type QueryType + OperationName string + StreamCursorField string + StreamCursorVariableName string + StreamCursorCurrValue interface{} JsonPatchSupported bool // indicate if client support Json Patch for this subscription LastSeenOnHasuraConnetion string // id of the hasura connection that this query was active } @@ -31,7 +35,7 @@ type BrowserConnection struct { Context context.Context // browser connection context ActiveSubscriptions map[string]GraphQlSubscription // active subscriptions of this connection (start, but no stop) ActiveSubscriptionsMutex sync.RWMutex // mutex to control the map usage - ConnectionInitMessage interface{} // init message received in this connection (to be used on hasura reconnect) + ConnectionInitMessage map[string]interface{} // init message received in this connection (to be used on hasura reconnect) HasuraConnection *HasuraConnection // associated hasura connection Disconnected bool // indicate if the connection is gone } diff --git a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go index 486f76b641..c66a299c35 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go +++ b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go @@ -1,6 +1,8 @@ package reader import ( + "context" + "errors" "github.com/iMDT/bbb-graphql-middleware/internal/common" "github.com/iMDT/bbb-graphql-middleware/internal/hascli/retransmiter" "github.com/iMDT/bbb-graphql-middleware/internal/msgpatch" @@ -23,7 +25,11 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan var message interface{} err := wsjson.Read(hc.Context, hc.Websocket, &message) if err != nil { - log.Errorf("Error: %v", err) + if errors.Is(err, context.Canceled) { + log.Debugf("Closing ws connection as Context was cancelled!") + } else { + log.Errorf("Error reading message from Hasura: %v", err) + } return } @@ -59,6 +65,19 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan subscription.Type == common.Subscription { msgpatch.PatchMessage(&messageAsMap, hc.Browserconn) } + + //Set last cursor value for stream + if subscription.Type == common.Streaming { + lastCursor := common.GetLastStreamCursorValueFromReceivedMessage(messageAsMap, subscription.StreamCursorField) + if lastCursor != nil && subscription.StreamCursorCurrValue != lastCursor { + subscription.StreamCursorCurrValue = lastCursor + + hc.Browserconn.ActiveSubscriptionsMutex.Lock() + hc.Browserconn.ActiveSubscriptions[queryId] = subscription + hc.Browserconn.ActiveSubscriptionsMutex.Unlock() + } + + } } // Write the message to browser diff --git a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go index 8b34d03609..334bc3c01a 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go +++ b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go @@ -41,7 +41,11 @@ RangeLoop: //Identify type based on query string messageType := common.Query + streamCursorField := "" + streamCursorVariableName := "" + var streamCursorInitialValue interface{} payload := fromBrowserMessageAsMap["payload"].(map[string]interface{}) + query, ok := payload["query"].(string) if ok { if strings.HasPrefix(query, "subscription") { @@ -49,6 +53,18 @@ RangeLoop: if strings.Contains(query, "_stream(") && strings.Contains(query, "cursor: {") { messageType = common.Streaming + + browserConnection.ActiveSubscriptionsMutex.RLock() + _, queryIdExists := browserConnection.ActiveSubscriptions[queryId] + browserConnection.ActiveSubscriptionsMutex.RUnlock() + if !queryIdExists { + streamCursorField, streamCursorVariableName, streamCursorInitialValue = common.GetStreamCursorPropsFromQuery(payload, query) + + //It's necessary to assure the cursor field will return in the result of the query + //To be able to store the last received cursor value + payload["query"] = common.PatchQueryIncludingCursorField(query, streamCursorField) + fromBrowserMessageAsMap["payload"] = payload + } } if strings.Contains(query, "_aggregate") && strings.Contains(query, "aggregate {") { @@ -72,7 +88,11 @@ RangeLoop: browserConnection.ActiveSubscriptionsMutex.Lock() browserConnection.ActiveSubscriptions[queryId] = common.GraphQlSubscription{ Id: queryId, - Message: fromBrowserMessage, + Message: fromBrowserMessageAsMap, + OperationName: operationName, + StreamCursorField: streamCursorField, + StreamCursorVariableName: streamCursorVariableName, + StreamCursorCurrValue: streamCursorInitialValue, LastSeenOnHasuraConnetion: hc.Id, JsonPatchSupported: jsonPatchSupported, Type: messageType, @@ -96,11 +116,11 @@ RangeLoop: } if fromBrowserMessageAsMap["type"] == "connection_init" { - browserConnection.ConnectionInitMessage = fromBrowserMessage + browserConnection.ConnectionInitMessage = fromBrowserMessageAsMap } - log.Tracef("sending to hasura: %v", fromBrowserMessage) - err := wsjson.Write(hc.Context, hc.Websocket, fromBrowserMessage) + log.Tracef("sending to hasura: %v", fromBrowserMessageAsMap) + err := wsjson.Write(hc.Context, hc.Websocket, fromBrowserMessageAsMap) if err != nil { log.Errorf("error on write (we're disconnected from hasura): %v", err) return diff --git a/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go b/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go index 5cb1bb7cd9..530d167e42 100644 --- a/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go +++ b/bbb-graphql-middleware/internal/hascli/retransmiter/retransmiter.go @@ -11,8 +11,14 @@ func RetransmitSubscriptionStartMessages(hc *common.HasuraConnection, fromBrowse hc.Browserconn.ActiveSubscriptionsMutex.RLock() for _, subscription := range hc.Browserconn.ActiveSubscriptions { if subscription.LastSeenOnHasuraConnetion != hc.Id { + log.Tracef("retransmiting subscription start: %v", subscription.Message) - fromBrowserToHasuraChannel.Send(subscription.Message) + + if subscription.Type == common.Streaming && subscription.StreamCursorCurrValue != nil { + fromBrowserToHasuraChannel.Send(common.PatchQuerySettingLastCursorValue(subscription)) + } else { + fromBrowserToHasuraChannel.Send(subscription.Message) + } } } hc.Browserconn.ActiveSubscriptionsMutex.RUnlock() From c8bea83de85904717966320749235843448748c7 Mon Sep 17 00:00:00 2001 From: Guilherme Pereira Leme <69865537+GuiLeme@users.noreply.github.com> Date: Tue, 23 Jan 2024 17:34:50 -0300 Subject: [PATCH 139/512] feat(plugin): refactor name of general exensible areas interface (#19467) * [plugin-sdk-issue-62] - refactor general extensible area interface * [plugin-sdk-issue-62] - refactor last components and bump SDK version --- .../buttons/LiveSelection.tsx | 4 ++-- .../components/action-bar/manager.tsx | 8 +++---- .../action-button-dropdown/manager.tsx | 8 +++---- .../audio-settings-dropdown/manager.tsx | 8 +++---- .../camera-settings-dropdown/manager.tsx | 8 +++---- .../components/nav-bar/manager.tsx | 8 +++---- .../components/options-dropdown/manager.tsx | 8 +++---- .../presentation-dropdown/manager.tsx | 8 +++---- .../presentation-toolbar/manager.tsx | 8 +++---- .../user-camera-dropdown/manager.tsx | 8 +++---- .../components/user-list-dropdown/manager.tsx | 8 +++---- .../manager.tsx | 8 +++---- .../plugins-engine/extensible-areas/types.ts | 22 +++++++++---------- .../list-item/component.tsx | 10 ++++----- .../user-actions/component.tsx | 14 ++++++------ bigbluebutton-html5/package-lock.json | 6 ++--- bigbluebutton-html5/package.json | 2 +- 17 files changed, 73 insertions(+), 73 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/LiveSelection.tsx b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/LiveSelection.tsx index f81930500c..518f28b5ec 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/LiveSelection.tsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/LiveSelection.tsx @@ -95,7 +95,7 @@ export const LiveSelection: React.FC = ({ const { pluginsExtensibleAreasAggregatedState, } = useContext(PluginsContext); - let audioSettingsDropdownItems = [] as PluginSdk.AudioSettingsDropdownItem[]; + let audioSettingsDropdownItems = [] as PluginSdk.AudioSettingsDropdownInterface[]; if (pluginsExtensibleAreasAggregatedState.audioSettingsDropdownItems) { audioSettingsDropdownItems = [ ...pluginsExtensibleAreasAggregatedState.audioSettingsDropdownItems, @@ -209,7 +209,7 @@ export const LiveSelection: React.FC = ({ .concat(leaveAudioOption); audioSettingsDropdownItems.forEach((audioSettingsDropdownItem: - PluginSdk.AudioSettingsDropdownItem) => { + PluginSdk.AudioSettingsDropdownInterface) => { switch (audioSettingsDropdownItem.type) { case AudioSettingsDropdownItemType.OPTION: { const audioSettingsDropdownOption = audioSettingsDropdownItem as PluginSdk.AudioSettingsDropdownOption; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/action-bar/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/action-bar/manager.tsx index 59dbea3692..8247ebe9ea 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/action-bar/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/action-bar/manager.tsx @@ -16,7 +16,7 @@ const ActionBarPluginStateContainer = (( const [ actionBarItems, setActionBarItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -29,7 +29,7 @@ const ActionBarPluginStateContainer = (( // Update context with computed aggregated list of all plugin provided toolbar items const aggregatedActionBarItems = ( - [] as PluginSdk.ActionsBarItem[]).concat( + [] as PluginSdk.ActionsBarInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.actionsBarItems), ); @@ -41,8 +41,8 @@ const ActionBarPluginStateContainer = (( ); }, [actionBarItems]); - pluginApi.setActionsBarItems = (items: PluginSdk.ActionsBarItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.ActionsBarItem[]; + pluginApi.setActionsBarItems = (items: PluginSdk.ActionsBarInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.ActionsBarInterface[]; return setActionBarItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/action-button-dropdown/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/action-button-dropdown/manager.tsx index 1879d935c0..41b0a64e56 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/action-button-dropdown/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/action-button-dropdown/manager.tsx @@ -19,7 +19,7 @@ const ActionButtonDropdownPluginStateContainer = (( const [ actionButtonDropdownItems, setActionButtonDropdownItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -32,7 +32,7 @@ const ActionButtonDropdownPluginStateContainer = (( // Update context with computed aggregated list of all plugin provided toolbar items const aggregatedActionButtonDropdownItems = ( - [] as PluginSdk.ActionButtonDropdownItem[]).concat( + [] as PluginSdk.ActionButtonDropdownInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.actionButtonDropdownItems), ); @@ -44,8 +44,8 @@ const ActionButtonDropdownPluginStateContainer = (( ); }, [actionButtonDropdownItems]); - pluginApi.setActionButtonDropdownItems = (items: PluginSdk.ActionButtonDropdownItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.ActionButtonDropdownItem[]; + pluginApi.setActionButtonDropdownItems = (items: PluginSdk.ActionButtonDropdownInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.ActionButtonDropdownInterface[]; return setActionButtonDropdownItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/audio-settings-dropdown/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/audio-settings-dropdown/manager.tsx index f13097dbe2..6363454385 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/audio-settings-dropdown/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/audio-settings-dropdown/manager.tsx @@ -18,7 +18,7 @@ const AudioSettingsDropdownPluginStateContainer = (( const [ audioSettingsDropdownItems, setAudioSettingsDropdownItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -30,7 +30,7 @@ const AudioSettingsDropdownPluginStateContainer = (( extensibleAreaMap[uuid].audioSettingsDropdownItems = audioSettingsDropdownItems; // Update context with computed aggregated list of all plugin provided toolbar items - const aggregatedAudioSettingsDropdownItems = ([] as PluginSdk.AudioSettingsDropdownItem[]).concat( + const aggregatedAudioSettingsDropdownItems = ([] as PluginSdk.AudioSettingsDropdownInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.audioSettingsDropdownItems), ); @@ -43,8 +43,8 @@ const AudioSettingsDropdownPluginStateContainer = (( ); }, [audioSettingsDropdownItems]); - pluginApi.setAudioSettingsDropdownItems = (items: PluginSdk.AudioSettingsDropdownItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.AudioSettingsDropdownItem[]; + pluginApi.setAudioSettingsDropdownItems = (items: PluginSdk.AudioSettingsDropdownInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.AudioSettingsDropdownInterface[]; return setAudioSettingsDropdownItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/camera-settings-dropdown/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/camera-settings-dropdown/manager.tsx index fe54d5857d..763da23b93 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/camera-settings-dropdown/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/camera-settings-dropdown/manager.tsx @@ -19,7 +19,7 @@ const CameraSettingsDropdownPluginStateContainer = (( const [ cameraSettingsDropdownItems, setCameraSettingsDropdownItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -32,7 +32,7 @@ const CameraSettingsDropdownPluginStateContainer = (( // Update context with computed aggregated list of all plugin provided toolbar items const aggregatedCameraSettingsDropdownItems = ( - [] as PluginSdk.CameraSettingsDropdownItem[]).concat( + [] as PluginSdk.CameraSettingsDropdownInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.cameraSettingsDropdownItems), ); @@ -44,8 +44,8 @@ const CameraSettingsDropdownPluginStateContainer = (( ); }, [cameraSettingsDropdownItems]); - pluginApi.setCameraSettingsDropdownItems = (items: PluginSdk.CameraSettingsDropdownItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.CameraSettingsDropdownItem[]; + pluginApi.setCameraSettingsDropdownItems = (items: PluginSdk.CameraSettingsDropdownInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.CameraSettingsDropdownInterface[]; return setCameraSettingsDropdownItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/nav-bar/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/nav-bar/manager.tsx index 74bc494266..09ccc431d1 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/nav-bar/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/nav-bar/manager.tsx @@ -19,7 +19,7 @@ const NavBarPluginStateContainer = (( const [ navBarItems, setNavBarItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -31,7 +31,7 @@ const NavBarPluginStateContainer = (( extensibleAreaMap[uuid].navBarItems = navBarItems; // Update context with computed aggregated list of all plugin provided toolbar items - const aggregatedNavBarItems = ([] as PluginSdk.NavBarItem[]).concat( + const aggregatedNavBarItems = ([] as PluginSdk.NavBarInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.navBarItems), ); @@ -44,8 +44,8 @@ const NavBarPluginStateContainer = (( ); }, [navBarItems]); - pluginApi.setNavBarItems = (items: PluginSdk.NavBarItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.NavBarItem[]; + pluginApi.setNavBarItems = (items: PluginSdk.NavBarInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.NavBarInterface[]; return setNavBarItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/options-dropdown/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/options-dropdown/manager.tsx index 17b9962595..b6cf48727c 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/options-dropdown/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/options-dropdown/manager.tsx @@ -19,7 +19,7 @@ const OptionsDropdownPluginStateContainer = (( const [ optionsDropdownItems, setOptionsDropdownItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -32,7 +32,7 @@ const OptionsDropdownPluginStateContainer = (( // Update context with computed aggregated list of all plugin provided toolbar items const aggregatedOptionsDropdownItems = ( - [] as PluginSdk.OptionsDropdownItem[]).concat( + [] as PluginSdk.OptionsDropdownInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.optionsDropdownItems), ); @@ -44,8 +44,8 @@ const OptionsDropdownPluginStateContainer = (( ); }, [optionsDropdownItems]); - pluginApi.setOptionsDropdownItems = (items: PluginSdk.OptionsDropdownItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.OptionsDropdownItem[]; + pluginApi.setOptionsDropdownItems = (items: PluginSdk.OptionsDropdownInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.OptionsDropdownInterface[]; return setOptionsDropdownItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/presentation-dropdown/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/presentation-dropdown/manager.tsx index d126270137..c8d97c66ca 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/presentation-dropdown/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/presentation-dropdown/manager.tsx @@ -19,7 +19,7 @@ const PresentationDropdownPluginStateContainer = (( const [ presentationDropdownItems, setPresentationDropdownItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -32,7 +32,7 @@ const PresentationDropdownPluginStateContainer = (( // Update context with computed aggregated list of all plugin provided toolbar items const aggregatedPresentationDropdownItems = ( - [] as PluginSdk.PresentationDropdownItem[]).concat( + [] as PluginSdk.PresentationDropdownInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.presentationDropdownItems), ); @@ -44,8 +44,8 @@ const PresentationDropdownPluginStateContainer = (( ); }, [presentationDropdownItems]); - pluginApi.setPresentationDropdownItems = (items: PluginSdk.PresentationDropdownItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.PresentationDropdownItem[]; + pluginApi.setPresentationDropdownItems = (items: PluginSdk.PresentationDropdownInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.PresentationDropdownInterface[]; return setPresentationDropdownItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/presentation-toolbar/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/presentation-toolbar/manager.tsx index ed1324fe6d..cd3e87aff4 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/presentation-toolbar/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/presentation-toolbar/manager.tsx @@ -19,7 +19,7 @@ const PresentationToolbarPluginStateContainer = (( const [ presentationToolbarItems, setPresentationToolbarItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -31,7 +31,7 @@ const PresentationToolbarPluginStateContainer = (( extensibleAreaMap[uuid].presentationToolbarItems = presentationToolbarItems; // Update context with computed aggregated list of all plugin provided toolbar items - const aggregatedPresentationToolbarItems = ([] as PluginSdk.PresentationToolbarItem[]).concat( + const aggregatedPresentationToolbarItems = ([] as PluginSdk.PresentationToolbarInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.presentationToolbarItems), ); @@ -44,8 +44,8 @@ const PresentationToolbarPluginStateContainer = (( ); }, [presentationToolbarItems]); - pluginApi.setPresentationToolbarItems = (items: PluginSdk.PresentationToolbarItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.PresentationToolbarItem[]; + pluginApi.setPresentationToolbarItems = (items: PluginSdk.PresentationToolbarInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.PresentationToolbarInterface[]; return setPresentationToolbarItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-camera-dropdown/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-camera-dropdown/manager.tsx index 7b3105e82a..34363b6fc0 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-camera-dropdown/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-camera-dropdown/manager.tsx @@ -19,7 +19,7 @@ const UserCameraDropdownPluginStateContainer = (( const [ userCameraDropdownItems, setUserCameraDropdownItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -32,7 +32,7 @@ const UserCameraDropdownPluginStateContainer = (( // Update context with computed aggregated list of all plugin provided toolbar items const aggregatedUserCameraDropdownItems = ( - [] as PluginSdk.UserCameraDropdownItem[]).concat( + [] as PluginSdk.UserCameraDropdownInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.userCameraDropdownItems), ); @@ -44,8 +44,8 @@ const UserCameraDropdownPluginStateContainer = (( ); }, [userCameraDropdownItems]); - pluginApi.setUserCameraDropdownItems = (items: PluginSdk.UserCameraDropdownItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.UserCameraDropdownItem[]; + pluginApi.setUserCameraDropdownItems = (items: PluginSdk.UserCameraDropdownInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.UserCameraDropdownInterface[]; return setUserCameraDropdownItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-list-dropdown/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-list-dropdown/manager.tsx index 176e2a5655..6e1f58f0d4 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-list-dropdown/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-list-dropdown/manager.tsx @@ -19,7 +19,7 @@ const UserListDropdownPluginStateContainer = (( const [ userListDropdownItems, setUserListDropdownItems, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -32,7 +32,7 @@ const UserListDropdownPluginStateContainer = (( // Update context with computed aggregated list of all plugin provided toolbar items const aggregatedUserListDropdownItems = ( - [] as PluginSdk.UserListDropdownItem[]).concat( + [] as PluginSdk.UserListDropdownInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.userListDropdownItems), ); @@ -44,8 +44,8 @@ const UserListDropdownPluginStateContainer = (( ); }, [userListDropdownItems]); - pluginApi.setUserListDropdownItems = (items: PluginSdk.UserListDropdownItem[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.UserListDropdownItem[]; + pluginApi.setUserListDropdownItems = (items: PluginSdk.UserListDropdownInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.UserListDropdownInterface[]; return setUserListDropdownItems(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-list-item-additional-information/manager.tsx b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-list-item-additional-information/manager.tsx index 28ed7f3bca..4eed563f82 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-list-item-additional-information/manager.tsx +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/components/user-list-item-additional-information/manager.tsx @@ -19,7 +19,7 @@ const UserListItemAdditionalInformationPluginStateContainer = (( const [ userListItemAdditionalInformation, setUserListItemAdditionalInformation, - ] = useState([]); + ] = useState([]); const { pluginsExtensibleAreasAggregatedState, @@ -32,7 +32,7 @@ const UserListItemAdditionalInformationPluginStateContainer = (( // Update context with computed aggregated list of all plugin provided toolbar items const aggregatedUserListItemAdditionalInformation = ( - [] as PluginSdk.UserListItemAdditionalInformation[]).concat( + [] as PluginSdk.UserListItemAdditionalInformationInterface[]).concat( ...Object.values(extensibleAreaMap) .map((extensibleArea: ExtensibleArea) => extensibleArea.userListItemAdditionalInformation), ); @@ -44,8 +44,8 @@ const UserListItemAdditionalInformationPluginStateContainer = (( ); }, [userListItemAdditionalInformation]); - pluginApi.setUserListItemAdditionalInformation = (items: PluginSdk.UserListItemAdditionalInformation[]) => { - const itemsWithId = items.map(generateItemWithId) as PluginSdk.UserListItemAdditionalInformation[]; + pluginApi.setUserListItemAdditionalInformation = (items: PluginSdk.UserListItemAdditionalInformationInterface[]) => { + const itemsWithId = items.map(generateItemWithId) as PluginSdk.UserListItemAdditionalInformationInterface[]; return setUserListItemAdditionalInformation(itemsWithId); }; return null; diff --git a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/types.ts b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/types.ts index a43bb3825c..a64a506b16 100644 --- a/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/types.ts +++ b/bigbluebutton-html5/imports/ui/components/plugins-engine/extensible-areas/types.ts @@ -8,17 +8,17 @@ export interface ExtensibleAreaStateManagerProps { } export interface ExtensibleArea { - presentationToolbarItems: PluginSdk.PresentationToolbarItem[]; - userListDropdownItems: PluginSdk.UserListDropdownItem[]; - actionButtonDropdownItems: PluginSdk.ActionButtonDropdownItem[]; - audioSettingsDropdownItems: PluginSdk.AudioSettingsDropdownItem[]; - actionsBarItems: PluginSdk.ActionsBarItem[]; - presentationDropdownItems: PluginSdk.PresentationDropdownItem[]; - navBarItems: PluginSdk.NavBarItem[]; - optionsDropdownItems: PluginSdk.OptionsDropdownItem[]; - cameraSettingsDropdownItems: PluginSdk.CameraSettingsDropdownItem[]; - userCameraDropdownItems: PluginSdk.UserCameraDropdownItem[]; - userListItemAdditionalInformation: PluginSdk.UserListItemAdditionalInformation[]; + presentationToolbarItems: PluginSdk.PresentationToolbarInterface[]; + userListDropdownItems: PluginSdk.UserListDropdownInterface[]; + actionButtonDropdownItems: PluginSdk.ActionButtonDropdownInterface[]; + audioSettingsDropdownItems: PluginSdk.AudioSettingsDropdownInterface[]; + actionsBarItems: PluginSdk.ActionsBarInterface[]; + presentationDropdownItems: PluginSdk.PresentationDropdownInterface[]; + navBarItems: PluginSdk.NavBarInterface[]; + optionsDropdownItems: PluginSdk.OptionsDropdownInterface[]; + cameraSettingsDropdownItems: PluginSdk.CameraSettingsDropdownInterface[]; + userCameraDropdownItems: PluginSdk.UserCameraDropdownInterface[]; + userListItemAdditionalInformation: PluginSdk.UserListItemAdditionalInformationInterface[]; floatingWindows: PluginSdk.FloatingWindowInterface[] } diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx index 1b0d641694..d33595331d 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx @@ -76,10 +76,10 @@ interface UserListItemProps { } const renderUserListItemIconsFromPlugin = ( - userItemsFromPlugin: PluginSdk.UserListItemAdditionalInformation[], + userItemsFromPlugin: PluginSdk.UserListItemAdditionalInformationInterface[], ) => userItemsFromPlugin.filter( (item) => item.type === UserListItemAdditionalInformationType.ICON, -).map((item: PluginSdk.UserListItemAdditionalInformation) => { +).map((item: PluginSdk.UserListItemAdditionalInformationInterface) => { const itemToRender = item as PluginSdk.UserListItemIcon; return ( = ({ emoji, native, size }) => ( const UserListItem: React.FC = ({ user, lockSettings }) => { const { pluginsExtensibleAreasAggregatedState } = useContext(PluginsContext); - let userItemsFromPlugin = [] as PluginSdk.UserListItemAdditionalInformation[]; + let userItemsFromPlugin = [] as PluginSdk.UserListItemAdditionalInformationInterface[]; if (pluginsExtensibleAreasAggregatedState.userListItemAdditionalInformation) { userItemsFromPlugin = pluginsExtensibleAreasAggregatedState.userListItemAdditionalInformation.filter((item) => { - const userListItem = item as PluginSdk.UserListItemAdditionalInformation; + const userListItem = item as PluginSdk.UserListItemAdditionalInformationInterface; return userListItem.userId === user.userId; - }) as PluginSdk.UserListItemAdditionalInformation[]; + }) as PluginSdk.UserListItemAdditionalInformationInterface[]; } const intl = useIntl(); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx index a0cabefa57..4583f022f0 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx @@ -151,10 +151,10 @@ const messages = defineMessages({ }, }); const makeDropdownPluginItem: ( - userDropdownItems: PluginSdk.UserListDropdownItem[]) => DropdownItem[] = ( - userDropdownItems: PluginSdk.UserListDropdownItem[], + userDropdownItems: PluginSdk.UserListDropdownInterface[]) => DropdownItem[] = ( + userDropdownItems: PluginSdk.UserListDropdownInterface[], ) => userDropdownItems.map( - (userDropdownItem: PluginSdk.UserListDropdownItem) => { + (userDropdownItem: PluginSdk.UserListDropdownInterface) => { const returnValue: DropdownItem = { isSeparator: false, key: userDropdownItem.id, @@ -272,7 +272,7 @@ const UserActions: React.FC = ({ && lockSettings.hasActiveLockSetting && !user.isModerator; - let userListDropdownItems = [] as PluginSdk.UserListDropdownItem[]; + let userListDropdownItems = [] as PluginSdk.UserListDropdownInterface[]; if (pluginsExtensibleAreasAggregatedState.userListDropdownItems) { userListDropdownItems = [ ...pluginsExtensibleAreasAggregatedState.userListDropdownItems, @@ -280,7 +280,7 @@ const UserActions: React.FC = ({ } const userDropdownItems = userListDropdownItems.filter( - (item: PluginSdk.UserListDropdownItem) => (user?.userId === item?.userId), + (item: PluginSdk.UserListDropdownInterface) => (user?.userId === item?.userId), ); const hasWhiteboardAccess = user.presPagesWritable?.length > 0; @@ -316,7 +316,7 @@ const UserActions: React.FC = ({ const dropdownOptions = [ ...makeDropdownPluginItem(userDropdownItems.filter( - (item: PluginSdk.UserListDropdownItem) => (item?.type === UserListDropdownItemType.INFORMATION), + (item: PluginSdk.UserListDropdownInterface) => (item?.type === UserListDropdownItemType.INFORMATION), )), { allowed: allowedToChangeStatus, @@ -556,7 +556,7 @@ const UserActions: React.FC = ({ icon: 'time', }, ...makeDropdownPluginItem(userDropdownItems.filter( - (item: PluginSdk.UserListDropdownItem) => (item?.type !== UserListDropdownItemType.INFORMATION), + (item: PluginSdk.UserListDropdownInterface) => (item?.type !== UserListDropdownItemType.INFORMATION), )), ]; diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index d3a81e4834..09e6671d21 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -3418,9 +3418,9 @@ "dev": true }, "bigbluebutton-html-plugin-sdk": { - "version": "0.0.32", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.32.tgz", - "integrity": "sha512-VDMSFwUFox/z5G9P8aeAafFVATX+mCPpDxb+r43qaDa7ZeABATvQ1KKhgQ+/L5JLm9QP6IzWiGVFVWHDjd6x4A==", + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.34.tgz", + "integrity": "sha512-quIzZP/a5rmGJiFnJeZucKF0ix4vxWWCLKF4HM7QxJ+HVb5g6BGDcI42TY/c5yoc8Ylmv5jOJNiF9OAXf1W/sw==", "requires": { "@apollo/client": "^3.8.7" } diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 5a67275380..425a577bce 100644 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -47,7 +47,7 @@ "autoprefixer": "^10.4.4", "axios": "^1.6.0", "babel-runtime": "~6.26.0", - "bigbluebutton-html-plugin-sdk": "0.0.32", + "bigbluebutton-html-plugin-sdk": "0.0.34", "bowser": "^2.11.0", "browser-bunyan": "^1.8.0", "classnames": "^2.2.6", From 47fdc4a87889f138112e4465f4efad5f3bc30627 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Tue, 23 Jan 2024 19:32:11 -0300 Subject: [PATCH 140/512] Fix Graphql error --- .../internal/common/SafeChannel.go | 21 ++++++++++++++++--- .../internal/common/types.go | 1 + .../internal/hascli/client.go | 15 ++++++------- .../internal/hascli/conn/reader/reader.go | 15 ++++++++++--- .../internal/hascli/conn/writer/writer.go | 13 +++++++++++- .../internal/websrv/connhandler.go | 9 ++++---- .../internal/websrv/reader/reader.go | 11 +++++----- 7 files changed, 62 insertions(+), 23 deletions(-) diff --git a/bbb-graphql-middleware/internal/common/SafeChannel.go b/bbb-graphql-middleware/internal/common/SafeChannel.go index 527576443e..fdc11c0784 100644 --- a/bbb-graphql-middleware/internal/common/SafeChannel.go +++ b/bbb-graphql-middleware/internal/common/SafeChannel.go @@ -5,9 +5,10 @@ import ( ) type SafeChannel struct { - ch chan interface{} - closed bool - mux sync.Mutex + ch chan interface{} + closed bool + mux sync.Mutex + freezeFlag bool } func NewSafeChannel(size int) *SafeChannel { @@ -45,3 +46,17 @@ func (s *SafeChannel) Close() { s.closed = true } } + +func (s *SafeChannel) FreezeChannel() { + if !s.freezeFlag { + s.mux.Lock() + s.freezeFlag = true + } +} + +func (s *SafeChannel) UnfreezeChannel() { + if s.freezeFlag { + s.mux.Unlock() + s.freezeFlag = false + } +} diff --git a/bbb-graphql-middleware/internal/common/types.go b/bbb-graphql-middleware/internal/common/types.go index 15bc7e5db5..f3ea4e1c83 100644 --- a/bbb-graphql-middleware/internal/common/types.go +++ b/bbb-graphql-middleware/internal/common/types.go @@ -38,6 +38,7 @@ type BrowserConnection struct { ConnectionInitMessage map[string]interface{} // init message received in this connection (to be used on hasura reconnect) HasuraConnection *HasuraConnection // associated hasura connection Disconnected bool // indicate if the connection is gone + ConnAckSentToBrowser bool // indicate if `connection_ack` msg was already sent to the browser } type HasuraConnection struct { diff --git a/bbb-graphql-middleware/internal/hascli/client.go b/bbb-graphql-middleware/internal/hascli/client.go index 224a066615..5bf445fb1f 100644 --- a/bbb-graphql-middleware/internal/hascli/client.go +++ b/bbb-graphql-middleware/internal/hascli/client.go @@ -67,7 +67,13 @@ func HasuraClient(browserConnection *common.BrowserConnection, cookies []*http.C } browserConnection.HasuraConnection = &thisConnection - defer func() { browserConnection.HasuraConnection = nil }() + defer func() { + browserConnection.HasuraConnection = nil + + //It's necessary to freeze the channel to avoid client trying to start subscriptions before Hasura connection is initialised + //It will unfreeze after `connection_ack` is sent by Hasura + fromBrowserToHasuraChannel.FreezeChannel() + }() // Make the connection c, _, err := websocket.Dial(hasuraConnectionContext, hasuraEndpoint, &dialOptions) @@ -90,16 +96,11 @@ func HasuraClient(browserConnection *common.BrowserConnection, cookies []*http.C // Start routines // reads from browser, writes to hasura - go writer.HasuraConnectionWriter(&thisConnection, fromBrowserToHasuraChannel, &wg) + go writer.HasuraConnectionWriter(&thisConnection, fromBrowserToHasuraChannel, &wg, browserConnection.ConnectionInitMessage) // reads from hasura, writes to browser go reader.HasuraConnectionReader(&thisConnection, fromHasuraToBrowserChannel, fromBrowserToHasuraChannel, &wg) - // if it's a reconnect, inject authentication - if !browserConnection.Disconnected && browserConnection.ConnectionInitMessage != nil { - fromBrowserToHasuraChannel.Send(browserConnection.ConnectionInitMessage) - } - // Wait wg.Wait() diff --git a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go index c66a299c35..07b01089f7 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go +++ b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go @@ -80,13 +80,22 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan } } - // Write the message to browser - fromHasuraToBrowserChannel.Send(messageAsMap) - // Retransmit the subscription start commands when hasura confirms the connection // this is useful in case of a connection invalidation if messageType == "connection_ack" { + //Hasura connection was initialized, now it's able to send new messages to Hasura + fromBrowserToHasuraChannel.UnfreezeChannel() + + //Avoid to send `connection_ack` to the browser when it's a reconnection + if hc.Browserconn.ConnAckSentToBrowser == false { + fromHasuraToBrowserChannel.Send(messageAsMap) + hc.Browserconn.ConnAckSentToBrowser = true + } + go retransmiter.RetransmitSubscriptionStartMessages(hc, fromBrowserToHasuraChannel) + } else { + // Forward the message to browser + fromHasuraToBrowserChannel.Send(messageAsMap) } } } diff --git a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go index 334bc3c01a..9d7ea23837 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go +++ b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go @@ -12,7 +12,7 @@ import ( // HasuraConnectionWriter // process messages (middleware to hasura) -func HasuraConnectionWriter(hc *common.HasuraConnection, fromBrowserToHasuraChannel *common.SafeChannel, wg *sync.WaitGroup) { +func HasuraConnectionWriter(hc *common.HasuraConnection, fromBrowserToHasuraChannel *common.SafeChannel, wg *sync.WaitGroup, initMessage map[string]interface{}) { log := log.WithField("_routine", "HasuraConnectionWriter") browserConnection := hc.Browserconn @@ -23,6 +23,17 @@ func HasuraConnectionWriter(hc *common.HasuraConnection, fromBrowserToHasuraChan defer hc.ContextCancelFunc() defer log.Debugf("finished") + //Send authentication (init) message at first + //It will not use the channel (fromBrowserToHasuraChannel) because this msg must bypass ChannelFreeze + if initMessage != nil { + log.Infof("it's a reconnection, injecting authentication (init) message") + err := wsjson.Write(hc.Context, hc.Websocket, initMessage) + if err != nil { + log.Errorf("error on write authentication (init) message (we're disconnected from hasura): %v", err) + return + } + } + RangeLoop: for { select { diff --git a/bbb-graphql-middleware/internal/websrv/connhandler.go b/bbb-graphql-middleware/internal/websrv/connhandler.go index 1e25a30cbc..038c755b6a 100644 --- a/bbb-graphql-middleware/internal/websrv/connhandler.go +++ b/bbb-graphql-middleware/internal/websrv/connhandler.go @@ -54,9 +54,10 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) { defer c.Close(websocket.StatusInternalError, "the sky is falling") var thisConnection = common.BrowserConnection{ - Id: browserConnectionId, - ActiveSubscriptions: make(map[string]common.GraphQlSubscription, 1), - Context: browserConnectionContext, + Id: browserConnectionId, + ActiveSubscriptions: make(map[string]common.GraphQlSubscription, 1), + Context: browserConnectionContext, + ConnAckSentToBrowser: false, } BrowserConnectionsMutex.Lock() @@ -97,8 +98,8 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) { BrowserConnectionsMutex.RLock() thisBrowserConnection := BrowserConnections[browserConnectionId] BrowserConnectionsMutex.RUnlock() - log.Debugf("created hasura client") if thisBrowserConnection != nil { + log.Debugf("created hasura client") hascli.HasuraClient(thisBrowserConnection, r.Cookies(), fromBrowserToHasuraChannel, fromHasuraToBrowserChannel) } time.Sleep(100 * time.Millisecond) diff --git a/bbb-graphql-middleware/internal/websrv/reader/reader.go b/bbb-graphql-middleware/internal/websrv/reader/reader.go index 9ba7a8aca4..2915ff5381 100644 --- a/bbb-graphql-middleware/internal/websrv/reader/reader.go +++ b/bbb-graphql-middleware/internal/websrv/reader/reader.go @@ -10,14 +10,14 @@ import ( "time" ) -func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromBrowserToHasuraChannel1 *common.SafeChannel, fromBrowserToHasuraChannel2 *common.SafeChannel, waitGroups []*sync.WaitGroup) { +func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromBrowserToHasuraChannel *common.SafeChannel, fromBrowserToHasuraConnectionEstablishingChannel *common.SafeChannel, waitGroups []*sync.WaitGroup) { log := log.WithField("_routine", "BrowserConnectionReader").WithField("browserConnectionId", browserConnectionId) defer log.Debugf("finished") log.Debugf("starting") defer func() { - fromBrowserToHasuraChannel1.Close() - fromBrowserToHasuraChannel2.Close() + fromBrowserToHasuraChannel.Close() + fromBrowserToHasuraConnectionEstablishingChannel.Close() }() defer func() { @@ -41,8 +41,9 @@ func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c } log.Tracef("received from browser: %v", v) + //fmt.Println("received from browser: %v", v) - fromBrowserToHasuraChannel1.Send(v) - fromBrowserToHasuraChannel2.Send(v) + fromBrowserToHasuraChannel.Send(v) + fromBrowserToHasuraConnectionEstablishingChannel.Send(v) } } From fab48cc1b6e9bea1d8aed091eb8d559650099d5f Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Tue, 23 Jan 2024 20:20:16 -0300 Subject: [PATCH 141/512] Fix Graphql error `start received before the connection is initialised` (#19497) --- .../internal/common/SafeChannel.go | 21 ++++++++++++++++--- .../internal/common/types.go | 1 + .../internal/hascli/client.go | 15 ++++++------- .../internal/hascli/conn/reader/reader.go | 15 ++++++++++--- .../internal/hascli/conn/writer/writer.go | 13 +++++++++++- .../internal/websrv/connhandler.go | 9 ++++---- .../internal/websrv/reader/reader.go | 11 +++++----- 7 files changed, 62 insertions(+), 23 deletions(-) diff --git a/bbb-graphql-middleware/internal/common/SafeChannel.go b/bbb-graphql-middleware/internal/common/SafeChannel.go index 527576443e..fdc11c0784 100644 --- a/bbb-graphql-middleware/internal/common/SafeChannel.go +++ b/bbb-graphql-middleware/internal/common/SafeChannel.go @@ -5,9 +5,10 @@ import ( ) type SafeChannel struct { - ch chan interface{} - closed bool - mux sync.Mutex + ch chan interface{} + closed bool + mux sync.Mutex + freezeFlag bool } func NewSafeChannel(size int) *SafeChannel { @@ -45,3 +46,17 @@ func (s *SafeChannel) Close() { s.closed = true } } + +func (s *SafeChannel) FreezeChannel() { + if !s.freezeFlag { + s.mux.Lock() + s.freezeFlag = true + } +} + +func (s *SafeChannel) UnfreezeChannel() { + if s.freezeFlag { + s.mux.Unlock() + s.freezeFlag = false + } +} diff --git a/bbb-graphql-middleware/internal/common/types.go b/bbb-graphql-middleware/internal/common/types.go index 15bc7e5db5..f3ea4e1c83 100644 --- a/bbb-graphql-middleware/internal/common/types.go +++ b/bbb-graphql-middleware/internal/common/types.go @@ -38,6 +38,7 @@ type BrowserConnection struct { ConnectionInitMessage map[string]interface{} // init message received in this connection (to be used on hasura reconnect) HasuraConnection *HasuraConnection // associated hasura connection Disconnected bool // indicate if the connection is gone + ConnAckSentToBrowser bool // indicate if `connection_ack` msg was already sent to the browser } type HasuraConnection struct { diff --git a/bbb-graphql-middleware/internal/hascli/client.go b/bbb-graphql-middleware/internal/hascli/client.go index 224a066615..5bf445fb1f 100644 --- a/bbb-graphql-middleware/internal/hascli/client.go +++ b/bbb-graphql-middleware/internal/hascli/client.go @@ -67,7 +67,13 @@ func HasuraClient(browserConnection *common.BrowserConnection, cookies []*http.C } browserConnection.HasuraConnection = &thisConnection - defer func() { browserConnection.HasuraConnection = nil }() + defer func() { + browserConnection.HasuraConnection = nil + + //It's necessary to freeze the channel to avoid client trying to start subscriptions before Hasura connection is initialised + //It will unfreeze after `connection_ack` is sent by Hasura + fromBrowserToHasuraChannel.FreezeChannel() + }() // Make the connection c, _, err := websocket.Dial(hasuraConnectionContext, hasuraEndpoint, &dialOptions) @@ -90,16 +96,11 @@ func HasuraClient(browserConnection *common.BrowserConnection, cookies []*http.C // Start routines // reads from browser, writes to hasura - go writer.HasuraConnectionWriter(&thisConnection, fromBrowserToHasuraChannel, &wg) + go writer.HasuraConnectionWriter(&thisConnection, fromBrowserToHasuraChannel, &wg, browserConnection.ConnectionInitMessage) // reads from hasura, writes to browser go reader.HasuraConnectionReader(&thisConnection, fromHasuraToBrowserChannel, fromBrowserToHasuraChannel, &wg) - // if it's a reconnect, inject authentication - if !browserConnection.Disconnected && browserConnection.ConnectionInitMessage != nil { - fromBrowserToHasuraChannel.Send(browserConnection.ConnectionInitMessage) - } - // Wait wg.Wait() diff --git a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go index c66a299c35..07b01089f7 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go +++ b/bbb-graphql-middleware/internal/hascli/conn/reader/reader.go @@ -80,13 +80,22 @@ func HasuraConnectionReader(hc *common.HasuraConnection, fromHasuraToBrowserChan } } - // Write the message to browser - fromHasuraToBrowserChannel.Send(messageAsMap) - // Retransmit the subscription start commands when hasura confirms the connection // this is useful in case of a connection invalidation if messageType == "connection_ack" { + //Hasura connection was initialized, now it's able to send new messages to Hasura + fromBrowserToHasuraChannel.UnfreezeChannel() + + //Avoid to send `connection_ack` to the browser when it's a reconnection + if hc.Browserconn.ConnAckSentToBrowser == false { + fromHasuraToBrowserChannel.Send(messageAsMap) + hc.Browserconn.ConnAckSentToBrowser = true + } + go retransmiter.RetransmitSubscriptionStartMessages(hc, fromBrowserToHasuraChannel) + } else { + // Forward the message to browser + fromHasuraToBrowserChannel.Send(messageAsMap) } } } diff --git a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go index 334bc3c01a..9d7ea23837 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go +++ b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go @@ -12,7 +12,7 @@ import ( // HasuraConnectionWriter // process messages (middleware to hasura) -func HasuraConnectionWriter(hc *common.HasuraConnection, fromBrowserToHasuraChannel *common.SafeChannel, wg *sync.WaitGroup) { +func HasuraConnectionWriter(hc *common.HasuraConnection, fromBrowserToHasuraChannel *common.SafeChannel, wg *sync.WaitGroup, initMessage map[string]interface{}) { log := log.WithField("_routine", "HasuraConnectionWriter") browserConnection := hc.Browserconn @@ -23,6 +23,17 @@ func HasuraConnectionWriter(hc *common.HasuraConnection, fromBrowserToHasuraChan defer hc.ContextCancelFunc() defer log.Debugf("finished") + //Send authentication (init) message at first + //It will not use the channel (fromBrowserToHasuraChannel) because this msg must bypass ChannelFreeze + if initMessage != nil { + log.Infof("it's a reconnection, injecting authentication (init) message") + err := wsjson.Write(hc.Context, hc.Websocket, initMessage) + if err != nil { + log.Errorf("error on write authentication (init) message (we're disconnected from hasura): %v", err) + return + } + } + RangeLoop: for { select { diff --git a/bbb-graphql-middleware/internal/websrv/connhandler.go b/bbb-graphql-middleware/internal/websrv/connhandler.go index 1e25a30cbc..038c755b6a 100644 --- a/bbb-graphql-middleware/internal/websrv/connhandler.go +++ b/bbb-graphql-middleware/internal/websrv/connhandler.go @@ -54,9 +54,10 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) { defer c.Close(websocket.StatusInternalError, "the sky is falling") var thisConnection = common.BrowserConnection{ - Id: browserConnectionId, - ActiveSubscriptions: make(map[string]common.GraphQlSubscription, 1), - Context: browserConnectionContext, + Id: browserConnectionId, + ActiveSubscriptions: make(map[string]common.GraphQlSubscription, 1), + Context: browserConnectionContext, + ConnAckSentToBrowser: false, } BrowserConnectionsMutex.Lock() @@ -97,8 +98,8 @@ func ConnectionHandler(w http.ResponseWriter, r *http.Request) { BrowserConnectionsMutex.RLock() thisBrowserConnection := BrowserConnections[browserConnectionId] BrowserConnectionsMutex.RUnlock() - log.Debugf("created hasura client") if thisBrowserConnection != nil { + log.Debugf("created hasura client") hascli.HasuraClient(thisBrowserConnection, r.Cookies(), fromBrowserToHasuraChannel, fromHasuraToBrowserChannel) } time.Sleep(100 * time.Millisecond) diff --git a/bbb-graphql-middleware/internal/websrv/reader/reader.go b/bbb-graphql-middleware/internal/websrv/reader/reader.go index 9ba7a8aca4..2915ff5381 100644 --- a/bbb-graphql-middleware/internal/websrv/reader/reader.go +++ b/bbb-graphql-middleware/internal/websrv/reader/reader.go @@ -10,14 +10,14 @@ import ( "time" ) -func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromBrowserToHasuraChannel1 *common.SafeChannel, fromBrowserToHasuraChannel2 *common.SafeChannel, waitGroups []*sync.WaitGroup) { +func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c *websocket.Conn, fromBrowserToHasuraChannel *common.SafeChannel, fromBrowserToHasuraConnectionEstablishingChannel *common.SafeChannel, waitGroups []*sync.WaitGroup) { log := log.WithField("_routine", "BrowserConnectionReader").WithField("browserConnectionId", browserConnectionId) defer log.Debugf("finished") log.Debugf("starting") defer func() { - fromBrowserToHasuraChannel1.Close() - fromBrowserToHasuraChannel2.Close() + fromBrowserToHasuraChannel.Close() + fromBrowserToHasuraConnectionEstablishingChannel.Close() }() defer func() { @@ -41,8 +41,9 @@ func BrowserConnectionReader(browserConnectionId string, ctx context.Context, c } log.Tracef("received from browser: %v", v) + //fmt.Println("received from browser: %v", v) - fromBrowserToHasuraChannel1.Send(v) - fromBrowserToHasuraChannel2.Send(v) + fromBrowserToHasuraChannel.Send(v) + fromBrowserToHasuraConnectionEstablishingChannel.Send(v) } } From 6b87c06b4cec1e06e8f35ba4e91c402b2a7b11f9 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Tue, 23 Jan 2024 20:24:09 -0300 Subject: [PATCH 142/512] Add some examples to Graphql client test --- bbb-graphql-client-test/src/Annotations.js | 40 ++++++++++--------- bbb-graphql-client-test/src/Auth.js | 25 +----------- bbb-graphql-client-test/src/CursorsStream.js | 3 +- .../src/UserConnectionStatus.js | 2 +- bbb-graphql-client-test/src/UserList.js | 30 +++++++++++--- 5 files changed, 52 insertions(+), 48 deletions(-) diff --git a/bbb-graphql-client-test/src/Annotations.js b/bbb-graphql-client-test/src/Annotations.js index a2531380dc..90e4f8dcd9 100644 --- a/bbb-graphql-client-test/src/Annotations.js +++ b/bbb-graphql-client-test/src/Annotations.js @@ -2,20 +2,24 @@ import {useSubscription, gql, useQuery} from '@apollo/client'; import React, { useState } from "react"; import usePatchedSubscription from "./usePatchedSubscription"; +export const CURRENT_PAGE_ANNOTATIONS_STREAM = gql`subscription annotationsStream($lastUpdatedAt: timestamptz){ + pres_annotation_curr_stream(batch_size: 10, cursor: {initial_value: {lastUpdatedAt: $lastUpdatedAt}}) { + annotationId annotationInfo pageId + presentationId + userId + } +}`; + export default function Annotations() { - const { loading, error, data } = usePatchedSubscription( - gql`subscription { - pres_annotation_curr_stream(batch_size: 10, cursor: {initial_value: {lastUpdatedAt: "\\"2023-03-29T20:26:29.002\\""}}) { - annotationId - annotationInfo - lastUpdatedAt - pageId - presentationId - userId - } - } - ` - ); + const lastUpdatedAt = "2023-03-29T20:26:29.002"; + + const { loading, error, data } = useSubscription( + CURRENT_PAGE_ANNOTATIONS_STREAM, + { + variables: { lastUpdatedAt }, + }, + ); + return !loading && !error && (
Private Chat MessagesCursor
userIdIdName extIddurationdurationInSeconds
{user.userId}{curr.name} {curr.extId}{curr.duration}{curr.durationInSeconds}
IduserId namejoinedStatus joinErrorCode joinErrorMessage
{curr.userId} {curr.name}{curr.joined ? 'Yes' : 'No'} - {curr.joined ? '' : } + {curr.joined && !curr.loggedOut && !curr.ejected ? 'joined' : ''} + {curr.loggedOut ? 'loggedOut' : ''} + {curr.ejected ? 'ejected' : ''} + {!curr.joined && !curr.loggedOut ? : ''} + {curr.joined && !curr.loggedOut && !curr.ejected ? : ''} {curr.joinErrorCode} {curr.joinErrorMessage} {user?.connectionStatus?.connectionAliveAt} {user.disconnected === true ? 'Yes' : 'No'}{user.loggedOut === true ? 'Yes' : 'No'}{user.loggedOut === true ? 'Yes' : 'No'} + {user.isModerator ? : ''} +
{JSON.stringify(curr.payloadJson)} {JSON.stringify(curr.toRoles)} {curr.createdAt} + +
+ + handleCheckboxChange(option.optionId)} + checked={checkedAnswers.includes(option.optionId)} + ariaLabelledBy={`pollAnswerLabel${option.optionDesc}`} + ariaDescribedBy={`pollAnswerDesc${option.optionDesc}`} + /> + +
@@ -24,24 +28,24 @@ export default function Annotations() { + - - - {data.map((curr) => { + + {data.pres_annotation_curr_stream.map((curr) => { console.log('pres_annotation_curr_stream', curr); return ( {/**/} + - ); })} - +
Annotations Stream (Full object)
lastUpdatedAt annotationId annotationInfolastUpdatedAt
{user.userId}{curr.lastUpdatedAt} {curr.annotationId} {curr.annotationInfo}{curr.lastUpdatedAt}
); } diff --git a/bbb-graphql-client-test/src/Auth.js b/bbb-graphql-client-test/src/Auth.js index 3c9965b596..e5099f15b1 100644 --- a/bbb-graphql-client-test/src/Auth.js +++ b/bbb-graphql-client-test/src/Auth.js @@ -78,6 +78,7 @@ export default function Auth() { loggedOut ejected isOnline + isModerator joined joinErrorCode joinErrorMessage @@ -93,12 +94,6 @@ export default function Auth() { }` ); - console.log("data"); - console.log(data); - console.log("error"); - console.log(error); - console.log(loading); - if(!loading && !error) { if(!data.hasOwnProperty('user_current') || @@ -156,10 +151,9 @@ export default function Auth() {
- -
+

@@ -190,21 +184,6 @@ export default function Auth() {
- - // return ( - // - // {curr.userId} - // {curr.name} - // {curr.joined && !curr.loggedOut && !curr.ejected ? 'joined' : ''} - // {curr.loggedOut ? 'loggedOut' : ''} - // {curr.ejected ? 'ejected' : ''} - // {!curr.joined && !curr.loggedOut ? : ''} - // {curr.joined && !curr.loggedOut && !curr.ejected ? : ''} - // - // {curr.joinErrorCode} - // {curr.joinErrorMessage} - // - // ); } diff --git a/bbb-graphql-client-test/src/CursorsStream.js b/bbb-graphql-client-test/src/CursorsStream.js index 31cba461d2..7cef428f29 100644 --- a/bbb-graphql-client-test/src/CursorsStream.js +++ b/bbb-graphql-client-test/src/CursorsStream.js @@ -3,8 +3,9 @@ import {useSubscription, gql, useQuery} from '@apollo/client'; export default function CursorsStream() { const { loading, error, data } = useSubscription( + //2023-03-29T20:26:29.002 gql`subscription { - pres_page_cursor_stream(batch_size: 10, cursor: {initial_value: {lastUpdatedAt: "\\"2023-03-29T20:26:29.002\\""}}) { + pres_page_cursor_stream(batch_size: 10, cursor: { initial_value: { lastUpdatedAt: "2024-01-20T13:12:20.945+00:00" } }) { isCurrentPage lastUpdatedAt pageId diff --git a/bbb-graphql-client-test/src/UserConnectionStatus.js b/bbb-graphql-client-test/src/UserConnectionStatus.js index cc5ed9c404..a949891764 100644 --- a/bbb-graphql-client-test/src/UserConnectionStatus.js +++ b/bbb-graphql-client-test/src/UserConnectionStatus.js @@ -90,7 +90,7 @@ export default function UserConnectionStatus() { {data.user_connectionStatus.map((curr) => { - console.log('user_connectionStatus', curr); + // console.log('user_connectionStatus', curr); if(curr.userClientResponseAt == null) { // handleUpdateUserClientResponseAt(); diff --git a/bbb-graphql-client-test/src/UserList.js b/bbb-graphql-client-test/src/UserList.js index 64bdaafccb..97ccf7fdfb 100644 --- a/bbb-graphql-client-test/src/UserList.js +++ b/bbb-graphql-client-test/src/UserList.js @@ -14,12 +14,12 @@ const ParentOfUserList = ({user}) => { setShouldRender(e.target.checked); } }> - {shouldRender && } + {shouldRender && } ); } -function UserList({userId}) { +function UserList({myUser}) { const [dispatchUserEject] = useMutation(gql` mutation UserEject($userId: String!) { @@ -38,6 +38,22 @@ function UserList({userId}) { }); }; + const [dispatchUserSetPresenter] = useMutation(gql` + mutation UserEject($userId: String!) { + userSetPresenter( + userId: $userId + ) + } + `); + + const handleDispatchUserSetPresenter = (userId) => { + dispatchUserSetPresenter({ + variables: { + userId: userId, + }, + }); + }; + const { loading, error, data } = usePatchedSubscription( gql`subscription { user(limit: 50, order_by: [ @@ -113,17 +129,20 @@ function UserList({userId}) { {data.map((user) => { - console.log('user', user); + // console.log('user', user); return ( {/*{user.userId}*/}
{user.name}
+ {myUser.userId == user.userId ? (You!) : ''} {user.role} {user.emoji} {user.avatar} - {user.presenter === true ? 'Yes' : 'No'} + {user.presenter === true ? 'Yes' : 'No'} + {myUser.isModerator && !user.presenter ? : ''} + {user.mobile === true ? 'Yes' : 'No'} {user.clientType} 0 ? '#A0DAA9' : ''}}>{user.cameras.length > 0 ? 'Yes' : 'No'} @@ -140,7 +159,8 @@ function UserList({userId}) { {user?.connectionStatus?.connectionAliveAt} {user.disconnected === true ? 'Yes' : 'No'} {user.loggedOut === true ? 'Yes' : 'No'} - {user.isModerator ? : ''} +
+ {myUser.isModerator ? : ''} ); From caeebfd109012f4b04bca232a5d37059c2b82b57 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Tue, 23 Jan 2024 20:47:43 -0300 Subject: [PATCH 143/512] Make graphql-action to send annotations expect type json instead of string --- bbb-graphql-server/metadata/actions.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bbb-graphql-server/metadata/actions.graphql b/bbb-graphql-server/metadata/actions.graphql index cd01bfb053..6277092c23 100644 --- a/bbb-graphql-server/metadata/actions.graphql +++ b/bbb-graphql-server/metadata/actions.graphql @@ -276,7 +276,7 @@ type Mutation { type Mutation { presAnnotationSubmit( pageId: String! - annotations: [String]! + annotations: json! ): Boolean } From d3b2242c7f15485f3afc6052b4e87552722d3091 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Tue, 23 Jan 2024 20:55:20 -0300 Subject: [PATCH 144/512] Validate if annotations is an valid Array --- bbb-graphql-actions/src/actions/presAnnotationSubmit.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bbb-graphql-actions/src/actions/presAnnotationSubmit.ts b/bbb-graphql-actions/src/actions/presAnnotationSubmit.ts index 803538e173..a9129c93ae 100644 --- a/bbb-graphql-actions/src/actions/presAnnotationSubmit.ts +++ b/bbb-graphql-actions/src/actions/presAnnotationSubmit.ts @@ -17,6 +17,10 @@ export default function buildRedisMessage(sessionVariables: Record Date: Wed, 24 Jan 2024 08:37:51 -0300 Subject: [PATCH 145/512] migrate sendBulkAnnotations action --- .../imports/api/annotations/server/index.js | 1 - .../imports/api/annotations/server/methods.js | 8 ----- .../server/methods/sendAnnotationHelper.js | 34 ------------------- .../server/methods/sendAnnotations.js | 17 ---------- .../server/methods/sendBulkAnnotations.js | 22 ------------ .../ui/components/presentation/mutations.jsx | 10 ++++++ .../ui/components/whiteboard/component.jsx | 10 +++--- .../ui/components/whiteboard/container.jsx | 22 ++++++++++-- .../ui/components/whiteboard/service.js | 14 ++++---- .../imports/ui/components/whiteboard/utils.js | 6 ---- bigbluebutton-html5/server/main.js | 1 - 11 files changed, 41 insertions(+), 104 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/annotations/server/index.js delete mode 100644 bigbluebutton-html5/imports/api/annotations/server/methods.js delete mode 100755 bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js delete mode 100755 bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotations.js delete mode 100644 bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js diff --git a/bigbluebutton-html5/imports/api/annotations/server/index.js b/bigbluebutton-html5/imports/api/annotations/server/index.js deleted file mode 100644 index ba55c4a16e..0000000000 --- a/bigbluebutton-html5/imports/api/annotations/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import './methods'; diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods.js b/bigbluebutton-html5/imports/api/annotations/server/methods.js deleted file mode 100644 index a2721ec9e9..0000000000 --- a/bigbluebutton-html5/imports/api/annotations/server/methods.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import sendAnnotations from './methods/sendAnnotations'; -import sendBulkAnnotations from './methods/sendBulkAnnotations'; - -Meteor.methods({ - sendAnnotations, - sendBulkAnnotations, -}); diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js deleted file mode 100755 index 3b39236edd..0000000000 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotationHelper.js +++ /dev/null @@ -1,34 +0,0 @@ -import RedisPubSub from '/imports/startup/server/redis'; -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import Logger from '/imports/startup/server/logger'; - -export default function sendAnnotationHelper(annotations, meetingId, requesterUserId) { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'SendWhiteboardAnnotationsPubMsg'; - - try { - check(annotations, Array); - // TODO see if really necessary, don't know if it's possible - // to have annotations from different pages - // group annotations by same whiteboardId - const groupedAnnotations = annotations.reduce((r, v, i, a, k = v.wbId) => ((r[k] || (r[k] = [])).push(v), r), {}) //groupBy wbId - - Object.entries(groupedAnnotations).forEach(([_, whiteboardAnnotations]) => { - const whiteboardId = whiteboardAnnotations[0].wbId; - check(whiteboardId, String); - - const payload = { - whiteboardId, - annotations: whiteboardAnnotations, - html5InstanceId: parseInt(process.env.INSTANCE_ID, 10) || 1, - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - }); - - } catch (err) { - Logger.error(`Exception while invoking method sendAnnotationHelper ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotations.js b/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotations.js deleted file mode 100755 index 530515f592..0000000000 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/sendAnnotations.js +++ /dev/null @@ -1,17 +0,0 @@ -import { check } from 'meteor/check'; -import sendAnnotationHelper from './sendAnnotationHelper'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import Logger from '/imports/startup/server/logger'; - -export default function sendAnnotations(annotations) { - try { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - - sendAnnotationHelper(annotations, meetingId, requesterUserId); - } catch (err) { - Logger.error(`Exception while invoking method sendAnnotation ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js b/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js deleted file mode 100644 index a650f6f91f..0000000000 --- a/bigbluebutton-html5/imports/api/annotations/server/methods/sendBulkAnnotations.js +++ /dev/null @@ -1,22 +0,0 @@ -import { extractCredentials } from '/imports/api/common/server/helpers'; -import sendAnnotationHelper from './sendAnnotationHelper'; -import { check } from 'meteor/check'; -import Logger from '/imports/startup/server/logger'; - -export default function sendBulkAnnotations(payload) { - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - try { - check(meetingId, String); - check(requesterUserId, String); - - console.log("!!!!!!! sendBulkAnnotations!!!!:", payload) - - sendAnnotationHelper(payload, meetingId, requesterUserId); - //payload.forEach((annotation) => sendAnnotationHelper(annotation, meetingId, requesterUserId)); - return true; - } catch (err) { - Logger.error(`Exception while invoking method sendBulkAnnotations ${err.stack}`); - return false; - } -} diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index 4a6d9aecfb..79910f22df 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -70,6 +70,15 @@ export const PRES_ANNOTATION_DELETE = gql` } `; +export const PRES_ANNOTATION_SUBMIT = gql` + mutation PresAnnotationSubmit($pageId: String!, $annotations: json!) { + presAnnotationSubmit( + pageId: $pageId, + annotations: $annotations, + ) + } +`; + export default { PRESENTATION_SET_ZOOM, PRESENTATION_SET_WRITERS, @@ -78,4 +87,5 @@ export default { PRESENTATION_SET_CURRENT, PRESENTATION_REMOVE, PRES_ANNOTATION_DELETE, + PRES_ANNOTATION_SUBMIT, }; diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index a06223b32f..cf4c69f4a7 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -99,7 +99,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) { isPresenter, removeShapes, initDefaultPages, - persistShape, + persistShapeWrapper, shapes, assets, currentUser, @@ -667,7 +667,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) { console.log("EDITOR : ", editor); - const debouncePersistShape = debounce({ delay: 0 }, persistShape); + const debouncePersistShape = debounce({ delay: 0 }, persistShapeWrapper); const colorStyles = ['black', 'blue', 'green', 'grey', 'light-blue', 'light-green', 'light-red', 'light-violet', 'orange', 'red', 'violet', 'yellow']; const dashStyles = ['dashed', 'dotted', 'draw', 'solid']; @@ -704,7 +704,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) { createdBy: currentUser?.userId, } }; - persistShape(updatedRecord, whiteboardId, isModerator); + persistShapeWrapper(updatedRecord, whiteboardId, isModerator); }); Object.values(updated).forEach(([_, record]) => { @@ -715,7 +715,7 @@ export default Whiteboard = React.memo(function Whiteboard(props) { createdBy: shapes[record?.id]?.meta?.createdBy, } }; - persistShape(updatedRecord, whiteboardId, isModerator); + persistShapeWrapper(updatedRecord, whiteboardId, isModerator); }); Object.values(removed).forEach((record) => { @@ -889,7 +889,7 @@ Whiteboard.propTypes = { isIphone: PropTypes.bool.isRequired, removeShapes: PropTypes.func.isRequired, initDefaultPages: PropTypes.func.isRequired, - persistShape: PropTypes.func.isRequired, + persistShapeWrapper: PropTypes.func.isRequired, notifyNotAllowedChange: PropTypes.func.isRequired, shapes: PropTypes.objectOf(PropTypes.shape).isRequired, assets: PropTypes.objectOf(PropTypes.shape).isRequired, diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index cf3b3fa3f9..4adba20bc4 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -32,7 +32,11 @@ import useMeeting from '/imports/ui/core/hooks/useMeeting'; import { AssetRecordType, } from "@tldraw/tldraw"; -import { PRESENTATION_SET_ZOOM, PRES_ANNOTATION_DELETE } from '../presentation/mutations'; +import { + PRESENTATION_SET_ZOOM, + PRES_ANNOTATION_DELETE, + PRES_ANNOTATION_SUBMIT, +} from '../presentation/mutations'; const WHITEBOARD_CONFIG = Meteor.settings.public.whiteboard; @@ -62,6 +66,7 @@ const WhiteboardContainer = (props) => { const [presentationSetZoom] = useMutation(PRESENTATION_SET_ZOOM); const [presentationDeleteAnnotations] = useMutation(PRES_ANNOTATION_DELETE); + const [presentationSubmitAnnotations] = useMutation(PRES_ANNOTATION_SUBMIT); const removeShapes = (shapeIds) => { presentationDeleteAnnotations({ @@ -88,6 +93,19 @@ const WhiteboardContainer = (props) => { }); }; + const submitAnnotations = async (newAnnotations) => { + await presentationSubmitAnnotations({ + variables: { + pageId: currentPresentationPage?.pageId, + annotations: newAnnotations, + }, + }); + }; + + const persistShapeWrapper = (shape, whiteboardId, isModerator) => { + persistShape(shape, whiteboardId, isModerator, submitAnnotations); + }; + const isMultiUserActive = whiteboardWriters?.length > 0; const { data: pollData } = useSubscription(POLL_RESULTS_SUBSCRIPTION); @@ -240,7 +258,7 @@ const WhiteboardContainer = (props) => { sidebarNavigationWidth, layoutContextDispatch, initDefaultPages, - persistShape, + persistShapeWrapper, isMultiUserActive, shapes, bgShape, diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js index 1be7fb562f..7eb10cd5ee 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js @@ -1,11 +1,9 @@ import Auth from '/imports/ui/services/auth'; import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user'; -import { makeCall } from '/imports/ui/services/api'; import PollService from '/imports/ui/components/poll/service'; import { defineMessages } from 'react-intl'; import { notify } from '/imports/ui/services/notification'; import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer'; -import { getTextSize } from './utils'; const intlMessages = defineMessages({ notifyNotAllowedChange: { @@ -30,7 +28,7 @@ const annotationsRetryDelay = 1000; let annotationsSenderIsRunning = false; -const proccessAnnotationsQueue = async () => { +const proccessAnnotationsQueue = async (submitAnnotations) => { annotationsSenderIsRunning = true; const queueSize = annotationsQueue.length; @@ -41,7 +39,7 @@ const proccessAnnotationsQueue = async () => { const annotations = annotationsQueue.splice(0, queueSize); - const isAnnotationSent = await makeCall('sendBulkAnnotations', annotations); + const isAnnotationSent = await submitAnnotations(annotations); if (!isAnnotationSent) { // undo splice @@ -58,7 +56,7 @@ const proccessAnnotationsQueue = async () => { } }; -const sendAnnotation = (annotation) => { +const sendAnnotation = (annotation, submitAnnotations) => { // Prevent sending annotations while disconnected // TODO: Change this to add the annotation, but delay the send until we're // reconnected. With this it will miss things @@ -70,7 +68,7 @@ const sendAnnotation = (annotation) => { } else { annotationsQueue.push(annotation); } - if (!annotationsSenderIsRunning) setTimeout(proccessAnnotationsQueue, annotationsBufferTimeMin); + if (!annotationsSenderIsRunning) setTimeout(() => proccessAnnotationsQueue(submitAnnotations), annotationsBufferTimeMin); }; const getMultiUser = (whiteboardId) => { @@ -87,7 +85,7 @@ const getMultiUser = (whiteboardId) => { return data.multiUser; }; -const persistShape = (shape, whiteboardId, isModerator) => { +const persistShape = (shape, whiteboardId, isModerator, submitAnnotations) => { const annotation = { id: shape.id, annotationInfo: { ...shape, isModerator }, @@ -95,7 +93,7 @@ const persistShape = (shape, whiteboardId, isModerator) => { userId: Auth.userID, }; - sendAnnotation(annotation); + sendAnnotation(annotation, submitAnnotations); }; const initDefaultPages = (count = 1) => { diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js b/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js index f2fdc45d5c..ca2965bf3b 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js @@ -1,10 +1,4 @@ import React from 'react'; -import { isEqual } from 'radash'; -import { - persistShape, - notifyNotAllowedChange, - notifyShapeNumberExceeded, -} from './service'; const WHITEBOARD_CONFIG = Meteor.settings.public.whiteboard; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js index 1a33749f2f..c66e0f42de 100755 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -3,7 +3,6 @@ import '/imports/startup/server'; // 2x import '/imports/api/meetings/server'; import '/imports/api/users/server'; -import '/imports/api/annotations/server'; import '/imports/api/cursor/server'; import '/imports/api/polls/server'; import '/imports/api/captions/server'; From b35833c25c6a84271ea6a883a92ff25deadf5eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Wed, 24 Jan 2024 09:18:42 -0300 Subject: [PATCH 146/512] fix send annotation --- .../ui/components/whiteboard/container.jsx | 4 ++- .../ui/components/whiteboard/service.js | 31 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index f196b731f0..5199368d40 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -97,12 +97,14 @@ const WhiteboardContainer = (props) => { }; const submitAnnotations = async (newAnnotations) => { - await presentationSubmitAnnotations({ + const isAnnotationSent = await presentationSubmitAnnotations({ variables: { pageId: currentPresentationPage?.pageId, annotations: newAnnotations, }, }); + + return isAnnotationSent?.data?.presAnnotationSubmit; }; const persistShapeWrapper = (shape, whiteboardId, isModerator) => { diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js index 7eb10cd5ee..db38ebb0af 100755 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/service.js +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/service.js @@ -39,20 +39,25 @@ const proccessAnnotationsQueue = async (submitAnnotations) => { const annotations = annotationsQueue.splice(0, queueSize); - const isAnnotationSent = await submitAnnotations(annotations); + try { + const isAnnotationSent = await submitAnnotations(annotations); - if (!isAnnotationSent) { - // undo splice + if (!isAnnotationSent) { + // undo splice + annotationsQueue.splice(0, 0, ...annotations); + setTimeout(() => proccessAnnotationsQueue(submitAnnotations), annotationsRetryDelay); + } else { + // ask tiago + const delayPerc = Math.min( + annotationsMaxDelayQueueSize, queueSize, + ) / annotationsMaxDelayQueueSize; + const delayDelta = annotationsBufferTimeMax - annotationsBufferTimeMin; + const delayTime = annotationsBufferTimeMin + delayDelta * delayPerc; + setTimeout(() => proccessAnnotationsQueue(submitAnnotations), delayTime); + } + } catch (error) { annotationsQueue.splice(0, 0, ...annotations); - setTimeout(proccessAnnotationsQueue, annotationsRetryDelay); - } else { - // ask tiago - const delayPerc = Math.min( - annotationsMaxDelayQueueSize, queueSize, - ) / annotationsMaxDelayQueueSize; - const delayDelta = annotationsBufferTimeMax - annotationsBufferTimeMin; - const delayTime = annotationsBufferTimeMin + delayDelta * delayPerc; - setTimeout(proccessAnnotationsQueue, delayTime); + setTimeout(() => proccessAnnotationsQueue(submitAnnotations), annotationsRetryDelay); } }; @@ -85,7 +90,7 @@ const getMultiUser = (whiteboardId) => { return data.multiUser; }; -const persistShape = (shape, whiteboardId, isModerator, submitAnnotations) => { +const persistShape = async (shape, whiteboardId, isModerator, submitAnnotations) => { const annotation = { id: shape.id, annotationInfo: { ...shape, isModerator }, From cd03c116b0e66b907f1c300b52ca8d754f2980d0 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Wed, 24 Jan 2024 11:33:38 -0300 Subject: [PATCH 147/512] Add configs from /enter to Graphql --- .../bigbluebutton/core/db/MeetingDAO.scala | 21 +++++++++++++++++++ .../common2/domain/Meeting2x.scala | 3 +++ .../org/bigbluebutton/api/MeetingService.java | 4 ++-- .../bigbluebutton/api2/IBbbWebApiGWApp.java | 3 +++ .../bigbluebutton/api2/BbbWebApiGWApp.scala | 14 ++++++++++++- bbb-graphql-server/bbb_schema.sql | 3 +++ .../tables/public_v_meeting.yaml | 7 +++++++ .../meetings/server/modifiers/addMeeting.js | 3 +++ 8 files changed, 55 insertions(+), 3 deletions(-) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/MeetingDAO.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/MeetingDAO.scala index ed8e9362da..08b0f0ccdd 100644 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/MeetingDAO.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/db/MeetingDAO.scala @@ -20,6 +20,9 @@ case class MeetingDbModel( presentationUploadExternalUrl: String, learningDashboardAccessToken: String, logoutUrl: String, + customLogoUrl: Option[String], + bannerText: Option[String], + bannerColor: Option[String], createdTime: Long, durationInSeconds: Int ) @@ -38,6 +41,9 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet presentationUploadExternalUrl, learningDashboardAccessToken, logoutUrl, + customLogoUrl, + bannerText, + bannerColor, createdTime, durationInSeconds ) <> (MeetingDbModel.tupled, MeetingDbModel.unapply) @@ -53,6 +59,9 @@ class MeetingDbTableDef(tag: Tag) extends Table[MeetingDbModel](tag, None, "meet val presentationUploadExternalUrl = column[String]("presentationUploadExternalUrl") val learningDashboardAccessToken = column[String]("learningDashboardAccessToken") val logoutUrl = column[String]("logoutUrl") + val customLogoUrl = column[Option[String]]("customLogoUrl") + val bannerText = column[Option[String]]("bannerText") + val bannerColor = column[Option[String]]("bannerColor") val createdTime = column[Long]("createdTime") val durationInSeconds = column[Int]("durationInSeconds") } @@ -74,6 +83,18 @@ object MeetingDAO { presentationUploadExternalUrl = meetingProps.meetingProp.presentationUploadExternalUrl, learningDashboardAccessToken = meetingProps.password.learningDashboardAccessToken, logoutUrl = meetingProps.systemProps.logoutUrl, + customLogoUrl = meetingProps.systemProps.customLogoURL match { + case "" => None + case logoUrl => Some(logoUrl) + }, + bannerText = meetingProps.systemProps.bannerText match { + case "" => None + case bannerText => Some(bannerText) + }, + bannerColor = meetingProps.systemProps.bannerColor match { + case "" => None + case bannerColor => Some(bannerColor) + }, createdTime = meetingProps.durationProps.createdTime, durationInSeconds = meetingProps.durationProps.duration * 60 ) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala index c729d6be73..17c03e9d05 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/domain/Meeting2x.scala @@ -70,6 +70,9 @@ case class LockSettingsProps( case class SystemProps( html5InstanceId: Int, logoutUrl: String, + customLogoURL: String, + bannerText: String, + bannerColor: String, ) case class GroupProps( diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java index 92a860ec3d..7961d705d4 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/MeetingService.java @@ -411,8 +411,8 @@ public class MeetingService implements MessageListener { m.getUserInactivityInspectTimerInMinutes(), m.getUserInactivityThresholdInMinutes(), m.getUserActivitySignResponseDelayInMinutes(), m.getEndWhenNoModerator(), m.getEndWhenNoModeratorDelayInMinutes(), m.getMuteOnStart(), m.getAllowModsToUnmuteUsers(), m.getAllowModsToEjectCameras(), m.getMeetingKeepEvents(), - m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(), m.getLogoutUrl(), - m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(), + m.breakoutRoomsParams, m.lockSettingsParams, m.getHtml5InstanceId(), m.getLogoutUrl(), m.getCustomLogoURL(), + m.getBannerText(), m.getBannerColor(), m.getGroups(), m.getDisabledFeatures(), m.getNotifyRecordingIsOn(), m.getPresentationUploadExternalDescription(), m.getPresentationUploadExternalUrl(), m.getOverrideClientSettings()); } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java index a9462f501e..f530eaba61 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api2/IBbbWebApiGWApp.java @@ -43,6 +43,9 @@ public interface IBbbWebApiGWApp { LockSettingsParams lockSettingsParams, Integer html5InstanceId, String logoutUrl, + String customLogoURL, + String bannerText, + String bannerColor, ArrayList groups, ArrayList disabledFeatures, Boolean notifyRecordingIsOn, diff --git a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala index eafd968e2f..41af48826e 100755 --- a/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala +++ b/bbb-common-web/src/main/scala/org/bigbluebutton/api2/BbbWebApiGWApp.scala @@ -150,6 +150,9 @@ class BbbWebApiGWApp( lockSettingsParams: LockSettingsParams, html5InstanceId: java.lang.Integer, logoutUrl: String, + customLogoURL: String, + bannerText: String, + bannerColor: String, groups: java.util.ArrayList[Group], disabledFeatures: java.util.ArrayList[String], notifyRecordingIsOn: java.lang.Boolean, @@ -232,7 +235,16 @@ class BbbWebApiGWApp( val systemProps = SystemProps( html5InstanceId, - logoutUrl + logoutUrl, + customLogoURL, + bannerText match { + case t: String => t + case _ => "" + }, + bannerColor match { + case c: String => c + case _ => "" + }, ) val groupsAsVector: Vector[GroupProps] = groups.asScala.toVector.map(g => GroupProps(g.getGroupId(), g.getName(), g.getUsersExtId().asScala.toVector)) diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index 9e59dc9520..4c20e85869 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -21,6 +21,9 @@ create table "meeting" ( "learningDashboardAccessToken" varchar(100), "html5InstanceId" varchar(100), "logoutUrl" varchar(500), + "customLogoUrl" varchar(500), + "bannerText" text, + "bannerColor" varchar(50), "createdTime" bigint, "durationInSeconds" integer ); diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_meeting.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_meeting.yaml index 2edd87193b..a56af019ae 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_meeting.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_meeting.yaml @@ -138,13 +138,17 @@ select_permissions: - role: bbb_client permission: columns: + - bannerColor + - bannerText - createdAt - createdTime + - customLogoUrl - disabledFeatures - durationInSeconds - extId - html5InstanceId - isBreakout + - logoutUrl - maxPinnedCameras - meetingCameraCap - meetingId @@ -158,6 +162,9 @@ select_permissions: - role: pre_join_bbb_client permission: columns: + - bannerColor + - bannerText + - customLogoUrl - logoutUrl - meetingId - name diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js index c0c794bf25..18ad2e112c 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/addMeeting.js @@ -132,6 +132,9 @@ export default async function addMeeting(meeting) { systemProps: { html5InstanceId: Number, logoutUrl: String, + customLogoURL: String, + bannerText: String, + bannerColor: String, }, groups: Array, overrideClientSettings: String, From f839fd8578a3841a6ff69ce87661d58eab30559c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Wed, 24 Jan 2024 13:19:51 -0300 Subject: [PATCH 148/512] fix(whiteboard): prevent annotation subscription from being recreated --- .../ui/components/whiteboard/container.jsx | 71 +++++++------------ 1 file changed, 27 insertions(+), 44 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index 26d8b07ed3..088a0fe71e 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -1,8 +1,7 @@ -import React from 'react'; -import { useQuery, useSubscription, useMutation } from '@apollo/client'; +import React, { useEffect, useState } from 'react'; +import { useSubscription, useMutation } from '@apollo/client'; import { CURRENT_PRESENTATION_PAGE_SUBSCRIPTION, - CURRENT_PAGE_ANNOTATIONS_QUERY, CURRENT_PAGE_ANNOTATIONS_STREAM, CURRENT_PAGE_WRITERS_SUBSCRIPTION, } from './queries'; @@ -39,9 +38,6 @@ import { PRESENTATION_SET_ZOOM } from '../presentation/mutations'; const WHITEBOARD_CONFIG = Meteor.settings.public.whiteboard; -let annotations = []; -let lastUpdatedAt = null; - const WhiteboardContainer = (props) => { const { intl, @@ -49,6 +45,8 @@ const WhiteboardContainer = (props) => { svgUri, } = props; + const [annotations, setAnnotations] = useState([]); + const meeting = useMeeting((m) => ({ lockSettings: m?.lockSettings, })); @@ -98,53 +96,38 @@ const WhiteboardContainer = (props) => { const { data: cursorData } = useSubscription(CURSOR_SUBSCRIPTION); const { pres_page_cursor: cursorArray } = (cursorData || []); - const { - loading: annotationsLoading, - data: annotationsData, - } = useQuery(CURRENT_PAGE_ANNOTATIONS_QUERY); - const { pres_annotation_curr: history } = (annotationsData || []); - - const lastHistoryTime = history?.[0]?.lastUpdatedAt || null; - - if (!lastUpdatedAt) { - if (lastHistoryTime) { - if (new Date(lastUpdatedAt).getTime() < new Date(lastHistoryTime).getTime()) { - lastUpdatedAt = lastHistoryTime; - } - } else { - const newLastUpdatedAt = new Date(); - lastUpdatedAt = newLastUpdatedAt.toISOString(); - } - } - - const { data: streamData } = useSubscription( + const { data: annotationStreamData, loading: annotationStreamLoading } = useSubscription( CURRENT_PAGE_ANNOTATIONS_STREAM, { - variables: { lastUpdatedAt }, + variables: { lastUpdatedAt: new Date(0).toISOString() }, }, ); - const { pres_annotation_curr_stream: streamDataItem } = (streamData || []); - if (streamDataItem) { - if (new Date(lastUpdatedAt).getTime() < new Date(streamDataItem[0].lastUpdatedAt).getTime()) { - if (streamDataItem[0].annotationInfo === '') { - // remove shape - annotations = annotations.filter( - (annotation) => annotation.annotationId !== streamDataItem[0].annotationId, - ); - } else { - // add shape - annotations = annotations.concat(streamDataItem); - } - lastUpdatedAt = streamDataItem[0].lastUpdatedAt; + useEffect(() => { + const { pres_annotation_curr_stream: annotationStream } = annotationStreamData || {}; + + if (annotationStream) { + const newAnnotations = []; + const annotationsToBeRemoved = []; + annotationStream.forEach((item) => { + if (item.annotationInfo === '') { + annotationsToBeRemoved.push(item.annotationId); + } else { + newAnnotations.push(item); + } + }); + const currentAnnotations = annotations.filter( + (annotation) => !annotationsToBeRemoved.includes(annotation.annotationId), + ); + setAnnotations([...currentAnnotations, ...newAnnotations]); } - } + }, [annotationStreamData]); + let shapes = {}; let bgShape = []; - if (!annotationsLoading && history) { - const pageAnnotations = history - .concat(annotations) + if (!annotationStreamLoading) { + const pageAnnotations = annotations .filter((annotation) => annotation.pageId === currentPresentationPage?.pageId); shapes = formatAnnotations(pageAnnotations, intl, curPageId, pollResults, currentPresentationPage); From 57e5dcc54a18e35b480e9f57e7a2127e671dfd1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Wed, 24 Jan 2024 13:53:01 -0300 Subject: [PATCH 149/512] fix: remove unnecessary condition --- .../ui/components/whiteboard/container.jsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx index 088a0fe71e..9f11ce11a9 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/container.jsx @@ -96,7 +96,7 @@ const WhiteboardContainer = (props) => { const { data: cursorData } = useSubscription(CURSOR_SUBSCRIPTION); const { pres_page_cursor: cursorArray } = (cursorData || []); - const { data: annotationStreamData, loading: annotationStreamLoading } = useSubscription( + const { data: annotationStreamData } = useSubscription( CURRENT_PAGE_ANNOTATIONS_STREAM, { variables: { lastUpdatedAt: new Date(0).toISOString() }, @@ -126,12 +126,16 @@ const WhiteboardContainer = (props) => { let shapes = {}; let bgShape = []; - if (!annotationStreamLoading) { - const pageAnnotations = annotations - .filter((annotation) => annotation.pageId === currentPresentationPage?.pageId); + const pageAnnotations = annotations + .filter((annotation) => annotation.pageId === currentPresentationPage?.pageId); - shapes = formatAnnotations(pageAnnotations, intl, curPageId, pollResults, currentPresentationPage); - } + shapes = formatAnnotations( + pageAnnotations, + intl, + curPageId, + pollResults, + currentPresentationPage, + ); const { isIphone } = deviceInfo; From 9b2b52804193abadf5edad28fd61ae3c0f06cd2d Mon Sep 17 00:00:00 2001 From: Gabriel Porfirio Date: Wed, 24 Jan 2024 14:49:03 -0300 Subject: [PATCH 150/512] fixing 2 options test and 1 poll test --- .../playwright/core/elements.js | 2 +- .../playwright/options/options.js | 5 ++++- ...oderator-page-dark-mode-Chromium-linux.png | Bin 117482 -> 185090 bytes ...oderator-page-font-size-Chromium-linux.png | Bin 125651 -> 195919 bytes .../playwright/polling/poll.js | 2 ++ 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-tests/playwright/core/elements.js b/bigbluebutton-tests/playwright/core/elements.js index c1c83f333f..b35191f428 100644 --- a/bigbluebutton-tests/playwright/core/elements.js +++ b/bigbluebutton-tests/playwright/core/elements.js @@ -468,7 +468,7 @@ exports.wbStickyNoteShape = 'button[data-testid="tools.note"]'; exports.wbTextShape = 'button[data-testid="tools.text"]'; exports.wbTypedText = 'div[data-shape="text"]'; exports.wbTypedStickyNote = 'div[data-shape="sticky"]'; -exports.wbDrawnRectangle = 'div[data-shape="rectangle"]'; +exports.wbDrawnRectangle = 'div[data-shape-type="geo"]'; exports.wbDrawnLine = 'div[data-shape="draw"]'; exports.multiUsersWhiteboardOn = 'button[data-test="turnMultiUsersWhiteboardOn"]'; exports.multiUsersWhiteboardOff = 'button[data-test="turnMultiUsersWhiteboardOff"]'; diff --git a/bigbluebutton-tests/playwright/options/options.js b/bigbluebutton-tests/playwright/options/options.js index 3bb487173b..88ee53ac9d 100644 --- a/bigbluebutton-tests/playwright/options/options.js +++ b/bigbluebutton-tests/playwright/options/options.js @@ -65,12 +65,13 @@ class Options extends MultiUsers { await this.modPage.waitAndClick(e.modalConfirmButton); const modPageLocator = this.modPage.getLocator('body'); + await this.modPage.page.setViewportSize({ width: 1924, height: 1080 }); const screenshotOptions = { maxDiffPixels: 1000, }; await this.modPage.closeAllToastNotifications(); - + await expect(modPageLocator).toHaveScreenshot('moderator-page-dark-mode.png', screenshotOptions); await openSettings(this.modPage); @@ -85,6 +86,8 @@ class Options extends MultiUsers { await this.modPage.waitAndClick(e.modalConfirmButton); const modPageLocator = this.modPage.getLocator('body'); + + await this.modPage.page.setViewportSize({ width: 1924, height: 1080 }); const screenshotOptions = { maxDiffPixels: 1000, }; diff --git a/bigbluebutton-tests/playwright/options/options.spec.js-snapshots/moderator-page-dark-mode-Chromium-linux.png b/bigbluebutton-tests/playwright/options/options.spec.js-snapshots/moderator-page-dark-mode-Chromium-linux.png index cb8ce1a93f3eef5da1745bd73c5d08dd9d81f06e..9c710ca2deec72d40de3ec354446fb3f2f3b49a6 100644 GIT binary patch literal 185090 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 VAMgydPnsUW%3m{>&6YB(6Aq=*5XYTqyj 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%0DeA0zXxq#df-%=J>G0Y}; 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

-P9AUrI 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%0D6}Cn!3ZqwkyEkv>0;6?=+xK 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 z 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 diff --git a/bigbluebutton-tests/playwright/options/options.spec.js-snapshots/moderator-page-font-size-Chromium-linux.png b/bigbluebutton-tests/playwright/options/options.spec.js-snapshots/moderator-page-font-size-Chromium-linux.png index 131228ea8eb6d946a901913ca4b49aa26c84c957..3419c66c927723681c7a96c27c8564e30eca82be 100644 GIT binary patch literal 195919 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 z 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ⅈ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%0AFo`Rg8cVtkq&v|719p3{6lyV5*l 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 U#UJ;x2bf|!&;S4c literal 125651 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 AN&o-= diff --git a/bigbluebutton-tests/playwright/polling/poll.js b/bigbluebutton-tests/playwright/polling/poll.js index 2cce8f2272..c753c1da99 100644 --- a/bigbluebutton-tests/playwright/polling/poll.js +++ b/bigbluebutton-tests/playwright/polling/poll.js @@ -139,6 +139,8 @@ class Polling extends MultiUsers { await this.modPage.type(e.pollQuestionArea, 'Test'); await this.modPage.waitAndClick(e.addPollItem); await this.modPage.type(e.pollOptionItem, 'test1'); + await this.modPage.waitAndClick(e.addPollItem); + await this.modPage.type(e.pollOptionItem2, 'test2'); await this.modPage.waitAndClick(e.startPoll); await this.userPage.hasElement(e.pollingContainer); From 4950470ff0189291125c13569e23e4a6cab96740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Thu, 25 Jan 2024 11:37:14 -0300 Subject: [PATCH 151/512] fix(chat): properly restore text input focus after a message is sent --- .../chat-message-form/component.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/component.tsx index e5f3411f9a..089701cb1e 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-form/component.tsx @@ -134,6 +134,7 @@ const ChatMessageForm: React.FC = ({ const [error, setError] = React.useState(null); const [message, setMessage] = React.useState(''); const [showEmojiPicker, setShowEmojiPicker] = React.useState(false); + const [isTextAreaFocused, setIsTextAreaFocused] = React.useState(false); const textAreaRef: RefObject = useRef(null); const { isMobile } = deviceInfo; const prevChatId = usePreviousValue(chatId); @@ -148,6 +149,10 @@ const ChatMessageForm: React.FC = ({ const [chatSetTyping] = useMutation(CHAT_SET_TYPING); + const [chatSendMessage, { + loading: chatSendMessageLoading, error: chatSendMessageError, + }] = useMutation(CHAT_SEND_MESSAGE); + const handleUserTyping = throttle( (hasError?: boolean) => { if (hasError || !ENABLE_TYPING_INDICATOR) return; @@ -196,6 +201,17 @@ const ChatMessageForm: React.FC = ({ setMessageHint(); }, [connected, locked, partnerIsLoggedOut]); + useEffect(() => { + const shouldRestoreFocus = textAreaRef.current + && !chatSendMessageLoading + && isTextAreaFocused + && document.activeElement !== textAreaRef.current.textarea; + + if (shouldRestoreFocus) { + textAreaRef.current.textarea.focus(); + } + }, [chatSendMessageLoading, textAreaRef.current]); + const setMessageHint = () => { let chatDisabledHint = null; @@ -258,10 +274,6 @@ const ChatMessageForm: React.FC = ({ const renderForm = () => { const formRef = useRef(null); - const [chatSendMessage, { - loading: chatSendMessageLoading, error: chatSendMessageError, - }] = useMutation(CHAT_SEND_MESSAGE); - const handleSubmit = (e: React.FormEvent | React.KeyboardEvent | Event) => { e.preventDefault(); @@ -295,10 +307,6 @@ const ChatMessageForm: React.FC = ({ setShowEmojiPicker(false); const sentMessageEvent = new CustomEvent(ChatEvents.SENT_MESSAGE); window.dispatchEvent(sentMessageEvent); - - setTimeout(() => { - textAreaRef.current?.textarea.focus(); - }, 100); }; const handleMessageKeyDown = (e: React.KeyboardEvent) => { @@ -364,9 +372,11 @@ const ChatMessageForm: React.FC = ({ value={message} onFocus={() => { window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormEventsNames.CHAT_INPUT_FOCUSED)); + setIsTextAreaFocused(true); }} onBlur={() => { window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormEventsNames.CHAT_INPUT_UNFOCUSED)); + setIsTextAreaFocused(false); }} onChange={handleMessageChange} onKeyDown={handleMessageKeyDown} From 968ad6961c6d908d681f6e21d2cccca23e6a3a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Thu, 25 Jan 2024 12:00:09 -0300 Subject: [PATCH 152/512] Client: System message style --- .../page/chat-message/component.tsx | 40 ++++++++++--------- .../message-content/text-content/styles.ts | 8 +--- .../page/chat-message/styles.ts | 6 +-- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx index 03fd6979e7..c385be57ed 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx @@ -95,10 +95,17 @@ const ChatMesssage: React.FC = ({ || lastSenderPreviousPage) === message?.user?.userId; const isSystemSender = message.messageType === ChatMessageType.BREAKOUT_ROOM; const dateTime = new Date(message?.createdAt); + + const msgTime = dateTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }); + const clearMessage = `${intl.formatMessage(intlMessages.chatClear)} at ${msgTime}`; + const { away } = JSON.parse(message.messageMetadata); + const awayMessage = (away) + ? `${msgTime} ${message.senderName} ${intl.formatMessage(intlMessages.userAway)}` + : `${msgTime} ${message.senderName} ${intl.formatMessage(intlMessages.userNotAway)}`; const messageContent: { name: string, - color: string, - isModerator: boolean, + color?: string, + isModerator?: boolean, component: React.ReactElement, } = useMemo(() => { switch (message.messageType) { @@ -123,12 +130,10 @@ const ChatMesssage: React.FC = ({ case ChatMessageType.CHAT_CLEAR: return { name: intl.formatMessage(intlMessages.systemLabel), - color: '#0F70D7', - isModerator: true, component: ( ), @@ -148,15 +153,12 @@ const ChatMesssage: React.FC = ({ ), }; case ChatMessageType.USER_AWAY_STATUS_MSG: { - const { away } = JSON.parse(message.messageMetadata); return { - name: message.senderName, - color: '#0F70D7', - isModerator: true, + name: '', component: ( ), @@ -191,12 +193,14 @@ const ChatMesssage: React.FC = ({ )} - + {!ChatMessageType.USER_AWAY_STATUS_MSG ? ( + + ) : null} {messageContent.component} diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts index 1f1562e0cb..b953e77bd0 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/message-content/text-content/styles.ts @@ -2,12 +2,8 @@ import styled from 'styled-components'; import { systemMessageBackgroundColor, systemMessageBorderColor, - systemMessageFontColor, colorText, } from '/imports/ui/stylesheets/styled-components/palette'; -import { - borderRadius, -} from '/imports/ui/stylesheets/styled-components/general'; import { fontSizeBase, btnFontWeight } from '/imports/ui/stylesheets/styled-components/typography'; interface ChatMessageProps { @@ -25,10 +21,10 @@ export const ChatMessage = styled.div` ${({ systemMsg }) => systemMsg && ` background: ${systemMessageBackgroundColor}; border: 1px solid ${systemMessageBorderColor}; - border-radius: ${borderRadius}; + border-radius: 1rem; font-weight: ${btnFontWeight}; padding: ${fontSizeBase}; - color: ${systemMessageFontColor}; + text-color: #1f252b; margin-top: 0; margin-bottom: 0; overflow-wrap: break-word; diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts index 812538236a..3294e4dfdb 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts @@ -25,9 +25,9 @@ interface ChatContentProps { } interface ChatAvatarProps { - avatar: string; - color: string; - moderator: boolean; + avatar?: string; + color?: string; + moderator?: boolean; emoji?: string; } From b9e51e3163f68c89bd2f0daac5e51e5ccfda612e Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Thu, 25 Jan 2024 12:27:53 -0300 Subject: [PATCH 153/512] Introduce networkRttInMs and applicationRttInMs --- bbb-graphql-server/bbb_schema.sql | 58 +++++++++++-------- .../public_v_user_connectionStatus.yaml | 4 +- .../connection-status/component.jsx | 41 +++++++++---- .../connection-status/mutations.jsx | 7 ++- .../components/connection-status/queries.jsx | 1 - 5 files changed, 73 insertions(+), 38 deletions(-) diff --git a/bbb-graphql-server/bbb_schema.sql b/bbb-graphql-server/bbb_schema.sql index 4c20e85869..ed9832296d 100644 --- a/bbb-graphql-server/bbb_schema.sql +++ b/bbb-graphql-server/bbb_schema.sql @@ -583,7 +583,8 @@ CREATE TABLE "user_connectionStatus" ( "meetingId" varchar(100) REFERENCES "meeting"("meetingId") ON DELETE CASCADE, "connectionAliveAt" timestamp with time zone, "userClientResponseAt" timestamp with time zone, - "rttInMs" numeric, + "networkRttInMs" numeric, + "applicationRttInMs" numeric, "status" varchar(25), "statusUpdatedAt" timestamp with time zone ); @@ -593,7 +594,7 @@ create view "v_user_connectionStatus" as select * from "user_connectionStatus"; --CREATE TABLE "user_connectionStatusHistory" ( -- "userId" varchar(50) REFERENCES "user"("userId") ON DELETE CASCADE, --- "rttInMs" numeric, +-- "applicationRttInMs" numeric, -- "status" varchar(25), -- "statusUpdatedAt" timestamp with time zone --); @@ -601,7 +602,8 @@ create view "v_user_connectionStatus" as select * from "user_connectionStatus"; -- "userId" varchar(50) REFERENCES "user"("userId") ON DELETE CASCADE, -- "status" varchar(25), -- "totalOfOccurrences" integer, --- "higherRttInMs" numeric, +-- "highestNetworkRttInMs" numeric, +-- "highestApplicationRttInMs" numeric, -- "statusInsertedAt" timestamp with time zone, -- "statusUpdatedAt" timestamp with time zone, -- CONSTRAINT "user_connectionStatusHistory_pkey" PRIMARY KEY ("userId","status") @@ -613,9 +615,12 @@ CREATE TABLE "user_connectionStatusMetrics" ( "occurrencesCount" integer, "firstOccurrenceAt" timestamp with time zone, "lastOccurrenceAt" timestamp with time zone, - "lowestRttInMs" numeric, - "highestRttInMs" numeric, - "lastRttInMs" numeric, + "lowestNetworkRttInMs" numeric, + "highestNetworkRttInMs" numeric, + "lastNetworkRttInMs" numeric, + "lowestApplicationRttInMs" numeric, + "highestApplicationRttInMs" numeric, + "lastApplicationRttInMs" numeric, CONSTRAINT "user_connectionStatusMetrics_pkey" PRIMARY KEY ("userId","status") ); @@ -624,31 +629,38 @@ create index "idx_user_connectionStatusMetrics_userId" on "user_connectionStatus --This function populate rtt, status and the table user_connectionStatusMetrics CREATE OR REPLACE FUNCTION "update_user_connectionStatus_trigger_func"() RETURNS TRIGGER AS $$ DECLARE - "newRttInMs" numeric; + "newApplicationRttInMs" numeric; "newStatus" varchar(25); BEGIN IF NEW."connectionAliveAt" IS NULL OR NEW."userClientResponseAt" IS NULL THEN RETURN NEW; END IF; - "newRttInMs" := (EXTRACT(EPOCH FROM (NEW."userClientResponseAt" - NEW."connectionAliveAt")) * 1000); - "newStatus" := CASE WHEN COALESCE("newRttInMs",0) > 2000 THEN 'critical' - WHEN COALESCE("newRttInMs",0) > 1000 THEN 'danger' - WHEN COALESCE("newRttInMs",0) > 500 THEN 'warning' + "newApplicationRttInMs" := (EXTRACT(EPOCH FROM (NEW."userClientResponseAt" - NEW."connectionAliveAt")) * 1000); + "newStatus" := CASE WHEN COALESCE(NEW."networkRttInMs",0) > 2000 THEN 'critical' + WHEN COALESCE(NEW."networkRttInMs",0) > 1000 THEN 'danger' + WHEN COALESCE(NEW."networkRttInMs",0) > 500 THEN 'warning' ELSE 'normal' END; --Update table user_connectionStatusMetrics WITH upsert AS (UPDATE "user_connectionStatusMetrics" SET "occurrencesCount" = "user_connectionStatusMetrics"."occurrencesCount" + 1, - "highestRttInMs" = GREATEST("user_connectionStatusMetrics"."highestRttInMs","newRttInMs"), - "lowestRttInMs" = LEAST("user_connectionStatusMetrics"."lowestRttInMs","newRttInMs"), - "lastRttInMs" = "newRttInMs", + "highestApplicationRttInMs" = GREATEST("user_connectionStatusMetrics"."highestApplicationRttInMs","newApplicationRttInMs"), + "lowestApplicationRttInMs" = LEAST("user_connectionStatusMetrics"."lowestApplicationRttInMs","newApplicationRttInMs"), + "lastApplicationRttInMs" = "newApplicationRttInMs", + "highestNetworkRttInMs" = GREATEST("user_connectionStatusMetrics"."highestNetworkRttInMs",NEW."networkRttInMs"), + "lowestNetworkRttInMs" = LEAST("user_connectionStatusMetrics"."lowestNetworkRttInMs",NEW."networkRttInMs"), + "lastNetworkRttInMs" = NEW."networkRttInMs", "lastOccurrenceAt" = current_timestamp WHERE "userId"=NEW."userId" AND "status"= "newStatus" RETURNING *) - INSERT INTO "user_connectionStatusMetrics"("userId","status","occurrencesCount", "highestRttInMs", "lowestRttInMs", "lastRttInMs", "firstOccurrenceAt") - SELECT NEW."userId", "newStatus", 1, "newRttInMs", "newRttInMs", "newRttInMs", current_timestamp + INSERT INTO "user_connectionStatusMetrics"("userId","status","occurrencesCount", "firstOccurrenceAt", + "highestApplicationRttInMs", "lowestApplicationRttInMs", "lastApplicationRttInMs", + "highestNetworkRttInMs", "lowestNetworkRttInMs", "lastNetworkRttInMs") + SELECT NEW."userId", "newStatus", 1, current_timestamp, + "newApplicationRttInMs", "newApplicationRttInMs", "newApplicationRttInMs", + NEW."networkRttInMs", NEW."networkRttInMs", NEW."networkRttInMs" WHERE NOT EXISTS (SELECT * FROM upsert); - --Update rttInMs, status, statusUpdatedAt in user_connectionStatus + --Update networkRttInMs, applicationRttInMs, status, statusUpdatedAt in user_connectionStatus UPDATE "user_connectionStatus" - SET "rttInMs" = "newRttInMs", + SET "applicationRttInMs" = "newApplicationRttInMs", "status" = "newStatus", "statusUpdatedAt" = now() WHERE "userId" = NEW."userId"; @@ -659,12 +671,12 @@ $$ LANGUAGE plpgsql; CREATE TRIGGER "update_user_connectionStatus_trigger" AFTER UPDATE OF "userClientResponseAt" ON "user_connectionStatus" FOR EACH ROW EXECUTE FUNCTION "update_user_connectionStatus_trigger_func"(); ---This function clear userClientResponseAt and rttInMs when connectionAliveAt is updated +--This function clear userClientResponseAt and applicationRttInMs when connectionAliveAt is updated CREATE OR REPLACE FUNCTION "update_user_connectionStatus_connectionAliveAt_trigger_func"() RETURNS TRIGGER AS $$ BEGIN IF NEW."connectionAliveAt" <> OLD."connectionAliveAt" THEN NEW."userClientResponseAt" := NULL; - NEW."rttInMs" := NULL; + NEW."applicationRttInMs" := NULL; END IF; RETURN NEW; END; @@ -678,8 +690,8 @@ CREATE OR REPLACE VIEW "v_user_connectionStatusReport" AS SELECT u."meetingId", u."userId", max(cs."connectionAliveAt") AS "connectionAliveAt", max(cs."status") AS "currentStatus", ---COALESCE(max(cs."rttInMs"),(EXTRACT(EPOCH FROM (current_timestamp - max(cs."connectionAliveAt"))) * 1000)) AS "rttInMs", -CASE WHEN max(cs."connectionAliveAt") < current_timestamp - INTERVAL '10 seconds' THEN TRUE ELSE FALSE END AS "clientNotResponding", +--COALESCE(max(cs."applicationRttInMs"),(EXTRACT(EPOCH FROM (current_timestamp - max(cs."connectionAliveAt"))) * 1000)) AS "applicationRttInMs", +CASE WHEN max(cs."connectionAliveAt") < current_timestamp - INTERVAL '12 seconds' THEN TRUE ELSE FALSE END AS "clientNotResponding", (array_agg(csm."status" ORDER BY csm."lastOccurrenceAt" DESC))[1] as "lastUnstableStatus", max(csm."lastOccurrenceAt") AS "lastUnstableStatusAt" FROM "user" u @@ -702,7 +714,7 @@ CREATE INDEX "idx_user_graphqlConnectionsessionToken" ON "user_graphqlConnection ---ALTER TABLE "user_connectionStatus" ADD COLUMN "rttInMs" NUMERIC GENERATED ALWAYS AS +--ALTER TABLE "user_connectionStatus" ADD COLUMN "applicationRttInMs" NUMERIC GENERATED ALWAYS AS --(CASE WHEN "connectionAliveAt" IS NULL OR "userClientResponseAt" IS NULL THEN NULL --ELSE EXTRACT(EPOCH FROM ("userClientResponseAt" - "connectionAliveAt")) * 1000 --END) STORED; diff --git a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_connectionStatus.yaml b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_connectionStatus.yaml index bf3a6652bd..9e4dada33f 100644 --- a/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_connectionStatus.yaml +++ b/bbb-graphql-server/metadata/databases/BigBlueButton/tables/public_v_user_connectionStatus.yaml @@ -22,7 +22,8 @@ select_permissions: columns: - connectionAliveAt - meetingId - - rttInMs + - applicationRttInMs + - networkRttInMs - status - statusUpdatedAt - userClientResponseAt @@ -39,6 +40,7 @@ update_permissions: columns: - connectionAliveAt - userClientResponseAt + - networkRttInMs filter: _and: - meetingId: diff --git a/bigbluebutton-html5/imports/ui/components/connection-status/component.jsx b/bigbluebutton-html5/imports/ui/components/connection-status/component.jsx index 622b5e2c6f..1068993244 100755 --- a/bigbluebutton-html5/imports/ui/components/connection-status/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/connection-status/component.jsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useMutation, useSubscription } from '@apollo/client'; import { CONNECTION_STATUS_SUBSCRIPTION } from './queries'; import { UPDATE_CONNECTION_ALIVE_AT, UPDATE_USER_CLIENT_RESPONSE_AT } from './mutations'; @@ -6,20 +6,36 @@ import { UPDATE_CONNECTION_ALIVE_AT, UPDATE_USER_CLIENT_RESPONSE_AT } from './mu const STATS_INTERVAL = Meteor.settings.public.stats.interval; const ConnectionStatus = () => { + const networkRttInMs = useRef(null); // Ref to store the current timeout + const lastStatusUpdatedAtReceived = useRef(null); // Ref to store the current timeout + const timeoutRef = useRef(null); + const [updateUserClientResponseAtToMeAsNow] = useMutation(UPDATE_USER_CLIENT_RESPONSE_AT); const handleUpdateUserClientResponseAt = () => { - updateUserClientResponseAtToMeAsNow(); + updateUserClientResponseAtToMeAsNow({ + variables: { + networkRttInMs: networkRttInMs.current, + }, + }); }; const [updateConnectionAliveAtToMeAsNow] = useMutation(UPDATE_CONNECTION_ALIVE_AT); const handleUpdateConnectionAliveAt = () => { - updateConnectionAliveAtToMeAsNow(); + const startTime = performance.now(); + updateConnectionAliveAtToMeAsNow().then(() => { + const endTime = performance.now(); + networkRttInMs.current = endTime - startTime; + }).finally(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } - setTimeout(() => { - handleUpdateConnectionAliveAt(); - }, STATS_INTERVAL); + timeoutRef.current = setTimeout(() => { + handleUpdateConnectionAliveAt(); + }, STATS_INTERVAL); + }); }; useEffect(() => { @@ -30,11 +46,14 @@ const ConnectionStatus = () => { if (!loading && !error && data) { data.user_connectionStatus.forEach((curr) => { - if (curr.userClientResponseAt == null) { - const delay = 500; - setTimeout(() => { - handleUpdateUserClientResponseAt(); - }, delay); + if (curr.connectionAliveAt != null + && curr.userClientResponseAt == null + && (curr.statusUpdatedAt == null + || curr.statusUpdatedAt !== lastStatusUpdatedAtReceived.current + ) + ) { + lastStatusUpdatedAtReceived.current = curr.statusUpdatedAt; + handleUpdateUserClientResponseAt(); } }); } diff --git a/bigbluebutton-html5/imports/ui/components/connection-status/mutations.jsx b/bigbluebutton-html5/imports/ui/components/connection-status/mutations.jsx index f382e04e4a..e8a5f6a72a 100644 --- a/bigbluebutton-html5/imports/ui/components/connection-status/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/connection-status/mutations.jsx @@ -11,10 +11,13 @@ export const UPDATE_CONNECTION_ALIVE_AT = gql` }`; export const UPDATE_USER_CLIENT_RESPONSE_AT = gql` - mutation UpdateConnectionAliveAt($userId: String, $userClientResponseAt: timestamp) { + mutation UpdateConnectionClientResponse($networkRttInMs: numeric) { update_user_connectionStatus( where: {userClientResponseAt: {_is_null: true}} - _set: { userClientResponseAt: "now()" } + _set: { + userClientResponseAt: "now()", + networkRttInMs: $networkRttInMs + } ) { affected_rows } diff --git a/bigbluebutton-html5/imports/ui/components/connection-status/queries.jsx b/bigbluebutton-html5/imports/ui/components/connection-status/queries.jsx index 94193d3b52..ec72f53871 100644 --- a/bigbluebutton-html5/imports/ui/components/connection-status/queries.jsx +++ b/bigbluebutton-html5/imports/ui/components/connection-status/queries.jsx @@ -21,7 +21,6 @@ export const CONNECTION_STATUS_SUBSCRIPTION = gql`subscription { user_connectionStatus { connectionAliveAt userClientResponseAt - rttInMs status statusUpdatedAt } From 32d18be2686ac9e75bf71c544e335742d0bdd7c2 Mon Sep 17 00:00:00 2001 From: Gabriel Porfirio Date: Thu, 25 Jan 2024 13:49:04 -0300 Subject: [PATCH 154/512] rm flaky flag skip slide test --- .../playwright/presentation/presentation.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bigbluebutton-tests/playwright/presentation/presentation.spec.js b/bigbluebutton-tests/playwright/presentation/presentation.spec.js index d206e39115..3bdb39e9c5 100644 --- a/bigbluebutton-tests/playwright/presentation/presentation.spec.js +++ b/bigbluebutton-tests/playwright/presentation/presentation.spec.js @@ -7,7 +7,7 @@ const customStyleAvoidUploadingNotifications = encodeCustomParams(`userdata-bbb_ test.describe.parallel('Presentation', () => { // https://docs.bigbluebutton.org/2.6/release-tests.html#navigation-automated - test('Skip slide @ci @flaky', async ({ browser, context, page }) => { + test('Skip slide @ci', async ({ browser, context, page }) => { const presentation = new Presentation(browser, context); await presentation.initPages(page); await presentation.skipSlide(); @@ -47,13 +47,13 @@ test.describe.parallel('Presentation', () => { await presentation.presentationFullscreen(); }); - test('Presentation snapshot @ci @flaky', async ({ browser, context, page }, testInfo) => { + test.only('Presentation snapshot @ci @flaky', async ({ browser, context, page }, testInfo) => { const presentation = new Presentation(browser, context); await presentation.initPages(page); await presentation.presentationSnapshot(testInfo); }); - test('Hide Presentation Toolbar @ci @flaky', async ({ browser, context, page }) => { + test.only('Hide Presentation Toolbar @ci @flaky', async ({ browser, context, page }) => { const presentation = new Presentation(browser, context); await presentation.initPages(page); await presentation.hidePresentationToolbar(); From a5e08b30122bd59d0c2339517ff659d24bdeec63 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Thu, 25 Jan 2024 13:43:59 -0500 Subject: [PATCH 155/512] chore: bump axios (bbb-html5) --- bigbluebutton-html5/package-lock.json | 14 +++++++------- bigbluebutton-html5/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index d3a81e4834..00501f1ac0 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -3339,11 +3339,11 @@ "dev": true }, "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.6.tgz", + "integrity": "sha512-XZLZDFfXKM9U/Y/B4nNynfCRUqNyVZ4sBC/n9GDRCkq9vd2mIvKjKKsbIh1WPmHmNbg6ND7cTBY3Y2+u1G3/2Q==", "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -4918,9 +4918,9 @@ } }, "follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" }, "for-each": { "version": "0.3.3", diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 5a67275380..507b0f6046 100644 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -45,7 +45,7 @@ "@types/ramda": "^0.29.2", "@types/react": "^18.2.18", "autoprefixer": "^10.4.4", - "axios": "^1.6.0", + "axios": "^1.6.4", "babel-runtime": "~6.26.0", "bigbluebutton-html-plugin-sdk": "0.0.32", "bowser": "^2.11.0", From 9eeaf1db2e8c84c099c38c5d0d9c2ed99d0ff2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Thu, 25 Jan 2024 16:21:51 -0300 Subject: [PATCH 156/512] Solving errors --- .../page/chat-message/component.tsx | 22 ++++++++++++------- .../page/chat-message/styles.ts | 6 ++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx index c385be57ed..2a104697d2 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx @@ -98,14 +98,11 @@ const ChatMesssage: React.FC = ({ const msgTime = dateTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }); const clearMessage = `${intl.formatMessage(intlMessages.chatClear)} at ${msgTime}`; - const { away } = JSON.parse(message.messageMetadata); - const awayMessage = (away) - ? `${msgTime} ${message.senderName} ${intl.formatMessage(intlMessages.userAway)}` - : `${msgTime} ${message.senderName} ${intl.formatMessage(intlMessages.userNotAway)}`; + const messageContent: { name: string, - color?: string, - isModerator?: boolean, + color: string, + isModerator: boolean, component: React.ReactElement, } = useMemo(() => { switch (message.messageType) { @@ -130,6 +127,9 @@ const ChatMesssage: React.FC = ({ case ChatMessageType.CHAT_CLEAR: return { name: intl.formatMessage(intlMessages.systemLabel), + color: '', + isModerator: false, + isSystemSender: true, component: ( = ({ ), }; case ChatMessageType.USER_AWAY_STATUS_MSG: { + const { away } = JSON.parse(message.messageMetadata); + const awayMessage = (away) + ? `${msgTime} ${message.senderName} ${intl.formatMessage(intlMessages.userAway)}` + : `${msgTime} ${message.senderName} ${intl.formatMessage(intlMessages.userNotAway)}`; return { - name: '', + name: message.senderName, + color: '', + isModerator: false, component: ( = ({ isOnline={message.user?.isOnline ?? true} dateTime={dateTime} /> - ) : null} + ) : null } {messageContent.component} diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts index 3294e4dfdb..812538236a 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/styles.ts @@ -25,9 +25,9 @@ interface ChatContentProps { } interface ChatAvatarProps { - avatar?: string; - color?: string; - moderator?: boolean; + avatar: string; + color: string; + moderator: boolean; emoji?: string; } From 3ef6e2254d710809ab2675af457c4b11dcf6f816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Thu, 25 Jan 2024 17:12:23 -0300 Subject: [PATCH 157/512] fix param --- .../presentation-download-dropdown/component.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/presentation-download-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/presentation-download-dropdown/component.jsx index 12f2c34b2c..84cfbe2947 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/presentation-download-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/presentation-download-dropdown/component.jsx @@ -103,7 +103,7 @@ class PresentationDownloadDropdown extends PureComponent { const downloadableExtension = downloadFileUri?.split('.').slice(-1)[0]; const originalFileExtension = name?.split('.').slice(-1)[0]; const changeDownloadOriginalOrConvertedPresentation = (enableDownload, fileStateType) => { - handleDownloadableChange(item, fileStateType, enableDownload); + handleDownloadableChange(item?.presentationId, fileStateType, enableDownload); if (enableDownload) { handleDownloadingOfPresentation(fileStateType); } From 5c8a597cfb2499cd89bb2290c7ec9ec841611fe7 Mon Sep 17 00:00:00 2001 From: Jan Kessler Date: Thu, 30 Nov 2023 19:24:14 +0100 Subject: [PATCH 158/512] fix away and raiseHands 'reactions' being hidden by avatar image --- .../user-list-participants/list-item/component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx index 1b0d641694..71bd556d92 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx @@ -151,7 +151,7 @@ const UserListItem: React.FC = ({ user, lockSettings }) => { const reactionsEnabled = isReactionsEnabled(); - const userAvatarFiltered = user.avatar; + const userAvatarFiltered = (user.raiseHand === true || user.away === true || ( user.reaction && user.reaction.reactionEmoji !== 'none')) ? '' : user.avatar; const emojiIcons = [ { From 1c62b972f4bc9033bd2dfb5c095f169962888dee Mon Sep 17 00:00:00 2001 From: srikantharika Date: Fri, 17 Nov 2023 23:56:35 +0530 Subject: [PATCH 159/512] Update install.md link path at line 336 path doesn't exist. modified with actual path. From 7b44d803046d1cb8c3ab643e13dfe435f014a099 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 27 Nov 2023 09:16:42 -0500 Subject: [PATCH 160/512] Update links formatting --- docs/docs/administration/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/administration/install.md b/docs/docs/administration/install.md index 97e5c58928..8f4b013b47 100644 --- a/docs/docs/administration/install.md +++ b/docs/docs/administration/install.md @@ -334,7 +334,7 @@ You can upgrade by re-running the `bbb-install.sh` script again -- it will downl If you are upgrading BigBlueButton 2.6 or 2.7 we recommend you set up a new Ubuntu 22.04 server with BigBlueButton 3.0 and then [copy over your existing recordings from the old server](/administration/customize#transfer-published-recordings-from-another-server). -Make sure you read through the "what's new in 3.0" document https://docs.bigbluebutton.org/3.0/new and especifically https://docs.bigbluebutton.org/3.0/new#other-notable-changes +Make sure you read through the ["what's new in 3.0" document](https://docs.bigbluebutton.org/3.0/new) and specifically [the section covering notable changes](https://docs.bigbluebutton.org/3.0/new#other-notable-changes) ### Restart your server From 289c80c68879d6f72f4ec440d831736dbc0da60a Mon Sep 17 00:00:00 2001 From: Tim Bird Date: Tue, 3 Oct 2023 10:57:33 -0600 Subject: [PATCH 161/512] Update bbb-conf.md - fix typo in word 'easi' Change 'easi' to 'easy' --- docs/docs/administration/bbb-conf.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/administration/bbb-conf.md b/docs/docs/administration/bbb-conf.md index 2e533c0f70..6b8c9e8bb4 100644 --- a/docs/docs/administration/bbb-conf.md +++ b/docs/docs/administration/bbb-conf.md @@ -10,7 +10,7 @@ keywords: ## Introduction -`bbb-conf` is BigBlueButton's configuration tool. It makes it easi for you to modify parts of BigBlueButton's configuration, manage the BigBlueButton system (start/stop/reset), and troubleshoot potential problems with your setup. +`bbb-conf` is BigBlueButton's configuration tool. It makes it easy for you to modify parts of BigBlueButton's configuration, manage the BigBlueButton system (start/stop/reset), and troubleshoot potential problems with your setup. As a historical note, this tool was created early in the development of BigBlueButton. The core developers wrote this tool to quickly update BigBlueButton's configuration files for setup and testing. From ee412b7c09dfcb67e1dde7b679a07283e73c2e6c Mon Sep 17 00:00:00 2001 From: farhatahmad Date: Thu, 23 Nov 2023 14:22:11 -0500 Subject: [PATCH 162/512] Greenlight v3 more docs updates --- docs/docs/greenlight/v3/migration.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/docs/greenlight/v3/migration.md b/docs/docs/greenlight/v3/migration.md index a23a760cd6..18131aef89 100644 --- a/docs/docs/greenlight/v3/migration.md +++ b/docs/docs/greenlight/v3/migration.md @@ -60,7 +60,7 @@ In Greenlight v2 **.env** file, add the following variables: ![env_migration_endpoints.png](/img/greenlight/v3/migration/env_migration_endpoints.png) -## Migration Steps +### Understanding the Migrations **The migrations must be run in the following order: roles, users, rooms, settings.** @@ -80,7 +80,7 @@ However, a failed migration resource should not hinder the whole migration proce **If re-running the migration does not solve the issue, the error message should give you a clue of what went wrong.** -### Roles Migration +## Roles Migration The custom Roles and the corresponding Role Permissions will be migrated. @@ -96,13 +96,15 @@ sudo docker exec -it greenlight-v2 bundle exec rake migrations:roles **If you have an error, try re-running the migration task to resolve any failed resources migration.** **Also, make sure that the Greenlight v3 server is running and accessible through your network.** -### Users Migration +## Users Migration The Users will be migrated with their corresponding role. Important notes: - Both local and external users will be migrated. -#### Local Accounts +### Local Accounts +** If you only have external users (google, office365, LDAP, SAML, etc..), please skip to the next section.** + When migrating local accounts from GLv2 to GLv3, the password_digest field will be securely transferred from v2 to v3. This ensures that local customers can seamlessly sign in using the exact same password as in v2. To enable this, it's crucial that both GLv2 and GLv3 share the same value for the SECRET_KEY_BASE environment variable, which is set in the .env file. @@ -128,19 +130,17 @@ On your GLv3 machine, replace the `SECRET_KEY_BASE` in your .env file with the s Ensure that the `SECRET_KEY_BASE` values for GLv2, GLv3, and the `V3_SECRET_KEY_BASE` variable in GLv2's `.env` file are now synchronized. -#### Migrating Users +### Migrating Users **To migrate all of your v2 users to v3, run the following command:** ```bash sudo docker exec -it greenlight-v2 bundle exec rake migrations:users ``` -**To migrate only a portion of the users starting from *FIRST_USER_ID* to *LAST_USER_ID*, run this command instead:** - **If you have an error, try re-running the migration task to resolve any failed resources migration.** **Also, make sure that the Roles migration has been successful.** -### Rooms Migration +## Rooms Migration The Rooms will be migrated with their corresponding Room Settings. Also, the Shared Accesses will be migrated. Important notes: @@ -158,7 +158,7 @@ sudo docker exec -it greenlight-v2 bundle exec rake migrations:rooms **If you have an error, try re-running the migration task to resolve any failed resources migration.** **Also, make sure that the Users migration has been successful.** -### Settings Migration +## Settings Migration The Site Settings and the Rooms Configuration will be migrated. - The *Site Settings* are customisable settings related to the Greenlight application, such as the Brand colors, the Brand image, the Registration method, the Terms & Conditions. From e34c2d0c627acdbb72bc56e2d89c9a1e328134cb Mon Sep 17 00:00:00 2001 From: SilentFlameCR Date: Mon, 27 Nov 2023 16:56:31 -0500 Subject: [PATCH 163/512] updated greenlight docs to include user:set_admin_role rake task --- docs/docs/greenlight/v3/install.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/docs/greenlight/v3/install.md b/docs/docs/greenlight/v3/install.md index 380dba2b93..7169143b20 100644 --- a/docs/docs/greenlight/v3/install.md +++ b/docs/docs/greenlight/v3/install.md @@ -48,6 +48,12 @@ You can also run it without any arguments to create the default admin account, w docker exec -it greenlight-v3 bundle exec rake admin:create ``` +### Upgrading an existing account to an Admin Account + +You can do that by running the following command: +```bash +docker exec -it greenlight-v3 bundle exec rake user:set_admin_role['email'] +``` ## Installing on a Standalone Server ### Greenlight Install Script From ec0799883fe76452bc62368ca1e937a72d4ad9db Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Thu, 25 Jan 2024 15:59:26 -0500 Subject: [PATCH 164/512] fix: drop eslint upsetting space --- .../user-list-participants/list-item/component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx index 71bd556d92..331d1ebd8d 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/list-item/component.tsx @@ -151,7 +151,7 @@ const UserListItem: React.FC = ({ user, lockSettings }) => { const reactionsEnabled = isReactionsEnabled(); - const userAvatarFiltered = (user.raiseHand === true || user.away === true || ( user.reaction && user.reaction.reactionEmoji !== 'none')) ? '' : user.avatar; + const userAvatarFiltered = (user.raiseHand === true || user.away === true || (user.reaction && user.reaction.reactionEmoji !== 'none')) ? '' : user.avatar; const emojiIcons = [ { From 5b9b877562657acb823c50c7b4bcf2d98126672e Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Thu, 25 Jan 2024 18:17:16 -0300 Subject: [PATCH 165/512] Client test code for new rtt calc --- bbb-graphql-client-test/src/Auth.js | 8 +-- .../src/UserConnectionStatus.js | 67 ++++++++++++++----- .../src/UserConnectionStatusReport.js | 2 +- 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/bbb-graphql-client-test/src/Auth.js b/bbb-graphql-client-test/src/Auth.js index e5099f15b1..0c730623fa 100644 --- a/bbb-graphql-client-test/src/Auth.js +++ b/bbb-graphql-client-test/src/Auth.js @@ -139,11 +139,11 @@ export default function Auth() { You are online, welcome {curr.name} ({curr.userId}) - {/**/} - {/*
*/} + +
- {/**/} - {/*
*/} + +
diff --git a/bbb-graphql-client-test/src/UserConnectionStatus.js b/bbb-graphql-client-test/src/UserConnectionStatus.js index a949891764..0d1b43b0a9 100644 --- a/bbb-graphql-client-test/src/UserConnectionStatus.js +++ b/bbb-graphql-client-test/src/UserConnectionStatus.js @@ -1,8 +1,10 @@ import {gql, useMutation, useSubscription} from '@apollo/client'; -import React, {useEffect} from "react"; +import React, {useEffect, useState, useRef } from "react"; import {applyPatch} from "fast-json-patch"; export default function UserConnectionStatus() { + const networkRttInMs = useRef(null); // Ref to store the current timeout + const lastStatusUpdatedAtReceived = useRef(null); // Ref to store the current timeout //example specifying where and time (new Date().toISOString()) //but its not necessary @@ -18,13 +20,20 @@ export default function UserConnectionStatus() { // `); + const timeoutRef = useRef(null); // Ref to store the current timeout + + + //where is not necessary once user can update only its own status //Hasura accepts "now()" as value to timestamp fields const [updateUserClientResponseAtToMeAsNow] = useMutation(gql` - mutation UpdateConnectionAliveAt($userId: String, $userClientResponseAt: timestamp) { + mutation UpdateConnectionClientResponse($networkRttInMs: numeric) { update_user_connectionStatus( where: {userClientResponseAt: {_is_null: true}} - _set: { userClientResponseAt: "now()" } + _set: { + userClientResponseAt: "now()", + networkRttInMs: $networkRttInMs + } ) { affected_rows } @@ -32,7 +41,11 @@ export default function UserConnectionStatus() { `); const handleUpdateUserClientResponseAt = () => { - updateUserClientResponseAtToMeAsNow(); + updateUserClientResponseAtToMeAsNow({ + variables: { + networkRttInMs: networkRttInMs.current + }, + }); }; @@ -48,11 +61,25 @@ export default function UserConnectionStatus() { `); const handleUpdateConnectionAliveAt = () => { - updateConnectionAliveAtToMeAsNow(); + const startTime = performance.now(); - setTimeout(() => { + try { + updateConnectionAliveAtToMeAsNow().then(result => { + const endTime = performance.now(); + networkRttInMs.current = endTime - startTime; + + }); + } catch (error) { + console.error('Error performing mutation:', error); + } + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + timeoutRef.current = setTimeout(() => { handleUpdateConnectionAliveAt(); - }, 25000); + }, 5000); }; useEffect(() => { @@ -66,7 +93,8 @@ export default function UserConnectionStatus() { user_connectionStatus { connectionAliveAt userClientResponseAt - rttInMs + applicationRttInMs + networkRttInMs status statusUpdatedAt } @@ -83,7 +111,8 @@ export default function UserConnectionStatus() { {/*Id*/} connectionAliveAt userClientResponseAt - rttInMs + applicationRttInMs + networkRttInMs status statusUpdatedAt @@ -92,12 +121,17 @@ export default function UserConnectionStatus() { {data.user_connectionStatus.map((curr) => { // console.log('user_connectionStatus', curr); - if(curr.userClientResponseAt == null) { - // handleUpdateUserClientResponseAt(); - const delay = 500; - setTimeout(() => { - handleUpdateUserClientResponseAt(); - },delay); + console.log('curr.statusUpdatedAt',curr.statusUpdatedAt); + console.log('lastStatusUpdatedAtReceived.current',lastStatusUpdatedAtReceived.current); + + if(curr.userClientResponseAt == null + && (curr.statusUpdatedAt == null || curr.statusUpdatedAt !== lastStatusUpdatedAtReceived.current)) { + + + + lastStatusUpdatedAtReceived.current = curr.statusUpdatedAt; + // setLastStatusUpdatedAtReceived(curr.statusUpdatedAt); + handleUpdateUserClientResponseAt(); } return ( @@ -106,7 +140,8 @@ export default function UserConnectionStatus() { {curr.userClientResponseAt} - {curr.rttInMs} + {curr.applicationRttInMs} + {curr.networkRttInMs} {curr.status} {curr.statusUpdatedAt} diff --git a/bbb-graphql-client-test/src/UserConnectionStatusReport.js b/bbb-graphql-client-test/src/UserConnectionStatusReport.js index 1b4157904c..b7bfbd0292 100644 --- a/bbb-graphql-client-test/src/UserConnectionStatusReport.js +++ b/bbb-graphql-client-test/src/UserConnectionStatusReport.js @@ -33,7 +33,7 @@ export default function UserConnectionStatusReport() { {data.user_connectionStatusReport.map((curr) => { - console.log('user_connectionStatusReport', curr); + //console.log('user_connectionStatusReport', curr); return ( {curr.user.name} From 6dcf99bd96d0605a85691d5a424f70a7c78d59aa Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Thu, 25 Jan 2024 16:23:37 -0500 Subject: [PATCH 166/512] docs: Capitalization in Support section --- docs/docusaurus.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index bb1b698515..02befcecd9 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -65,7 +65,7 @@ const config = { {to: '/administration/install', label: 'Administration', position: 'left'}, {to: '/greenlight/v3/install', label: 'Greenlight', position: 'left'}, {to: '/new-features', label: 'New Features', position: 'left'}, - {to: '/support/getting-help', label: 'support', position: 'left'}, + {to: '/support/getting-help', label: 'Support', position: 'left'}, { type: 'docsVersionDropdown', position: 'right', From 05ddcb659da9c96818d3770ddc08aa91eb782aed Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Thu, 25 Jan 2024 18:50:21 -0300 Subject: [PATCH 167/512] Prevent middlware from retransmiting Mutations --- .../internal/hascli/conn/writer/writer.go | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go index 9d7ea23837..d6fa628f2e 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go +++ b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go @@ -96,20 +96,23 @@ RangeLoop: jsonPatchSupported = true } - browserConnection.ActiveSubscriptionsMutex.Lock() - browserConnection.ActiveSubscriptions[queryId] = common.GraphQlSubscription{ - Id: queryId, - Message: fromBrowserMessageAsMap, - OperationName: operationName, - StreamCursorField: streamCursorField, - StreamCursorVariableName: streamCursorVariableName, - StreamCursorCurrValue: streamCursorInitialValue, - LastSeenOnHasuraConnetion: hc.Id, - JsonPatchSupported: jsonPatchSupported, - Type: messageType, + //Not storing Mutations because they will not be retransmitted in case of reconnection + if messageType != common.Mutation { + browserConnection.ActiveSubscriptionsMutex.Lock() + browserConnection.ActiveSubscriptions[queryId] = common.GraphQlSubscription{ + Id: queryId, + Message: fromBrowserMessageAsMap, + OperationName: operationName, + StreamCursorField: streamCursorField, + StreamCursorVariableName: streamCursorVariableName, + StreamCursorCurrValue: streamCursorInitialValue, + LastSeenOnHasuraConnetion: hc.Id, + JsonPatchSupported: jsonPatchSupported, + Type: messageType, + } + // log.Tracef("Current queries: %v", browserConnection.ActiveSubscriptions) + browserConnection.ActiveSubscriptionsMutex.Unlock() } - // log.Tracef("Current queries: %v", browserConnection.ActiveSubscriptions) - browserConnection.ActiveSubscriptionsMutex.Unlock() } if fromBrowserMessageAsMap["type"] == "stop" { From 9530cbdd9eed11378869eb386a02eac7722602f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Fri, 26 Jan 2024 09:55:55 -0300 Subject: [PATCH 168/512] fix export presentation, add new action --- .../src/actions/presentationExport.ts | 27 +++++++++++++++++++ .../actions/presentationSetDownloadable.ts | 1 - bbb-graphql-server/metadata/actions.graphql | 7 +++++ bbb-graphql-server/metadata/actions.yaml | 7 +++++ .../ui/components/presentation/mutations.jsx | 12 +++++++++ .../presentation-uploader/container.jsx | 11 +++++--- 6 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 bbb-graphql-actions/src/actions/presentationExport.ts diff --git a/bbb-graphql-actions/src/actions/presentationExport.ts b/bbb-graphql-actions/src/actions/presentationExport.ts new file mode 100644 index 0000000000..4c2b9daf6e --- /dev/null +++ b/bbb-graphql-actions/src/actions/presentationExport.ts @@ -0,0 +1,27 @@ +import { RedisMessage } from '../types'; +import {throwErrorIfNotPresenter} from "../imports/validation"; + +export default function buildRedisMessage(sessionVariables: Record, input: Record): RedisMessage { + throwErrorIfNotPresenter(sessionVariables); + const eventName = `MakePresentationDownloadReqMsg`; + + const routing = { + meetingId: sessionVariables['x-hasura-meetingid'] as String, + userId: sessionVariables['x-hasura-userid'] as String + }; + + const header = { + name: eventName, + meetingId: routing.meetingId, + userId: routing.userId + }; + + const body = { + presId: input.presentationId, + allPages: true, + fileStateType: input.fileStateType, + pages: [], + }; + + return { eventName, routing, header, body }; +} diff --git a/bbb-graphql-actions/src/actions/presentationSetDownloadable.ts b/bbb-graphql-actions/src/actions/presentationSetDownloadable.ts index cab7190a5a..33ee462298 100644 --- a/bbb-graphql-actions/src/actions/presentationSetDownloadable.ts +++ b/bbb-graphql-actions/src/actions/presentationSetDownloadable.ts @@ -1,5 +1,4 @@ import { RedisMessage } from '../types'; -import { ValidationError } from '../types/ValidationError'; import {throwErrorIfNotPresenter} from "../imports/validation"; export default function buildRedisMessage(sessionVariables: Record, input: Record): RedisMessage { diff --git a/bbb-graphql-server/metadata/actions.graphql b/bbb-graphql-server/metadata/actions.graphql index 6277092c23..dab9f538eb 100644 --- a/bbb-graphql-server/metadata/actions.graphql +++ b/bbb-graphql-server/metadata/actions.graphql @@ -280,6 +280,13 @@ type Mutation { ): Boolean } +type Mutation { + presentationExport( + presentationId: String! + fileStateType: String! + ): Boolean +} + type Mutation { presentationRemove( presentationId: String! diff --git a/bbb-graphql-server/metadata/actions.yaml b/bbb-graphql-server/metadata/actions.yaml index 3a87de56bd..8cf4d4bdb3 100644 --- a/bbb-graphql-server/metadata/actions.yaml +++ b/bbb-graphql-server/metadata/actions.yaml @@ -245,6 +245,13 @@ actions: handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' permissions: - role: bbb_client + - name: presentationExport + definition: + kind: synchronous + handler: '{{HASURA_BBB_GRAPHQL_ACTIONS_ADAPTER_URL}}' + permissions: + - role: bbb_client + comment: presentationExport - name: presentationRemove definition: kind: synchronous diff --git a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx index 79910f22df..e5d082205e 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/mutations.jsx @@ -45,6 +45,17 @@ export const PRESENTATION_SET_DOWNLOADABLE = gql` } `; +export const PRESENTATION_EXPORT = gql` + mutation PresentationExport( + $presentationId: String!, + $fileStateType: String!,) { + presentationExport( + presentationId: $presentationId, + fileStateType: $fileStateType, + ) + } +`; + export const PRESENTATION_SET_CURRENT = gql` mutation PresentationSetCurrent($presentationId: String!) { presentationSetCurrent( @@ -84,6 +95,7 @@ export default { PRESENTATION_SET_WRITERS, PRESENTATION_SET_PAGE, PRESENTATION_SET_DOWNLOADABLE, + PRESENTATION_EXPORT, PRESENTATION_SET_CURRENT, PRESENTATION_REMOVE, PRES_ANNOTATION_DELETE, diff --git a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx index 181f09cbf9..6f60955ed3 100644 --- a/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/presentation/presentation-uploader/container.jsx @@ -17,7 +17,12 @@ import { PRESENTATIONS_SUBSCRIPTION, } from '/imports/ui/components/whiteboard/queries'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; -import { PRESENTATION_SET_DOWNLOADABLE, PRESENTATION_SET_CURRENT, PRESENTATION_REMOVE } from '../mutations'; +import { + PRESENTATION_SET_DOWNLOADABLE, + PRESENTATION_EXPORT, + PRESENTATION_SET_CURRENT, + PRESENTATION_REMOVE, +} from '../mutations'; const PRESENTATION_CONFIG = Meteor.settings.public.presentation; @@ -32,14 +37,14 @@ const PresentationUploaderContainer = (props) => { const currentPresentation = presentations.find((p) => p.current)?.presentationId || ''; const [presentationSetDownloadable] = useMutation(PRESENTATION_SET_DOWNLOADABLE); + const [presentationExport] = useMutation(PRESENTATION_EXPORT); const [presentationSetCurrent] = useMutation(PRESENTATION_SET_CURRENT); const [presentationRemove] = useMutation(PRESENTATION_REMOVE); const exportPresentation = (presentationId, fileStateType) => { - presentationSetDownloadable({ + presentationExport({ variables: { presentationId, - downloadable: true, fileStateType, }, }); From 3be1f84e979e59e7bda2fc4c6fac4a1496dbbda7 Mon Sep 17 00:00:00 2001 From: Gustavo Trott Date: Fri, 26 Jan 2024 14:42:35 -0300 Subject: [PATCH 169/512] Revert "Prevent graphql-middlware from re-transmitting Mutations" --- .../internal/hascli/conn/writer/writer.go | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go index d6fa628f2e..9d7ea23837 100644 --- a/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go +++ b/bbb-graphql-middleware/internal/hascli/conn/writer/writer.go @@ -96,23 +96,20 @@ RangeLoop: jsonPatchSupported = true } - //Not storing Mutations because they will not be retransmitted in case of reconnection - if messageType != common.Mutation { - browserConnection.ActiveSubscriptionsMutex.Lock() - browserConnection.ActiveSubscriptions[queryId] = common.GraphQlSubscription{ - Id: queryId, - Message: fromBrowserMessageAsMap, - OperationName: operationName, - StreamCursorField: streamCursorField, - StreamCursorVariableName: streamCursorVariableName, - StreamCursorCurrValue: streamCursorInitialValue, - LastSeenOnHasuraConnetion: hc.Id, - JsonPatchSupported: jsonPatchSupported, - Type: messageType, - } - // log.Tracef("Current queries: %v", browserConnection.ActiveSubscriptions) - browserConnection.ActiveSubscriptionsMutex.Unlock() + browserConnection.ActiveSubscriptionsMutex.Lock() + browserConnection.ActiveSubscriptions[queryId] = common.GraphQlSubscription{ + Id: queryId, + Message: fromBrowserMessageAsMap, + OperationName: operationName, + StreamCursorField: streamCursorField, + StreamCursorVariableName: streamCursorVariableName, + StreamCursorCurrValue: streamCursorInitialValue, + LastSeenOnHasuraConnetion: hc.Id, + JsonPatchSupported: jsonPatchSupported, + Type: messageType, } + // log.Tracef("Current queries: %v", browserConnection.ActiveSubscriptions) + browserConnection.ActiveSubscriptionsMutex.Unlock() } if fromBrowserMessageAsMap["type"] == "stop" { From 06b746318386f8928ff7c04c37d9dd454167ae0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Thu, 25 Jan 2024 14:53:36 -0300 Subject: [PATCH 170/512] migrate requestUserInformation --- .../imports/api/users-infos/server/methods.js | 2 -- .../server/methods/requestUserInformation.js | 27 ------------------- .../user-actions/component.tsx | 9 +++++-- .../user-actions/mutations.tsx | 9 +++++++ 4 files changed, 16 insertions(+), 31 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js diff --git a/bigbluebutton-html5/imports/api/users-infos/server/methods.js b/bigbluebutton-html5/imports/api/users-infos/server/methods.js index 3b11b958f5..f032653e6c 100644 --- a/bigbluebutton-html5/imports/api/users-infos/server/methods.js +++ b/bigbluebutton-html5/imports/api/users-infos/server/methods.js @@ -1,8 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import requestUserInformation from './methods/requestUserInformation'; import removeUserInformation from './methods/removeUserInformation'; Meteor.methods({ - requestUserInformation, removeUserInformation, }); diff --git a/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js b/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js deleted file mode 100644 index 6a9b2c4daa..0000000000 --- a/bigbluebutton-html5/imports/api/users-infos/server/methods/requestUserInformation.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import RedisPubSub from '/imports/startup/server/redis'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import Logger from '/imports/startup/server/logger'; - -export default function getUserInformation(externalUserId) { - try { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toThirdParty; - const EVENT_NAME = 'LookUpUserReqMsg'; - - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(externalUserId, String); - - const payload = { - externalUserId, - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method getUserInformation ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx index 4583f022f0..bb48ad270c 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx @@ -9,6 +9,7 @@ import { SET_ROLE, USER_EJECT_CAMERAS, CHAT_CREATE_WITH_USER, + REQUEST_USER_INFO, } from './mutations'; import { SET_CAMERA_PINNED, @@ -26,7 +27,6 @@ import { isVoiceOnlyUser, } from './service'; -import { makeCall } from '/imports/ui/services/api'; import { isChatEnabled } from '/imports/ui/services/features'; import { layoutDispatch } from '/imports/ui/components/layout/context'; import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums'; @@ -295,6 +295,7 @@ const UserActions: React.FC = ({ const [setEmojiStatus] = useMutation(SET_EMOJI_STATUS); const [setLocked] = useMutation(SET_LOCKED); const [userEjectCameras] = useMutation(USER_EJECT_CAMERAS); + const [requestUserInfo] = useMutation(REQUEST_USER_INFO); const removeUser = (userId: string, banUser: boolean) => { if (isVoiceOnlyUser(user.userId)) { @@ -508,7 +509,11 @@ const UserActions: React.FC = ({ key: 'directoryLookup', label: intl.formatMessage(messages.DirectoryLookupLabel), onClick: () => { - makeCall('requestUserInformation', user.extId); + requestUserInfo({ + variables: { + extId: user.extId, + }, + }); setSelected(false); }, icon: 'user', diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/mutations.tsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/mutations.tsx index 9b2245ecb6..7297949211 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/mutations.tsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/mutations.tsx @@ -33,9 +33,18 @@ export const CHAT_CREATE_WITH_USER = gql` } `; +export const REQUEST_USER_INFO = gql` + mutation RequestUserInfo($extId: String!) { + userThirdPartyInfoResquest( + externalUserId: $extId + ) + } +`; + export default { SET_AWAY, SET_ROLE, USER_EJECT_CAMERAS, CHAT_CREATE_WITH_USER, + REQUEST_USER_INFO, }; From 349535b5ceb3c8d070601189c08925a03447ba6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Fri, 26 Jan 2024 10:29:52 -0300 Subject: [PATCH 171/512] migrate userShareWebcam --- .../api/video-streams/server/methods.js | 2 -- .../server/methods/userShareWebcam.js | 34 ------------------- .../components/video-provider/component.jsx | 4 ++- .../components/video-provider/container.jsx | 17 +++++++++- .../ui/components/video-provider/mutations.ts | 13 +++++++ .../ui/components/video-provider/service.js | 1 + 6 files changed, 33 insertions(+), 38 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js create mode 100644 bigbluebutton-html5/imports/ui/components/video-provider/mutations.ts diff --git a/bigbluebutton-html5/imports/api/video-streams/server/methods.js b/bigbluebutton-html5/imports/api/video-streams/server/methods.js index f0cc07f13b..7a33758c50 100644 --- a/bigbluebutton-html5/imports/api/video-streams/server/methods.js +++ b/bigbluebutton-html5/imports/api/video-streams/server/methods.js @@ -1,8 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import userShareWebcam from './methods/userShareWebcam'; import userUnshareWebcam from './methods/userUnshareWebcam'; Meteor.methods({ - userShareWebcam, userUnshareWebcam, }); diff --git a/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js b/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js deleted file mode 100644 index 82098a5ea7..0000000000 --- a/bigbluebutton-html5/imports/api/video-streams/server/methods/userShareWebcam.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import Logger from '/imports/startup/server/logger'; -import RedisPubSub from '/imports/startup/server/redis'; -import { extractCredentials } from '/imports/api/common/server/helpers'; - -export default function userShareWebcam(stream) { - try { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'UserBroadcastCamStartMsg'; - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(stream, String); - - Logger.info(`user sharing webcam: ${meetingId} ${requesterUserId}`); - - // const actionName = 'joinVideo'; - /* TODO throw an error if user has no permission to share webcam - if (!isAllowedTo(actionName, credentials)) { - throw new Meteor.Error('not-allowed', `You are not allowed to share webcam`); - } */ - - const payload = { - stream, - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method userShareWebcam ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx index cf507eabe0..1b33880864 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx @@ -123,6 +123,7 @@ const propTypes = { currentVideoPageIndex: PropTypes.number.isRequired, totalNumberOfStreams: PropTypes.number.isRequired, isMeteorConnected: PropTypes.bool.isRequired, + playStart: PropTypes.func.isRequired, }; class VideoProvider extends Component { @@ -1153,6 +1154,7 @@ class VideoProvider extends Component { handlePlayStart(message) { const { cameraId: stream, role } = message; const peer = this.webRtcPeers[stream]; + const { playStart } = this.props; if (peer) { logger.info({ @@ -1169,7 +1171,7 @@ class VideoProvider extends Component { this.clearRestartTimers(stream); this.attachVideoStream(stream); - VideoService.playStart(stream); + playStart(stream); } else { logger.warn({ logCode: 'video_provider_playstart_no_peer', diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx index f5480a3846..379bc4edec 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx @@ -1,14 +1,29 @@ import React from 'react'; import { withTracker } from 'meteor/react-meteor-data'; +import { useMutation } from '@apollo/client'; import VideoProvider from './component'; import VideoService from './service'; import { sortVideoStreams } from '/imports/ui/components/video-provider/stream-sorting'; +import { CAMERA_BROADCAST_START } from './mutations'; const { defaultSorting: DEFAULT_SORTING } = Meteor.settings.public.kurento.cameraSortingModes; const VideoProviderContainer = ({ children, ...props }) => { const { streams, isGridEnabled } = props; - return (!streams.length && !isGridEnabled ? null : {children}); + const [cameraBroadcastStart] = useMutation(CAMERA_BROADCAST_START); + + const sendUserShareWebcam = (cameraId) => { + cameraBroadcastStart({ variables: { cameraId } }); + }; + + const playStart = (cameraId) => { + if (VideoService.isLocalStream(cameraId)) { + sendUserShareWebcam(cameraId); + VideoService.joinedVideo(); + } + }; + + return (!streams.length && !isGridEnabled ? null : {children}); }; export default withTracker(({ swapLayout, ...rest }) => { diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/mutations.ts b/bigbluebutton-html5/imports/ui/components/video-provider/mutations.ts new file mode 100644 index 0000000000..f5d6d7ebea --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-provider/mutations.ts @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; + +export const CAMERA_BROADCAST_START = gql` + mutation CameraBroadcastStart($cameraId: String!) { + cameraBroadcastStart( + stream: $cameraId + ) + } +`; + +export default { + CAMERA_BROADCAST_START, +}; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js index 956b493022..5a1b108a28 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js @@ -1073,4 +1073,5 @@ export default { getPreloadedStream: () => videoService.getPreloadedStream(), getStats: () => videoService.getStats(), updatePeerDictionaryReference: (newRef) => videoService.updatePeerDictionaryReference(newRef), + joinedVideo: () => videoService.joinedVideo(), }; From deb4a17712c77e21bc7dd01e8ae9fc527e825e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Fri, 26 Jan 2024 13:21:58 -0300 Subject: [PATCH 172/512] migrate userUnshareWebcam --- .../imports/api/video-streams/server/index.js | 1 - .../api/video-streams/server/methods.js | 6 ---- .../server/methods/userUnshareWebcam.js | 34 ------------------- .../breakout-join-confirmation/component.jsx | 4 ++- .../breakout-join-confirmation/container.jsx | 7 ++++ .../ui/components/breakout-room/component.jsx | 3 +- .../ui/components/breakout-room/container.jsx | 7 ++++ .../ui/components/video-preview/container.jsx | 10 ++++-- .../components/video-provider/component.jsx | 29 ++++++++++------ .../components/video-provider/container.jsx | 21 ++++++++++-- .../ui/components/video-provider/mutations.ts | 9 +++++ .../ui/components/video-provider/service.js | 27 ++++++++------- .../video-provider/video-button/component.jsx | 6 ++-- .../video-provider/video-button/container.jsx | 9 +++++ 14 files changed, 101 insertions(+), 72 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/video-streams/server/methods.js delete mode 100644 bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js diff --git a/bigbluebutton-html5/imports/api/video-streams/server/index.js b/bigbluebutton-html5/imports/api/video-streams/server/index.js index eff5e3f61e..888b8bec5d 100644 --- a/bigbluebutton-html5/imports/api/video-streams/server/index.js +++ b/bigbluebutton-html5/imports/api/video-streams/server/index.js @@ -1,3 +1,2 @@ import './eventHandlers'; -import './methods'; import './publisher'; diff --git a/bigbluebutton-html5/imports/api/video-streams/server/methods.js b/bigbluebutton-html5/imports/api/video-streams/server/methods.js deleted file mode 100644 index 7a33758c50..0000000000 --- a/bigbluebutton-html5/imports/api/video-streams/server/methods.js +++ /dev/null @@ -1,6 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import userUnshareWebcam from './methods/userUnshareWebcam'; - -Meteor.methods({ - userUnshareWebcam, -}); diff --git a/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js b/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js deleted file mode 100644 index afae00c436..0000000000 --- a/bigbluebutton-html5/imports/api/video-streams/server/methods/userUnshareWebcam.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; -import Logger from '/imports/startup/server/logger'; -import RedisPubSub from '/imports/startup/server/redis'; -import { extractCredentials } from '/imports/api/common/server/helpers'; - -export default function userUnshareWebcam(stream) { - try { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'UserBroadcastCamStopMsg'; - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - check(stream, String); - - Logger.info(`user unsharing webcam: ${meetingId} ${requesterUserId}`); - - // const actionName = 'joinVideo'; - /* TODO throw an error if user has no permission to share webcam - if (!isAllowedTo(actionName, credentials)) { - throw new Meteor.Error('not-allowed', `You are not allowed to share webcam`); - } */ - - const payload = { - stream, - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method userUnshareWebcam ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx index 2e94ab0583..7eaefe563d 100755 --- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/component.jsx @@ -54,6 +54,7 @@ const propTypes = { requestJoinURL: PropTypes.func.isRequired, breakouts: PropTypes.arrayOf(Object).isRequired, breakoutName: PropTypes.string.isRequired, + sendUserUnshareWebcam: PropTypes.func.isRequired, }; let interval = null; @@ -101,6 +102,7 @@ class BreakoutJoinConfirmation extends Component { voiceUserJoined, requestJoinURL, amIPresenter, + sendUserUnshareWebcam, } = this.props; const { selectValue } = this.state; @@ -120,7 +122,7 @@ class BreakoutJoinConfirmation extends Component { } VideoService.storeDeviceIds(); - VideoService.exitVideo(); + VideoService.exitVideo(sendUserUnshareWebcam); if (amIPresenter) screenshareHasEnded(); if (url === '') { logger.error({ diff --git a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/container.jsx b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/container.jsx index e7955e7aba..32868c977b 100755 --- a/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-join-confirmation/container.jsx @@ -8,6 +8,7 @@ import AudioManager from '/imports/ui/services/audio-manager'; import BreakoutJoinConfirmationComponent from './component'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; import { BREAKOUT_ROOM_REQUEST_JOIN_URL } from '../breakout-room/mutations'; +import { CAMERA_BROADCAST_STOP } from '../video-provider/mutations'; const BreakoutJoinConfirmationContrainer = (props) => { const { data: currentUserData } = useCurrentUser((user) => ({ @@ -16,6 +17,11 @@ const BreakoutJoinConfirmationContrainer = (props) => { const amIPresenter = currentUserData?.presenter; const [breakoutRoomRequestJoinURL] = useMutation(BREAKOUT_ROOM_REQUEST_JOIN_URL); + const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP); + + const sendUserUnshareWebcam = (cameraId) => { + cameraBroadcastStop({ variables: { cameraId } }); + }; const requestJoinURL = (breakoutRoomId) => { breakoutRoomRequestJoinURL({ variables: { breakoutRoomId } }); @@ -25,6 +31,7 @@ const BreakoutJoinConfirmationContrainer = (props) => { {...props} amIPresenter={amIPresenter} requestJoinURL={requestJoinURL} + sendUserUnshareWebcam={sendUserUnshareWebcam} /> }; diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx index 6da2905506..6ae0a57d76 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/component.jsx @@ -292,6 +292,7 @@ class BreakoutRoom extends PureComponent { rejoinAudio, setBreakoutAudioTransferStatus, getBreakoutAudioTransferStatus, + sendUserUnshareWebcam, } = this.props; const { @@ -362,7 +363,7 @@ class BreakoutRoom extends PureComponent { extraInfo: { logType: 'user_action' }, }, 'joining breakout room closed audio in the main room'); VideoService.storeDeviceIds(); - VideoService.exitVideo(); + VideoService.exitVideo(sendUserUnshareWebcam); if (amIPresenter) screenshareHasEnded(); Tracker.autorun((c) => { diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx index 28dbd6a92b..5786b86423 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx @@ -19,6 +19,7 @@ import { BREAKOUT_ROOM_REQUEST_JOIN_URL, } from './mutations'; import logger from '/imports/startup/client/logger'; +import { CAMERA_BROADCAST_STOP } from '../video-provider/mutations'; const BreakoutContainer = (props) => { const layoutContextDispatch = layoutDispatch(); @@ -34,6 +35,11 @@ const BreakoutContainer = (props) => { const [breakoutRoomSetTime] = useMutation(BREAKOUT_ROOM_SET_TIME); const [breakoutRoomTransfer] = useMutation(USER_TRANSFER_VOICE_TO_MEETING); const [breakoutRoomRequestJoinURL] = useMutation(BREAKOUT_ROOM_REQUEST_JOIN_URL); + const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP); + + const sendUserUnshareWebcam = (cameraId) => { + cameraBroadcastStop({ variables: { cameraId } }); + }; const endAllBreakouts = () => { Service.setCapturedContentUploading(); @@ -67,6 +73,7 @@ const BreakoutContainer = (props) => { setBreakoutsTime={setBreakoutsTime} transferUserToMeeting={transferUserToMeeting} requestJoinURL={requestJoinURL} + sendUserUnshareWebcam={sendUserUnshareWebcam} {...{ layoutContextDispatch, isRTL, amIModerator, ...props }} />; }; diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx index 6c90090887..99e77a6a19 100755 --- a/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx @@ -8,11 +8,17 @@ import ScreenShareService from '/imports/ui/components/screenshare/service'; import logger from '/imports/startup/client/logger'; import { SCREENSHARING_ERRORS } from '/imports/api/screenshare/client/bridge/errors'; import { EXTERNAL_VIDEO_STOP } from '../external-video-player/mutations'; +import { CAMERA_BROADCAST_STOP } from '../video-provider/mutations'; const VideoPreviewContainer = (props) => ; export default withTracker(({ setIsOpen, callbackToClose }) => { const [stopExternalVideoShare] = useMutation(EXTERNAL_VIDEO_STOP); + const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP); + + const sendUserUnshareWebcam = (cameraId) => { + cameraBroadcastStop({ variables: { cameraId } }); + }; return { startSharing: (deviceId) => { @@ -47,9 +53,9 @@ export default withTracker(({ setIsOpen, callbackToClose }) => { setIsOpen(false); if (deviceId) { const streamId = VideoService.getMyStreamId(deviceId); - if (streamId) VideoService.stopVideo(streamId); + if (streamId) VideoService.stopVideo(streamId, sendUserUnshareWebcam); } else { - VideoService.exitVideo(); + VideoService.exitVideo(sendUserUnshareWebcam); } }, stopSharingCameraAsContent: () => { diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx index 1b33880864..c225d1400c 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx @@ -124,11 +124,13 @@ const propTypes = { totalNumberOfStreams: PropTypes.number.isRequired, isMeteorConnected: PropTypes.bool.isRequired, playStart: PropTypes.func.isRequired, + sendUserUnshareWebcam: PropTypes.func.isRequired, }; class VideoProvider extends Component { - static onBeforeUnload() { - VideoService.onBeforeUnload(); + onBeforeUnload() { + const { sendUserUnshareWebcam } = this.props; + VideoService.onBeforeUnload(sendUserUnshareWebcam); } static shouldAttachVideoStream(peer, videoElement) { @@ -183,13 +185,14 @@ class VideoProvider extends Component { { leading: false, trailing: true }, ); this.startVirtualBackgroundByDrop = this.startVirtualBackgroundByDrop.bind(this); + this.onBeforeUnload = this.onBeforeUnload.bind(this); } componentDidMount() { this._isMounted = true; VideoService.updatePeerDictionaryReference(this.webRtcPeers); this.ws = this.openWs(); - window.addEventListener('beforeunload', VideoProvider.onBeforeUnload); + window.addEventListener('beforeunload', this.onBeforeUnload); } componentDidUpdate(prevProps) { @@ -197,7 +200,8 @@ class VideoProvider extends Component { isUserLocked, streams, currentVideoPageIndex, - isMeteorConnected + isMeteorConnected, + sendUserUnshareWebcam, } = this.props; const { socketOpen } = this.state; @@ -206,7 +210,7 @@ class VideoProvider extends Component { && prevProps.currentVideoPageIndex !== currentVideoPageIndex; if (isMeteorConnected && socketOpen) this.updateStreams(streams, shouldDebounce); - if (!prevProps.isUserLocked && isUserLocked) VideoService.lockUser(); + if (!prevProps.isUserLocked && isUserLocked) VideoService.lockUser(sendUserUnshareWebcam); // Signaling socket expired its retries and meteor is connected - create // a new signaling socket instance from scratch @@ -218,6 +222,7 @@ class VideoProvider extends Component { } componentWillUnmount() { + const { sendUserUnshareWebcam } = this.props; this._isMounted = false; VideoService.updatePeerDictionaryReference({}); @@ -225,8 +230,8 @@ class VideoProvider extends Component { this.ws.onopen = null; this.ws.onclose = null; - window.removeEventListener('beforeunload', VideoProvider.onBeforeUnload); - VideoService.exitVideo(); + window.removeEventListener('beforeunload', this.onBeforeUnload); + VideoService.exitVideo(sendUserUnshareWebcam); Object.keys(this.webRtcPeers).forEach((stream) => { this.stopWebRTCPeer(stream, false); }); @@ -334,12 +339,13 @@ class VideoProvider extends Component { } onWsClose() { + const { sendUserUnshareWebcam } = this.props; logger.info({ logCode: 'video_provider_onwsclose', }, 'Multiple video provider websocket connection closed.'); this.clearWSHeartbeat(); - VideoService.exitVideo(); + VideoService.exitVideo(sendUserUnshareWebcam); // Media is currently tied to signaling state - so if signaling shuts down, // media will shut down server-side. This cleans up our local state faster // and notify the state change as failed so the UI rolls back to the placeholder @@ -577,6 +583,7 @@ class VideoProvider extends Component { stopWebRTCPeer(stream, restarting = false) { const isLocal = VideoService.isLocalStream(stream); + const { sendUserUnshareWebcam } = this.props; // in this case, 'closed' state is not caused by an error; // we stop listening to prevent this from being treated as an error @@ -587,7 +594,7 @@ class VideoProvider extends Component { } if (isLocal) { - VideoService.stopVideo(stream); + VideoService.stopVideo(stream, sendUserUnshareWebcam); } const role = VideoService.getRole(isLocal); @@ -1181,7 +1188,7 @@ class VideoProvider extends Component { } handleSFUError(message) { - const { intl, streams } = this.props; + const { intl, streams, sendUserUnshareWebcam } = this.props; const { code, reason, streamId } = message; const isLocal = VideoService.isLocalStream(streamId); const role = VideoService.getRole(isLocal); @@ -1200,7 +1207,7 @@ class VideoProvider extends Component { // The publisher instance received an error from the server. There's no reconnect, // stop it. VideoService.notify(intl.formatMessage(intlSFUErrors[code] || intlSFUErrors[2200])); - VideoService.stopVideo(streamId); + VideoService.stopVideo(streamId, sendUserUnshareWebcam); } else { const peer = this.webRtcPeers[streamId]; const stillExists = streams.some(({ stream }) => streamId === stream); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx index 379bc4edec..a10120bc3d 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/container.jsx @@ -4,18 +4,23 @@ import { useMutation } from '@apollo/client'; import VideoProvider from './component'; import VideoService from './service'; import { sortVideoStreams } from '/imports/ui/components/video-provider/stream-sorting'; -import { CAMERA_BROADCAST_START } from './mutations'; +import { CAMERA_BROADCAST_START, CAMERA_BROADCAST_STOP } from './mutations'; const { defaultSorting: DEFAULT_SORTING } = Meteor.settings.public.kurento.cameraSortingModes; const VideoProviderContainer = ({ children, ...props }) => { const { streams, isGridEnabled } = props; const [cameraBroadcastStart] = useMutation(CAMERA_BROADCAST_START); + const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP); const sendUserShareWebcam = (cameraId) => { cameraBroadcastStart({ variables: { cameraId } }); }; + const sendUserUnshareWebcam = (cameraId) => { + cameraBroadcastStop({ variables: { cameraId } }); + }; + const playStart = (cameraId) => { if (VideoService.isLocalStream(cameraId)) { sendUserShareWebcam(cameraId); @@ -23,7 +28,19 @@ const VideoProviderContainer = ({ children, ...props }) => { } }; - return (!streams.length && !isGridEnabled ? null : {children}); + return ( + !streams.length && !isGridEnabled + ? null + : ( + + {children} + + ) + ); }; export default withTracker(({ swapLayout, ...rest }) => { diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/mutations.ts b/bigbluebutton-html5/imports/ui/components/video-provider/mutations.ts index f5d6d7ebea..5c7a20eda7 100644 --- a/bigbluebutton-html5/imports/ui/components/video-provider/mutations.ts +++ b/bigbluebutton-html5/imports/ui/components/video-provider/mutations.ts @@ -8,6 +8,15 @@ export const CAMERA_BROADCAST_START = gql` } `; +export const CAMERA_BROADCAST_STOP = gql` + mutation CameraBroadcastStop($cameraId: String!) { + cameraBroadcastStop( + stream: $cameraId + ) + } +`; + export default { CAMERA_BROADCAST_START, + CAMERA_BROADCAST_STOP, }; diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/service.js b/bigbluebutton-html5/imports/ui/components/video-provider/service.js index 5a1b108a28..bdb69c4389 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/service.js +++ b/bigbluebutton-html5/imports/ui/components/video-provider/service.js @@ -164,7 +164,7 @@ class VideoService { Session.set('deviceIds', deviceIds.join()); } - exitVideo() { + exitVideo(sendUserUnshareWebcam) { if (this.isConnected) { logger.info({ logCode: 'video_provider_unsharewebcam', @@ -176,7 +176,7 @@ class VideoService { }, { fields: { stream: 1 } }, ).fetch(); - streams.forEach(s => this.sendUserUnshareWebcam(s.stream)); + streams.forEach(s => sendUserUnshareWebcam(s.stream)); this.exitedVideo(); } } @@ -187,7 +187,7 @@ class VideoService { this.isConnected = false; } - stopVideo(cameraId) { + stopVideo(cameraId, sendUserUnshareWebcam) { const streams = VideoStreams.find( { meetingId: Auth.meetingID, @@ -201,7 +201,7 @@ class VideoService { // Check if the target (cameraId) stream exists in the remote collection. // If it does, means it was successfully shared. So do the full stop procedure. if (hasTargetStream) { - this.sendUserUnshareWebcam(cameraId); + sendUserUnshareWebcam(cameraId); } if (!hasOtherStream) { @@ -720,9 +720,9 @@ class VideoService { }, { fields: {} }) && this.disableCam(); } - lockUser() { + lockUser(sendUserUnshareWebcam) { if (this.isConnected) { - this.exitVideo(); + this.exitVideo(sendUserUnshareWebcam); } } @@ -776,8 +776,8 @@ class VideoService { } } - onBeforeUnload() { - this.exitVideo(); + onBeforeUnload(sendUserUnshareWebcam) { + this.exitVideo(sendUserUnshareWebcam); } getStatus() { @@ -1025,14 +1025,17 @@ const videoService = new VideoService(); export default { storeDeviceIds: () => videoService.storeDeviceIds(), - exitVideo: () => videoService.exitVideo(), + exitVideo: (sendUserUnshareWebcam) => videoService.exitVideo(sendUserUnshareWebcam), joinVideo: deviceId => videoService.joinVideo(deviceId), - stopVideo: cameraId => videoService.stopVideo(cameraId), + stopVideo: (cameraId, sendUserUnshareWebcam) => videoService.stopVideo( + cameraId, + sendUserUnshareWebcam, + ), getVideoStreams: () => videoService.getVideoStreams(), getInfo: () => videoService.getInfo(), getMyStreamId: deviceId => videoService.getMyStreamId(deviceId), isUserLocked: () => videoService.isUserLocked(), - lockUser: () => videoService.lockUser(), + lockUser: (sendUserUnshareWebcam) => videoService.lockUser(sendUserUnshareWebcam), getAuthenticatedURL: () => videoService.getAuthenticatedURL(), isLocalStream: cameraId => videoService.isLocalStream(cameraId), hasVideoStream: () => videoService.hasVideoStream(), @@ -1050,7 +1053,7 @@ export default { isMultipleCamerasEnabled: () => videoService.isMultipleCamerasEnabled(), mirrorOwnWebcam: userId => videoService.mirrorOwnWebcam(userId), hasCapReached: () => videoService.hasCapReached(), - onBeforeUnload: () => videoService.onBeforeUnload(), + onBeforeUnload: (sendUserUnshareWebcam) => videoService.onBeforeUnload(sendUserUnshareWebcam), notify: message => notify(message, 'error', 'video'), updateNumberOfDevices: devices => videoService.updateNumberOfDevices(devices), applyCameraProfile: debounce( diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx index 5aa2588d3e..2d5020a827 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/component.jsx @@ -65,6 +65,7 @@ const propTypes = { id: PropTypes.string, type: PropTypes.string, })).isRequired, + sendUserUnshareWebcam: PropTypes.func.isRequired, }; const JoinVideoButton = ({ @@ -74,6 +75,7 @@ const JoinVideoButton = ({ disableReason, updateSettings, cameraSettingsDropdownItems, + sendUserUnshareWebcam, }) => { const { isMobile } = deviceInfo; const isMobileSharingCamera = hasVideoStream && isMobile; @@ -108,12 +110,12 @@ const JoinVideoButton = ({ const handleOnClick = debounce(() => { switch (status) { case 'videoConnecting': - VideoService.stopVideo(); + VideoService.stopVideo(undefined, sendUserUnshareWebcam); break; case 'connected': default: if (exitVideo()) { - VideoService.exitVideo(); + VideoService.exitVideo(sendUserUnshareWebcam); } else { setForceOpen(isMobileSharingCamera); setVideoPreviewModalIsOpen(true); diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx index f8c98edd48..e17ae01cc8 100755 --- a/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-provider/video-button/container.jsx @@ -2,12 +2,14 @@ import React from 'react'; import { useContext } from 'react'; import { withTracker } from 'meteor/react-meteor-data'; import { injectIntl } from 'react-intl'; +import { useMutation } from '@apollo/client'; import JoinVideoButton from './component'; import VideoService from '../service'; import { updateSettings, } from '/imports/ui/components/settings/service'; import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context'; +import { CAMERA_BROADCAST_STOP } from '../mutations'; const JoinVideoOptionsContainer = (props) => { const { @@ -19,6 +21,12 @@ const JoinVideoOptionsContainer = (props) => { ...restProps } = props; + const [cameraBroadcastStop] = useMutation(CAMERA_BROADCAST_STOP); + + const sendUserUnshareWebcam = (cameraId) => { + cameraBroadcastStop({ variables: { cameraId } }); + }; + const { pluginsExtensibleAreasAggregatedState, } = useContext(PluginsContext); @@ -35,6 +43,7 @@ const JoinVideoOptionsContainer = (props) => { updateSettings, disableReason, status, + sendUserUnshareWebcam, ...restProps, }} /> From f5cbad283c3aae6688b023b1250c6af7a8322766 Mon Sep 17 00:00:00 2001 From: Gabriel Porfirio Date: Mon, 29 Jan 2024 09:25:45 -0300 Subject: [PATCH 173/512] removes .only --- .../playwright/presentation/presentation.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigbluebutton-tests/playwright/presentation/presentation.spec.js b/bigbluebutton-tests/playwright/presentation/presentation.spec.js index 3bdb39e9c5..a2079bc5a8 100644 --- a/bigbluebutton-tests/playwright/presentation/presentation.spec.js +++ b/bigbluebutton-tests/playwright/presentation/presentation.spec.js @@ -47,13 +47,13 @@ test.describe.parallel('Presentation', () => { await presentation.presentationFullscreen(); }); - test.only('Presentation snapshot @ci @flaky', async ({ browser, context, page }, testInfo) => { + test('Presentation snapshot @ci @flaky', async ({ browser, context, page }, testInfo) => { const presentation = new Presentation(browser, context); await presentation.initPages(page); await presentation.presentationSnapshot(testInfo); }); - test.only('Hide Presentation Toolbar @ci @flaky', async ({ browser, context, page }) => { + test('Hide Presentation Toolbar @ci @flaky', async ({ browser, context, page }) => { const presentation = new Presentation(browser, context); await presentation.initPages(page); await presentation.hidePresentationToolbar(); From ef0c48d46eec168126f65ee885cf9f9217a3c505 Mon Sep 17 00:00:00 2001 From: Gabriel Porfirio Date: Mon, 29 Jan 2024 09:55:11 -0300 Subject: [PATCH 174/512] removing polling changes, sending in another pr --- bigbluebutton-tests/playwright/polling/poll.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/bigbluebutton-tests/playwright/polling/poll.js b/bigbluebutton-tests/playwright/polling/poll.js index c753c1da99..2cce8f2272 100644 --- a/bigbluebutton-tests/playwright/polling/poll.js +++ b/bigbluebutton-tests/playwright/polling/poll.js @@ -139,8 +139,6 @@ class Polling extends MultiUsers { await this.modPage.type(e.pollQuestionArea, 'Test'); await this.modPage.waitAndClick(e.addPollItem); await this.modPage.type(e.pollOptionItem, 'test1'); - await this.modPage.waitAndClick(e.addPollItem); - await this.modPage.type(e.pollOptionItem2, 'test2'); await this.modPage.waitAndClick(e.startPoll); await this.userPage.hasElement(e.pollingContainer); From 431511b8dac0f98d31f82ea67f0997b6479c26ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Mon, 29 Jan 2024 09:49:40 -0300 Subject: [PATCH 175/512] migrate toggleVoice --- .../imports/api/voice-users/server/index.js | 1 - .../imports/api/voice-users/server/methods.js | 6 -- .../voice-users/server/methods/muteToggle.js | 66 ------------------- .../imports/ui/components/app/component.jsx | 3 +- .../imports/ui/components/app/container.jsx | 3 + .../buttons/LiveSelection.tsx | 2 +- .../buttons/muteToggle.tsx | 6 +- .../input-stream-live-selector/service.ts | 9 ++- .../audio-graphql/hooks/useToggleVoice.ts | 28 ++++++++ .../audio/audio-graphql/mutations.ts | 14 ++++ .../components/audio/audio-graphql/queries.ts | 21 ++++++ .../imports/ui/components/audio/container.jsx | 7 +- .../imports/ui/components/audio/service.js | 25 ++++--- .../ui/components/breakout-room/container.jsx | 6 +- .../talking-indicator/component.tsx | 8 ++- .../talking-indicator/service.ts | 11 +++- .../ui/components/user-list/service.js | 6 +- .../user-actions/component.tsx | 6 +- .../user-actions/service.ts | 7 +- 19 files changed, 125 insertions(+), 110 deletions(-) delete mode 100755 bigbluebutton-html5/imports/api/voice-users/server/methods.js delete mode 100644 bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/hooks/useToggleVoice.ts create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/mutations.ts create mode 100644 bigbluebutton-html5/imports/ui/components/audio/audio-graphql/queries.ts diff --git a/bigbluebutton-html5/imports/api/voice-users/server/index.js b/bigbluebutton-html5/imports/api/voice-users/server/index.js index af6a7345b5..f993f38e5b 100644 --- a/bigbluebutton-html5/imports/api/voice-users/server/index.js +++ b/bigbluebutton-html5/imports/api/voice-users/server/index.js @@ -1,3 +1,2 @@ import './eventHandlers'; import './publishers'; -import './methods'; diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods.js b/bigbluebutton-html5/imports/api/voice-users/server/methods.js deleted file mode 100755 index 637a3798a9..0000000000 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods.js +++ /dev/null @@ -1,6 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import muteToggle from './methods/muteToggle'; - -Meteor.methods({ - toggleVoice: muteToggle, -}); diff --git a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js b/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js deleted file mode 100644 index 7c8101a079..0000000000 --- a/bigbluebutton-html5/imports/api/voice-users/server/methods/muteToggle.js +++ /dev/null @@ -1,66 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { extractCredentials } from '/imports/api/common/server/helpers'; -import RedisPubSub from '/imports/startup/server/redis'; -import Users from '/imports/api/users'; -import VoiceUsers from '/imports/api/voice-users'; -import Meetings from '/imports/api/meetings'; -import Logger from '/imports/startup/server/logger'; -import { check } from 'meteor/check'; - -export default async function muteToggle(uId, toggle) { - try { - const REDIS_CONFIG = Meteor.settings.private.redis; - const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; - const EVENT_NAME = 'MuteUserCmdMsg'; - - const { meetingId, requesterUserId } = extractCredentials(this.userId); - - check(meetingId, String); - check(requesterUserId, String); - - const userToMute = uId || requesterUserId; - - const requester = await Users.findOneAsync({ - meetingId, - userId: requesterUserId, - }); - - const voiceUser = await VoiceUsers.findOneAsync({ - intId: userToMute, - meetingId, - }); - - if (!requester || !voiceUser) return; - - const { listenOnly, muted } = voiceUser; - if (listenOnly) return; - - // if allowModsToUnmuteUsers is false, users will be kicked out for attempting to unmute others - if (requesterUserId !== userToMute && muted) { - const meeting = await Meetings.findOneAsync({ meetingId }, - { fields: { 'usersProp.allowModsToUnmuteUsers': 1 } }); - if (meeting.usersProp && !meeting.usersProp.allowModsToUnmuteUsers) { - Logger.warn(`Attempted unmuting by another user meetingId:${meetingId} requester: ${requesterUserId} userId: ${userToMute}`); - return; - } - } - - let _muted; - - if ((toggle === undefined) || (toggle === null)) { - _muted = !muted; - } else { - _muted = !!toggle; - } - - const payload = { - userId: userToMute, - mutedBy: requesterUserId, - mute: _muted, - }; - - RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); - } catch (err) { - Logger.error(`Exception while invoking method muteToggle ${err.stack}`); - } -} diff --git a/bigbluebutton-html5/imports/ui/components/app/component.jsx b/bigbluebutton-html5/imports/ui/components/app/component.jsx index 9e90a61982..9b973d41d6 100644 --- a/bigbluebutton-html5/imports/ui/components/app/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/component.jsx @@ -174,6 +174,7 @@ class App extends Component { layoutContextDispatch, isRTL, setMobileUser, + toggleVoice, } = this.props; const { browserName } = browserInfo; const { osName } = deviceInfo; @@ -217,7 +218,7 @@ class App extends Component { if (CONFIRMATION_ON_LEAVE) { window.onbeforeunload = (event) => { - AudioService.muteMicrophone(); + AudioService.muteMicrophone(toggleVoice); event.stopImmediatePropagation(); event.preventDefault(); // eslint-disable-next-line no-param-reassign diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index 5fcd0183aa..449135f338 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -34,6 +34,7 @@ import { } from './service'; import App from './component'; +import useToggleVoice from '../audio/audio-graphql/hooks/useToggleVoice'; const CUSTOM_STYLE_URL = Meteor.settings.public.app.customStyleUrl; @@ -90,6 +91,7 @@ const AppContainer = (props) => { const [setMobileFlag] = useMutation(SET_MOBILE_FLAG); const [setSyncWithPresenterLayout] = useMutation(SET_SYNC_WITH_PRESENTER_LAYOUT); const [setMeetingLayoutProps] = useMutation(SET_LAYOUT_PROPS); + const toggleVoice = useToggleVoice(); const setMobileUser = (mobile) => { setMobileFlag({ @@ -238,6 +240,7 @@ const AppContainer = (props) => { shouldShowSharedNotes, shouldShowPresentation, setMobileUser, + toggleVoice, }} {...otherProps} /> diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/LiveSelection.tsx b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/LiveSelection.tsx index 518f28b5ec..09d70cc4ba 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/LiveSelection.tsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/LiveSelection.tsx @@ -63,7 +63,7 @@ interface MuteToggleProps { muted: boolean; disabled: boolean; isAudioLocked: boolean; - toggleMuteMicrophone: (muted: boolean) => void; + toggleMuteMicrophone: (muted: boolean, toggleVoice: (userId?: string | null, muted?: boolean | null) => void) => void; } interface LiveSelectionProps extends MuteToggleProps { diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/muteToggle.tsx b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/muteToggle.tsx index e378e31ded..376ac1bbeb 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/muteToggle.tsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/buttons/muteToggle.tsx @@ -3,6 +3,7 @@ import { defineMessages, useIntl } from 'react-intl'; import Styled from '../styles'; import { useShortcut } from '/imports/ui/core/hooks/useShortcut'; import Settings from '/imports/ui/services/settings'; +import useToggleVoice from '../../../hooks/useToggleVoice'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - temporary while settings are still in .js @@ -24,7 +25,7 @@ interface MuteToggleProps { muted: boolean; disabled: boolean; isAudioLocked: boolean; - toggleMuteMicrophone: (muted: boolean) => void; + toggleMuteMicrophone: (muted: boolean, toggleVoice: (userId?: string | null, muted?: boolean | null) => void) => void; } export const Mutetoggle: React.FC = ({ @@ -36,12 +37,13 @@ export const Mutetoggle: React.FC = ({ }) => { const intl = useIntl(); const toggleMuteShourtcut = useShortcut('toggleMute'); + const toggleVoice = useToggleVoice(); const label = muted ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio); const onClickCallback = (e: React.MouseEvent) => { e.stopPropagation(); - toggleMuteMicrophone(muted); + toggleMuteMicrophone(muted, toggleVoice); }; return ( // eslint-disable-next-line jsx-a11y/no-access-key diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/service.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/service.ts index 93dceafa96..c0daa31c60 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/service.ts +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/audio-controls/input-stream-live-selector/service.ts @@ -39,7 +39,10 @@ export const handleLeaveAudio = (meetingIsBreakout: boolean) => { ); }; -export const toggleMuteMicrophone = (muted: boolean) => { +export const toggleMuteMicrophone = ( + muted: boolean, + toggleVoice: (userId?: string | null, muted?: boolean | null) => void, +) => { Storage.setItem(MUTED_KEY, !muted); if (muted) { @@ -50,7 +53,7 @@ export const toggleMuteMicrophone = (muted: boolean) => { }, 'microphone unmuted by user', ); - makeCall('toggleVoice'); + toggleVoice(); } else { logger.info( { @@ -59,7 +62,7 @@ export const toggleMuteMicrophone = (muted: boolean) => { }, 'microphone muted by user', ); - makeCall('toggleVoice'); + toggleVoice(); } }; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/hooks/useToggleVoice.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/hooks/useToggleVoice.ts new file mode 100644 index 0000000000..adcdfae52c --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/hooks/useToggleVoice.ts @@ -0,0 +1,28 @@ +import { useCallback } from 'react'; +import { useMutation, useSubscription } from '@apollo/client'; +import Auth from '/imports/ui/services/auth'; +import { USER_SET_MUTED } from '../mutations'; +import { USER_MUTED, UserMutedResponse } from '../queries'; + +const useToggleVoice = () => { + const [userSetMuted] = useMutation(USER_SET_MUTED); + const { data: userMutedData } = useSubscription(USER_MUTED); + + const toggleVoice = async (userId?: string | null, muted?: boolean | null) => { + let shouldMute = muted; + const userToMute = userId ?? Auth.userID; + + if (muted === undefined || muted === null) { + const { user_voice } = userMutedData || {}; + const userData = user_voice && user_voice.find((u) => u.userId === userToMute); + if (!userData) return; + shouldMute = !userData.muted; + } + + userSetMuted({ variables: { muted: shouldMute, userId: userToMute } }); + }; + + return useCallback(toggleVoice, [userMutedData]); +}; + +export default useToggleVoice; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/mutations.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/mutations.ts new file mode 100644 index 0000000000..79e82332cb --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/mutations.ts @@ -0,0 +1,14 @@ +import { gql } from '@apollo/client'; + +export const USER_SET_MUTED = gql` + mutation UserSetMuted($userId: String, $muted: Boolean!) { + userSetMuted( + userId: $userId, + muted: $muted + ) + } +`; + +export default { + USER_SET_MUTED, +}; diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/queries.ts b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/queries.ts new file mode 100644 index 0000000000..cb124e7484 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-graphql/queries.ts @@ -0,0 +1,21 @@ +import { gql } from '@apollo/client'; + +export interface UserMutedResponse { + user_voice: Array<{ + muted: boolean; + userId: string; + }>; +} + +export const USER_MUTED = gql` + subscription UserMuted { + user_voice { + muted + userId + } + } +`; + +export default { + USER_MUTED, +}; diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx index 86a2be70c5..46c2210baf 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx @@ -22,6 +22,7 @@ import { import Service from './service'; import AudioModalContainer from './audio-modal/container'; import Settings from '/imports/ui/services/settings'; +import useToggleVoice from './audio-graphql/hooks/useToggleVoice'; const APP_CONFIG = Meteor.settings.public.app; const KURENTO_CONFIG = Meteor.settings.public.kurento; @@ -201,11 +202,13 @@ export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks, i setVideoPreviewModalIsOpen(true); }; + const toggleVoice = useToggleVoice(); + if (Service.isConnected() && !Service.isListenOnly()) { Service.updateAudioConstraints(microphoneConstraints); if (userMic && !Service.isMuted()) { - Service.toggleMuteMicrophone(); + Service.toggleMuteMicrophone(toggleVoice); notify(intl.formatMessage(intlMessages.reconectingAsListener), 'info', 'volume_level_2'); } } @@ -244,7 +247,7 @@ export default lockContextContainer(injectIntl(withTracker(({ intl, userLocks, i isAudioModalOpen, setAudioModalIsOpen, init: async () => { - await Service.init(messages, intl); + await Service.init(messages, intl, toggleVoice); const enableVideo = getFromUserSettings('bbb_enable_video', KURENTO_CONFIG.enableVideo); const autoShareWebcam = getFromUserSettings('bbb_auto_share_webcam', KURENTO_CONFIG.autoShareWebcam); if ((!autoJoin || didMountAutoJoin)) { diff --git a/bigbluebutton-html5/imports/ui/components/audio/service.js b/bigbluebutton-html5/imports/ui/components/audio/service.js index 559b91517f..76c6be665b 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/service.js +++ b/bigbluebutton-html5/imports/ui/components/audio/service.js @@ -4,7 +4,6 @@ import { throttle } from '/imports/utils/throttle'; import { debounce } from '/imports/utils/debounce'; import AudioManager from '/imports/ui/services/audio-manager'; import Meetings from '/imports/api/meetings'; -import { makeCall } from '/imports/ui/services/api'; import VoiceUsers from '/imports/api/voice-users'; import logger from '/imports/startup/client/logger'; import Storage from '../../services/storage/session'; @@ -19,7 +18,7 @@ const { const MUTED_KEY = 'muted'; -const recoverMicState = () => { +const recoverMicState = (toggleVoice) => { const muted = Storage.getItem(MUTED_KEY); if ((muted === undefined) || (muted === null)) { @@ -29,24 +28,24 @@ const recoverMicState = () => { logger.debug({ logCode: 'audio_recover_mic_state', }, `Audio recover previous mic state: muted = ${muted}`); - makeCall('toggleVoice', null, muted); + toggleVoice(null, muted); }; -const audioEventHandler = (event) => { +const audioEventHandler = (toggleVoice) => (event) => { if (!event) { return; } switch (event.name) { case 'started': - if (!event.isListenOnly) recoverMicState(); + if (!event.isListenOnly) recoverMicState(toggleVoice); break; default: break; } }; -const init = (messages, intl) => { +const init = (messages, intl, toggleVoice) => { AudioManager.setAudioMessages(messages, intl); if (AudioManager.initialized) return Promise.resolve(false); const meetingId = Auth.meetingID; @@ -69,10 +68,10 @@ const init = (messages, intl) => { microphoneLockEnforced, }; - return AudioManager.init(userData, audioEventHandler); + return AudioManager.init(userData, audioEventHandler(toggleVoice)); }; -const muteMicrophone = () => { +const muteMicrophone = (toggleVoice) => { const user = VoiceUsers.findOne({ meetingId: Auth.meetingID, intId: Auth.userID, }, { fields: { muted: 1 } }); @@ -83,7 +82,7 @@ const muteMicrophone = () => { extraInfo: { logType: 'user_action' }, }, 'User wants to leave conference. Microphone muted'); AudioManager.setSenderTrackEnabled(false); - makeCall('toggleVoice'); + toggleVoice(); } }; @@ -93,7 +92,7 @@ const isVoiceUser = () => { return voiceUser ? voiceUser.joined : false; }; -const toggleMuteMicrophone = throttle(() => { +const toggleMuteMicrophone = throttle((toggleVoice) => { const user = VoiceUsers.findOne({ meetingId: Auth.meetingID, intId: Auth.userID, }, { fields: { muted: 1 } }); @@ -105,13 +104,13 @@ const toggleMuteMicrophone = throttle(() => { logCode: 'audiomanager_unmute_audio', extraInfo: { logType: 'user_action' }, }, 'microphone unmuted by user'); - makeCall('toggleVoice'); + toggleVoice(); } else { logger.info({ logCode: 'audiomanager_mute_audio', extraInfo: { logType: 'user_action' }, }, 'microphone muted by user'); - makeCall('toggleVoice'); + toggleVoice(); } }, TOGGLE_MUTE_THROTTLE_TIME); @@ -160,7 +159,7 @@ export default { updateAudioConstraints: (constraints) => AudioManager.updateAudioConstraints(constraints), recoverMicState, - muteMicrophone: () => muteMicrophone(), + muteMicrophone: (toggleVoice) => muteMicrophone(toggleVoice), isReconnecting: () => AudioManager.isReconnecting, setBreakoutAudioTransferStatus: (status) => AudioManager .setBreakoutAudioTransferStatus(status), diff --git a/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx b/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx index 5786b86423..3733c94633 100644 --- a/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/breakout-room/container.jsx @@ -10,7 +10,6 @@ import { didUserSelectedMicrophone, didUserSelectedListenOnly, } from '/imports/ui/components/audio/audio-modal/service'; -import { makeCall } from '/imports/ui/services/api'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; import { BREAKOUT_ROOM_END_ALL, @@ -20,6 +19,7 @@ import { } from './mutations'; import logger from '/imports/startup/client/logger'; import { CAMERA_BROADCAST_STOP } from '../video-provider/mutations'; +import useToggleVoice from '../audio/audio-graphql/hooks/useToggleVoice'; const BreakoutContainer = (props) => { const layoutContextDispatch = layoutDispatch(); @@ -97,6 +97,8 @@ export default withTracker((props) => { getBreakoutAudioTransferStatus, } = AudioService; + const toggleVoice = useToggleVoice(); + const logUserCouldNotRejoinAudio = () => { logger.warn({ logCode: 'mainroom_audio_rejoin', @@ -107,7 +109,7 @@ export default withTracker((props) => { const rejoinAudio = () => { if (didUserSelectedMicrophone()) { AudioManager.joinMicrophone().then(() => { - makeCall('toggleVoice', null, true).catch(() => { + toggleVoice(null, true).catch(() => { AudioManager.forceExitAudio(); logUserCouldNotRejoinAudio(); }); diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/talking-indicator/component.tsx b/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/talking-indicator/component.tsx index 0a5dffdd45..4658d0fa27 100644 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/talking-indicator/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/talking-indicator/component.tsx @@ -13,6 +13,7 @@ import Styled from './styles'; import { User } from '/imports/ui/Types/user'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; import { muteUser } from './service'; +import useToggleVoice from '../../../audio/audio-graphql/hooks/useToggleVoice'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - temporary, while meteor exists in the project @@ -53,6 +54,7 @@ interface TalkingIndicatorProps { isBreakout: boolean; moreThanMaxIndicators: boolean; isModerator: boolean; + toggleVoice: (userId?: string | null, muted?: boolean | null) => void; } const TalkingIndicator: React.FC = ({ @@ -60,6 +62,7 @@ const TalkingIndicator: React.FC = ({ isBreakout, moreThanMaxIndicators, isModerator, + toggleVoice, }) => { const intl = useIntl(); const talkingElements = useMemo(() => talkingUsers.map((talkingUser: Partial) => { @@ -97,7 +100,7 @@ const TalkingIndicator: React.FC = ({ onClick={() => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - call signature is misse due the function being wrapped - muteUser(talkingUser.userId, muted, isBreakout, isModerator); + muteUser(talkingUser.userId, muted, isBreakout, isModerator, toggleVoice); }} label={name} tooltipLabel={!muted && isModerator @@ -194,6 +197,8 @@ const TalkingIndicatorContainer: React.FC = (() => { error: isBreakoutError, } = useSubscription(MEETING_ISBREAKOUT_SUBSCRIPTION); + const toggleVoice = useToggleVoice(); + if (talkingIndicatorLoading || isBreakoutLoading) return null; if (talkingIndicatorError || isBreakoutError) { @@ -214,6 +219,7 @@ const TalkingIndicatorContainer: React.FC = (() => { isBreakout={isBreakout} moreThanMaxIndicators={talkingUsers.length >= TALKING_INDICATORS_MAX} isModerator={currentUser?.isModerator ?? false} + toggleVoice={toggleVoice} /> ); }; diff --git a/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/talking-indicator/service.ts b/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/talking-indicator/service.ts index f79ceb581c..2b9cfbd055 100644 --- a/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/talking-indicator/service.ts +++ b/bigbluebutton-html5/imports/ui/components/nav-bar/nav-bar-graphql/talking-indicator/service.ts @@ -1,13 +1,18 @@ import { debounce } from 'radash'; -import { makeCall } from '/imports/ui/services/api'; const TALKING_INDICATOR_MUTE_INTERVAL = 500; export const muteUser = debounce( { delay: TALKING_INDICATOR_MUTE_INTERVAL }, - (id: string, muted: boolean | undefined, isBreakout: boolean, isModerator: boolean) => { + ( + id: string, + muted: boolean | undefined, + isBreakout: boolean, + isModerator: boolean, + toggleVoice: (userId?: string | null, muted?: string | null) => void, + ) => { if (!isModerator || isBreakout || muted) return null; - makeCall('toggleVoice', id); + toggleVoice(id); return null; }, ); diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js index 358eddf27e..448586038e 100755 --- a/bigbluebutton-html5/imports/ui/components/user-list/service.js +++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js @@ -480,11 +480,11 @@ const normalizeEmojiName = (emoji) => ( emoji in EMOJI_STATUSES ? EMOJI_STATUSES[emoji] : emoji ); -const toggleVoice = (userId) => { +const toggleVoice = (userId, voiceToggle) => { if (userId === Auth.userID) { - AudioService.toggleMuteMicrophone(); + AudioService.toggleMuteMicrophone(voiceToggle); } else { - makeCall('toggleVoice', userId); + voiceToggle(userId); logger.info({ logCode: 'usermenu_option_mute_toggle_audio', extraInfo: { logType: 'moderator_action', userId }, diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx index bb48ad270c..0bce0f3ca7 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/component.tsx @@ -41,6 +41,7 @@ import Styled from './styles'; import { useMutation, useLazyQuery } from '@apollo/client'; import { CURRENT_PAGE_WRITERS_QUERY } from '/imports/ui/components/whiteboard/queries'; import { PRESENTATION_SET_WRITERS } from '/imports/ui/components/presentation/mutations'; +import useToggleVoice from '/imports/ui/components/audio/audio-graphql/hooks/useToggleVoice'; interface UserActionsProps { user: User; @@ -215,6 +216,7 @@ const UserActions: React.FC = ({ const [presentationSetWriters] = useMutation(PRESENTATION_SET_WRITERS); const [getWriters, { data: usersData }] = useLazyQuery(CURRENT_PAGE_WRITERS_QUERY, { fetchPolicy: 'no-cache' }); const writers = usersData?.pres_page_writers || null; + const voiceToggle = useToggleVoice(); // users will only be fetched when getWriters is called useEffect(() => { @@ -405,7 +407,7 @@ const UserActions: React.FC = ({ key: 'mute', label: intl.formatMessage(messages.MuteUserAudioLabel), onClick: () => { - toggleVoice(user.userId); + toggleVoice(user.userId, voiceToggle); setSelected(false); }, icon: 'mute', @@ -417,7 +419,7 @@ const UserActions: React.FC = ({ key: 'unmute', label: intl.formatMessage(messages.UnmuteUserAudioLabel), onClick: () => { - toggleVoice(user.userId); + toggleVoice(user.userId, voiceToggle); setSelected(false); }, icon: 'unmute', diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/service.ts b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/service.ts index 1ba270ce88..0c621398e9 100644 --- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/service.ts +++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-participants/user-actions/service.ts @@ -5,7 +5,6 @@ import { } from '/imports/ui/Types/meeting'; import Auth from '/imports/ui/services/auth'; import { EMOJI_STATUSES } from '/imports/utils/statuses'; -import { makeCall } from '/imports/ui/services/api'; import AudioService from '/imports/ui/components/audio/service'; import logger from '/imports/startup/client/logger'; @@ -128,11 +127,11 @@ export const isVideoPinEnabledForCurrentUser = ( // so this code is duplicated from the old userlist service // session for chats the current user started -export const toggleVoice = (userId: string) => { +export const toggleVoice = (userId: string, voiceToggle: (userId?: string | null, muted?: boolean | null) => void) => { if (userId === Auth.userID) { - AudioService.toggleMuteMicrophone(); + AudioService.toggleMuteMicrophone(voiceToggle); } else { - makeCall('toggleVoice', userId); + voiceToggle(userId); logger.info({ logCode: 'usermenu_option_mute_toggle_audio', extraInfo: { logType: 'moderator_action', userId }, From b5349aacb45ca41a7203802a648dfdb9728332a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Mon, 29 Jan 2024 11:52:44 -0300 Subject: [PATCH 176/512] Corrections --- .../page/chat-message/component.tsx | 44 +++++++++++-------- bigbluebutton-html5/public/locales/en.json | 2 +- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx index 54b0b2768d..82acf789d7 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx +++ b/bigbluebutton-html5/imports/ui/components/chat/chat-graphql/chat-message-list/page/chat-message/component.tsx @@ -110,9 +110,12 @@ const ChatMesssage: React.FC = ({ || lastSenderPreviousPage) === message?.user?.userId; const isSystemSender = message.messageType === ChatMessageType.BREAKOUT_ROOM; const dateTime = new Date(message?.createdAt); - - const msgTime = dateTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }); - const clearMessage = `${intl.formatMessage(intlMessages.chatClear)} at ${msgTime}`; + const formattedTime = intl.formatTime(dateTime, { + hour: 'numeric', + minute: 'numeric', + }); + const msgTime = formattedTime; + const clearMessage = `${intl.formatMessage(intlMessages.chatClear, { 0: msgTime })}`; const messageContent: { name: string, @@ -204,24 +207,27 @@ const ChatMesssage: React.FC = ({ }, []); return ( - {(!message?.user || !sameSender) && ( - - {!message.user || message.user?.avatar.length === 0 ? messageContent.name.toLowerCase().slice(0, 2) || '' : ''} - + {(!message?.user || !sameSender) && + (message.messageType !== ChatMessageType.USER_AWAY_STATUS_MSG + && message.messageType !== ChatMessageType.CHAT_CLEAR) && ( + + {!message.user || message.user?.avatar.length === 0 ? messageContent.name.toLowerCase().slice(0, 2) || '' : ''} + )} - {!ChatMessageType.USER_AWAY_STATUS_MSG ? ( - - ) : null } + {message.messageType !== ChatMessageType.USER_AWAY_STATUS_MSG + && message.messageType !== ChatMessageType.CHAT_CLEAR && ( + + )} {messageContent.component} diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index 11871d2242..4757124e0b 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -28,7 +28,7 @@ "app.chat.emptyLogLabel": "Chat log empty", "app.chat.away": "Is away", "app.chat.notAway": "Is not away anymore", - "app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator", + "app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator at {0}", "app.chat.multi.typing": "Multiple users are typing", "app.chat.someone.typing": "Someone is typing", "app.chat.one.typing": "{0} is typing", From e28408bca285b3a5532d9bfe37b0e40654551e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ram=C3=B3n=20Souza?= Date: Mon, 29 Jan 2024 13:27:30 -0300 Subject: [PATCH 177/512] remove unused audio captions code --- .../imports/api/audio-captions/index.js | 9 - .../audio-captions/server/eventHandlers.js | 4 - .../server/handlers/transcriptUpdated.js | 12 - .../api/audio-captions/server/index.js | 2 - .../server/modifiers/clearAudioCaptions.js | 26 -- .../server/modifiers/setTranscript.js | 30 -- .../api/audio-captions/server/publishers.js | 26 -- .../server/modifiers/meetingHasEnded.js | 2 - .../ui/components/actions-bar/component.jsx | 2 +- .../imports/ui/components/app/container.jsx | 2 +- .../audio/captions/button/component.jsx | 259 ------------------ .../audio/captions/button/container.jsx | 28 -- .../audio/captions/button/styles.js | 61 ----- .../audio/captions/live/component.jsx | 104 ------- .../audio/captions/live/container.jsx | 21 -- .../audio/captions/live/user/component.jsx | 39 --- .../audio/captions/live/user/container.jsx | 44 --- .../ui/components/audio/captions/service.js | 30 -- .../nav-bar/options-dropdown/container.jsx | 15 +- .../ui/components/subscriptions/component.jsx | 1 - .../private/config/settings.yml | 2 +- bigbluebutton-html5/server/main.js | 1 - 22 files changed, 16 insertions(+), 704 deletions(-) delete mode 100644 bigbluebutton-html5/imports/api/audio-captions/index.js delete mode 100644 bigbluebutton-html5/imports/api/audio-captions/server/eventHandlers.js delete mode 100644 bigbluebutton-html5/imports/api/audio-captions/server/handlers/transcriptUpdated.js delete mode 100644 bigbluebutton-html5/imports/api/audio-captions/server/index.js delete mode 100644 bigbluebutton-html5/imports/api/audio-captions/server/modifiers/clearAudioCaptions.js delete mode 100644 bigbluebutton-html5/imports/api/audio-captions/server/modifiers/setTranscript.js delete mode 100644 bigbluebutton-html5/imports/api/audio-captions/server/publishers.js delete mode 100644 bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx delete mode 100644 bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx delete mode 100644 bigbluebutton-html5/imports/ui/components/audio/captions/button/styles.js delete mode 100644 bigbluebutton-html5/imports/ui/components/audio/captions/live/component.jsx delete mode 100644 bigbluebutton-html5/imports/ui/components/audio/captions/live/container.jsx delete mode 100644 bigbluebutton-html5/imports/ui/components/audio/captions/live/user/component.jsx delete mode 100644 bigbluebutton-html5/imports/ui/components/audio/captions/live/user/container.jsx diff --git a/bigbluebutton-html5/imports/api/audio-captions/index.js b/bigbluebutton-html5/imports/api/audio-captions/index.js deleted file mode 100644 index cf10470261..0000000000 --- a/bigbluebutton-html5/imports/api/audio-captions/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -const AudioCaptions = new Mongo.Collection('audio-captions'); - -if (Meteor.isServer) { - AudioCaptions.createIndexAsync({ meetingId: 1 }); -} - -export default AudioCaptions; diff --git a/bigbluebutton-html5/imports/api/audio-captions/server/eventHandlers.js b/bigbluebutton-html5/imports/api/audio-captions/server/eventHandlers.js deleted file mode 100644 index 9a8e2f1a96..0000000000 --- a/bigbluebutton-html5/imports/api/audio-captions/server/eventHandlers.js +++ /dev/null @@ -1,4 +0,0 @@ -import RedisPubSub from '/imports/startup/server/redis'; -import handleTranscriptUpdated from '/imports/api/audio-captions/server/handlers/transcriptUpdated'; - -RedisPubSub.on('TranscriptUpdatedEvtMsg', handleTranscriptUpdated); diff --git a/bigbluebutton-html5/imports/api/audio-captions/server/handlers/transcriptUpdated.js b/bigbluebutton-html5/imports/api/audio-captions/server/handlers/transcriptUpdated.js deleted file mode 100644 index d8e66c6cfc..0000000000 --- a/bigbluebutton-html5/imports/api/audio-captions/server/handlers/transcriptUpdated.js +++ /dev/null @@ -1,12 +0,0 @@ -import setTranscript from '/imports/api/audio-captions/server/modifiers/setTranscript'; - -export default async function transcriptUpdated({ header, body }) { - const { meetingId } = header; - - const { - transcriptId, - transcript, - } = body; - - await setTranscript(meetingId, transcriptId, transcript); -} diff --git a/bigbluebutton-html5/imports/api/audio-captions/server/index.js b/bigbluebutton-html5/imports/api/audio-captions/server/index.js deleted file mode 100644 index f993f38e5b..0000000000 --- a/bigbluebutton-html5/imports/api/audio-captions/server/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import './eventHandlers'; -import './publishers'; diff --git a/bigbluebutton-html5/imports/api/audio-captions/server/modifiers/clearAudioCaptions.js b/bigbluebutton-html5/imports/api/audio-captions/server/modifiers/clearAudioCaptions.js deleted file mode 100644 index cd97c33f14..0000000000 --- a/bigbluebutton-html5/imports/api/audio-captions/server/modifiers/clearAudioCaptions.js +++ /dev/null @@ -1,26 +0,0 @@ -import AudioCaptions from '/imports/api/audio-captions'; -import Logger from '/imports/startup/server/logger'; - -export default async function clearAudioCaptions(meetingId) { - if (meetingId) { - try { - const numberAffected = await AudioCaptions.removeAsync({ meetingId }); - - if (numberAffected) { - Logger.info(`Cleared AudioCaptions (${meetingId})`); - } - } catch (err) { - Logger.error(`Error on clearing audio captions (${meetingId}). ${err}`); - } - } else { - try { - const numberAffected = await AudioCaptions.removeAsync({}); - - if (numberAffected) { - Logger.info('Cleared AudioCaptions (all)'); - } - } catch (err) { - Logger.error(`Error on clearing audio captions (all). ${err}`); - } - } -} diff --git a/bigbluebutton-html5/imports/api/audio-captions/server/modifiers/setTranscript.js b/bigbluebutton-html5/imports/api/audio-captions/server/modifiers/setTranscript.js deleted file mode 100644 index d5ef7b7973..0000000000 --- a/bigbluebutton-html5/imports/api/audio-captions/server/modifiers/setTranscript.js +++ /dev/null @@ -1,30 +0,0 @@ -import { check } from 'meteor/check'; -import AudioCaptions from '/imports/api/audio-captions'; -import Logger from '/imports/startup/server/logger'; - -export default async function setTranscript(meetingId, transcriptId, transcript) { - try { - check(meetingId, String); - check(transcriptId, String); - check(transcript, String); - - const selector = { meetingId }; - - const modifier = { - $set: { - transcriptId, - transcript, - }, - }; - - const numberAffected = await AudioCaptions.upsertAsync(selector, modifier); - - if (numberAffected) { - Logger.debug(`Set transcriptId=${transcriptId} transcript=${transcript} meeting=${meetingId}`); - } else { - Logger.debug(`Upserted transcriptId=${transcriptId} transcript=${transcript} meeting=${meetingId}`); - } - } catch (err) { - Logger.error(`Setting audio captions transcript to the collection: ${err}`); - } -} diff --git a/bigbluebutton-html5/imports/api/audio-captions/server/publishers.js b/bigbluebutton-html5/imports/api/audio-captions/server/publishers.js deleted file mode 100644 index 17d4632fea..0000000000 --- a/bigbluebutton-html5/imports/api/audio-captions/server/publishers.js +++ /dev/null @@ -1,26 +0,0 @@ -import AudioCaptions from '/imports/api/audio-captions'; -import { Meteor } from 'meteor/meteor'; -import Logger from '/imports/startup/server/logger'; -import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation'; - -async function audioCaptions() { - const tokenValidation = await AuthTokenValidation - .findOneAsync({ connectionId: this.connection.id }); - - if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) { - Logger.warn(`Publishing AudioCaptions was requested by unauth connection ${this.connection.id}`); - return AudioCaptions.find({ meetingId: '' }); - } - - const { meetingId, userId } = tokenValidation; - Logger.debug('Publishing AudioCaptions', { meetingId, requestedBy: userId }); - - return AudioCaptions.find({ meetingId }); -} - -function publish(...args) { - const boundAudioCaptions = audioCaptions.bind(this); - return boundAudioCaptions(...args); -} - -Meteor.publish('audio-captions', publish); diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js index 7ea6d69e7c..131c7b0db1 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js @@ -13,7 +13,6 @@ import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoic import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo'; import clearScreenshare from '/imports/api/screenshare/server/modifiers/clearScreenshare'; import clearTimer from '/imports/api/timer/server/modifiers/clearTimer'; -import clearAudioCaptions from '/imports/api/audio-captions/server/modifiers/clearAudioCaptions'; import clearMeetingTimeRemaining from '/imports/api/meetings/server/modifiers/clearMeetingTimeRemaining'; import clearLocalSettings from '/imports/api/local-settings/server/modifiers/clearLocalSettings'; import clearRecordMeeting from './clearRecordMeeting'; @@ -42,7 +41,6 @@ export default async function meetingHasEnded(meetingId) { clearVoiceUsers(meetingId), clearUserInfo(meetingId), clearTimer(meetingId), - clearAudioCaptions(meetingId), clearLocalSettings(meetingId), clearMeetingTimeRemaining(meetingId), clearRecordMeeting(meetingId), diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index c3506db1f4..20ddbc4263 100755 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -4,7 +4,7 @@ import deviceInfo from '/imports/utils/deviceInfo'; import { ActionsBarItemType, ActionsBarPosition } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/actions-bar-item/enums'; import Styled from './styles'; import ActionsDropdown from './actions-dropdown/container'; -import AudioCaptionsButtonContainer from '/imports/ui/components/audio/captions/button/container'; +import AudioCaptionsButtonContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/button/component'; import CaptionsReaderMenuContainer from '/imports/ui/components/captions/reader-menu/container'; import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container'; import ReactionsButtonContainer from './reactions-button/container'; diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index 5fcd0183aa..7e44f892e9 100755 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -3,7 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data'; import Auth from '/imports/ui/services/auth'; import Users from '/imports/api/users'; import Meetings, { LayoutMeetings } from '/imports/api/meetings'; -import AudioCaptionsLiveContainer from '/imports/ui/components/audio/captions/live/container'; +import AudioCaptionsLiveContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/live/component'; import AudioCaptionsService from '/imports/ui/components/audio/captions/service'; import { notify } from '/imports/ui/services/notification'; import CaptionsContainer from '/imports/ui/components/captions/live/container'; diff --git a/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx deleted file mode 100644 index b79ffe4ad2..0000000000 --- a/bigbluebutton-html5/imports/ui/components/audio/captions/button/component.jsx +++ /dev/null @@ -1,259 +0,0 @@ -import React, { useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; -import Service from '/imports/ui/components/audio/captions/service'; -import SpeechService from '/imports/ui/components/audio/captions/speech/service'; -import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji'; -import BBBMenu from '/imports/ui/components/common/menu/component'; -import Styled from './styles'; -import { useMutation } from '@apollo/client'; -import { SET_SPEECH_LOCALE } from '/imports/ui/core/graphql/mutations/userMutations'; - -const intlMessages = defineMessages({ - start: { - id: 'app.audio.captions.button.start', - description: 'Start audio captions', - }, - stop: { - id: 'app.audio.captions.button.stop', - description: 'Stop audio captions', - }, - transcriptionSettings: { - id: 'app.audio.captions.button.transcriptionSettings', - description: 'Audio captions settings modal', - }, - transcription: { - id: 'app.audio.captions.button.transcription', - description: 'Audio speech transcription label', - }, - transcriptionOn: { - id: 'app.switch.onLabel', - }, - transcriptionOff: { - id: 'app.switch.offLabel', - }, - language: { - id: 'app.audio.captions.button.language', - description: 'Audio speech recognition language label', - }, - 'de-DE': { - id: 'app.audio.captions.select.de-DE', - description: 'Audio speech recognition german language', - }, - 'en-US': { - id: 'app.audio.captions.select.en-US', - description: 'Audio speech recognition english language', - }, - 'es-ES': { - id: 'app.audio.captions.select.es-ES', - description: 'Audio speech recognition spanish language', - }, - 'fr-FR': { - id: 'app.audio.captions.select.fr-FR', - description: 'Audio speech recognition french language', - }, - 'hi-ID': { - id: 'app.audio.captions.select.hi-ID', - description: 'Audio speech recognition indian language', - }, - 'it-IT': { - id: 'app.audio.captions.select.it-IT', - description: 'Audio speech recognition italian language', - }, - 'ja-JP': { - id: 'app.audio.captions.select.ja-JP', - description: 'Audio speech recognition japanese language', - }, - 'pt-BR': { - id: 'app.audio.captions.select.pt-BR', - description: 'Audio speech recognition portuguese language', - }, - 'ru-RU': { - id: 'app.audio.captions.select.ru-RU', - description: 'Audio speech recognition russian language', - }, - 'zh-CN': { - id: 'app.audio.captions.select.zh-CN', - description: 'Audio speech recognition chinese language', - }, -}); - -const DEFAULT_LOCALE = 'en-US'; -const DISABLED = ''; - -const CaptionsButton = ({ - intl, - active, - isRTL, - enabled, - currentSpeechLocale, - availableVoices, - isSupported, - isVoiceUser, -}) => { - const isTranscriptionDisabled = () => ( - currentSpeechLocale === DISABLED - ); - - const [setSpeechLocale] = useMutation(SET_SPEECH_LOCALE); - - const setUserSpeechLocale = (speechLocale, provider) => { - setSpeechLocale({ - variables: { - locale: speechLocale, - provider, - }, - }); - }; - - const fallbackLocale = availableVoices.includes(navigator.language) - ? navigator.language : DEFAULT_LOCALE; - - const getSelectedLocaleValue = (isTranscriptionDisabled() ? fallbackLocale : currentSpeechLocale); - - const selectedLocale = useRef(getSelectedLocaleValue); - - useEffect(() => { - if (!isTranscriptionDisabled()) selectedLocale.current = getSelectedLocaleValue; - }, [currentSpeechLocale]); - - if (!enabled) return null; - - const shouldRenderChevron = isSupported && isVoiceUser; - - const getAvailableLocales = () => { - let indexToInsertSeparator = -1; - const availableVoicesObjectToMenu = availableVoices.map((availableVoice, index) => { - if (availableVoice === availableVoices[0]) { - indexToInsertSeparator = index; - } - return ( - { - icon: '', - label: intl.formatMessage(intlMessages[availableVoice]), - key: availableVoice, - iconRight: selectedLocale.current === availableVoice ? 'check' : null, - customStyles: (selectedLocale.current === availableVoice) && Styled.SelectedLabel, - disabled: isTranscriptionDisabled(), - onClick: () => { - selectedLocale.current = availableVoice; - SpeechService.setSpeechLocale(selectedLocale.current, setUserSpeechLocale); - }, - } - ); - }); - if (indexToInsertSeparator >= 0) { - availableVoicesObjectToMenu.splice(indexToInsertSeparator, 0, { - key: 'separator-01', - isSeparator: true, - }); - } - return [ - ...availableVoicesObjectToMenu, - ]; - }; - - const toggleTranscription = () => { - SpeechService.setSpeechLocale( - isTranscriptionDisabled() ? selectedLocale.current : DISABLED, setUserSpeechLocale, - ); - }; - - const getAvailableLocalesList = () => ( - [{ - key: 'availableLocalesList', - label: intl.formatMessage(intlMessages.language), - customStyles: Styled.TitleLabel, - disabled: true, - }, - ...getAvailableLocales(), - { - key: 'divider', - label: intl.formatMessage(intlMessages.transcription), - customStyles: Styled.TitleLabel, - disabled: true, - }, - { - key: 'separator-02', - isSeparator: true, - }, - { - key: 'transcriptionStatus', - label: intl.formatMessage( - isTranscriptionDisabled() - ? intlMessages.transcriptionOn - : intlMessages.transcriptionOff, - ), - customStyles: isTranscriptionDisabled() - ? Styled.EnableTrascription : Styled.DisableTrascription, - disabled: false, - onClick: toggleTranscription, - }] - ); - - const onToggleClick = (e) => { - e.stopPropagation(); - Service.setAudioCaptions(!active); - }; - - const startStopCaptionsButton = ( - - ); - - return ( - shouldRenderChevron - ? ( - - - { startStopCaptionsButton } - - - )} - actions={getAvailableLocalesList()} - opts={{ - id: 'default-dropdown-menu', - keepMounted: true, - transitionDuration: 0, - elevation: 3, - getcontentanchorel: null, - fullwidth: 'true', - anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' }, - transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' }, - }} - /> - - ) : startStopCaptionsButton - ); -}; - -CaptionsButton.propTypes = { - intl: PropTypes.shape({ - formatMessage: PropTypes.func.isRequired, - }).isRequired, - active: PropTypes.bool.isRequired, - isRTL: PropTypes.bool.isRequired, - enabled: PropTypes.bool.isRequired, - currentSpeechLocale: PropTypes.string.isRequired, - availableVoices: PropTypes.arrayOf(PropTypes.string).isRequired, - isSupported: PropTypes.bool.isRequired, - isVoiceUser: PropTypes.bool.isRequired, -}; - -export default injectIntl(CaptionsButton); diff --git a/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx deleted file mode 100644 index 11fc878ad3..0000000000 --- a/bigbluebutton-html5/imports/ui/components/audio/captions/button/container.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { withTracker } from 'meteor/react-meteor-data'; -import Service from '/imports/ui/components/audio/captions/service'; -import Button from './component'; -import SpeechService from '/imports/ui/components/audio/captions/speech/service'; -import AudioService from '/imports/ui/components/audio/service'; -import AudioCaptionsButtonContainer from '../../audio-graphql/audio-captions/button/component'; - -const Container = (props) =>