From c62f2f2790ea08dc0063752260733c8ed3955449 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Tue, 27 Aug 2024 15:55:11 -0500 Subject: [PATCH 001/290] fix(RN/shared-video): sharedVideoAllowedURLDomains prop from branding. On mobile (React-Native) the sharedVideoAllowedURLDomains property from dynamic branding was filtered and therefore the allow list from the branding was not reaching redux and was ignored. --- react/features/dynamic-branding/middleware.native.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/react/features/dynamic-branding/middleware.native.ts b/react/features/dynamic-branding/middleware.native.ts index 941136942de6..adf80b244f21 100644 --- a/react/features/dynamic-branding/middleware.native.ts +++ b/react/features/dynamic-branding/middleware.native.ts @@ -24,7 +24,8 @@ MiddlewareRegistry.register(store => next => action => { brandedIcons, didPageUrl, inviteDomain, - labels + labels, + sharedVideoAllowedURLDomains } = action.value; action.value = { @@ -34,7 +35,8 @@ MiddlewareRegistry.register(store => next => action => { brandedIcons, didPageUrl, inviteDomain, - labels + labels, + sharedVideoAllowedURLDomains }; // The backend may send an empty string, make sure we skip that. From 697ede207b4c2d7be7eedcdd7c53e77911da3edf Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 27 Aug 2024 15:59:07 -0500 Subject: [PATCH 002/290] fix(shared-video): Fix skip showing confirm dialog for YouTube links. --- react/features/shared-video/middleware.any.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react/features/shared-video/middleware.any.ts b/react/features/shared-video/middleware.any.ts index 328f8c8c1208..d41dec71506b 100644 --- a/react/features/shared-video/middleware.any.ts +++ b/react/features/shared-video/middleware.any.ts @@ -65,7 +65,7 @@ MiddlewareRegistry.register(store => next => action => { return; } - if (isURLAllowedForSharedVideo(value) + if (isURLAllowedForSharedVideo(value, getState()['features/shared-video'].allowedUrlDomains, true) || localParticipantId === from || getState()['features/shared-video'].confirmShowVideo) { // if confirmed skip asking again handleSharingVideoStatus(store, value, attributes, conference); From 32f9f8ba926151c894ce1f38b253fb8ec5b4a544 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Tue, 27 Aug 2024 17:14:29 -0500 Subject: [PATCH 003/290] fix(gifs): trim the message before extracting the URL. --- .../base/react/components/web/Message.tsx | 5 ++-- .../chat/components/native/GifMessage.tsx | 4 +-- react/features/chat/middleware.ts | 5 ++-- react/features/gifs/function.any.ts | 26 ++++++++++++++++--- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/react/features/base/react/components/web/Message.tsx b/react/features/base/react/components/web/Message.tsx index 170cabcbf673..ec56a9143e29 100644 --- a/react/features/base/react/components/web/Message.tsx +++ b/react/features/base/react/components/web/Message.tsx @@ -4,8 +4,7 @@ import { connect } from 'react-redux'; import { IReduxState } from '../../../../app/types'; import GifMessage from '../../../../chat/components/web/GifMessage'; -import { GIF_PREFIX } from '../../../../gifs/constants'; -import { isGifEnabled, isGifMessage } from '../../../../gifs/functions.web'; +import { extractGifURL, isGifEnabled, isGifMessage } from '../../../../gifs/functions.web'; import Linkify from './Linkify'; @@ -55,7 +54,7 @@ class Message extends Component { // check if the message is a GIF if (gifEnabled && isGifMessage(text)) { - const url = text.substring(GIF_PREFIX.length, text.length - 1); + const url = extractGifURL(text); content.push( { - const url = message.substring(GIF_PREFIX.length, message.length - 1); + const url = extractGifURL(message); return ( Date: Wed, 28 Aug 2024 12:07:15 -0500 Subject: [PATCH 004/290] feat(shared-video): Closes confirm dialog if shown on stop. (#15065) * feat(shared-video): Closes confirm dialog if shown on stop. * squash: Show notification about the stopped video. --- lang/main.json | 3 ++- react/features/shared-video/actions.any.ts | 13 ++++++++++++- react/features/shared-video/middleware.any.ts | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lang/main.json b/lang/main.json index 401557058d03..b3914fc852b5 100644 --- a/lang/main.json +++ b/lang/main.json @@ -441,7 +441,8 @@ "shareScreenWarningTitle": "You need to stop audio sharing before sharing your screen", "shareVideoConfirmPlay": "You’re about to open an external website. Do you want to continue?", "shareVideoConfirmPlayTitle": "{{name}} has shared a video with you.", - "shareVideoLinkError": "Please provide a correct video link.", + "shareVideoLinkError": "Oops, this video cannot be played.", + "shareVideoLinkStopped": "The video from {{name}} was stopped.", "shareVideoTitle": "Share video", "shareYourScreen": "Share your screen", "shareYourScreenDisabled": "Screen sharing disabled.", diff --git a/react/features/shared-video/actions.any.ts b/react/features/shared-video/actions.any.ts index ba0ca210e239..d2a2fcafede3 100644 --- a/react/features/shared-video/actions.any.ts +++ b/react/features/shared-video/actions.any.ts @@ -1,6 +1,6 @@ import { IStore } from '../app/types'; import { getCurrentConference } from '../base/conference/functions'; -import { openDialog } from '../base/dialog/actions'; +import { hideDialog, openDialog } from '../base/dialog/actions'; import { getLocalParticipant } from '../base/participants/functions'; import { @@ -187,3 +187,14 @@ export function showConfirmPlayingDialog(actor: String, onSubmit: Function) { })); }; } + +/** + * Hides the video play confirmation dialog. + * + * @returns {Function} + */ +export function hideConfirmPlayingDialog() { + return (dispatch: IStore['dispatch']) => { + dispatch(hideDialog(ShareVideoConfirmDialog)); + }; +} diff --git a/react/features/shared-video/middleware.any.ts b/react/features/shared-video/middleware.any.ts index d41dec71506b..e9358102dc4b 100644 --- a/react/features/shared-video/middleware.any.ts +++ b/react/features/shared-video/middleware.any.ts @@ -12,9 +12,12 @@ import { getLocalParticipant, getParticipantById, getParticipantDisplayName } fr import { FakeParticipant } from '../base/participants/types'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import { SET_DYNAMIC_BRANDING_DATA } from '../dynamic-branding/actionTypes'; +import { showWarningNotification } from '../notifications/actions'; +import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants'; import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes'; import { + hideConfirmPlayingDialog, resetSharedVideoStatus, setAllowedUrlDomians, setSharedVideoStatus, @@ -84,6 +87,17 @@ MiddlewareRegistry.register(store => next => action => { if (sharedVideoStatus === 'stop') { const videoParticipant = getParticipantById(state, value); + if (getState()['features/shared-video'].confirmShowVideo === false) { + dispatch(showWarningNotification({ + titleKey: 'dialog.shareVideoLinkStopped', + titleArguments: { + name: getParticipantDisplayName(getState(), from) + } + }, NOTIFICATION_TIMEOUT_TYPE.LONG)); + } + + dispatch(hideConfirmPlayingDialog()); + dispatch(participantLeft(value, conference, { fakeParticipant: videoParticipant?.fakeParticipant })); From 2d56dbe2494270c817f55024c94ed888669edd85 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Thu, 22 Aug 2024 17:04:44 -0500 Subject: [PATCH 005/290] fix(subtitles): Styles. - Move the styles from css to tss-react ones - Dynamic fontSize based on the visible area of the page - Remove the gaps in the background when a line is wrapped. - Change the text color to white. - Remove transparency. --- css/_transcription-subtitles.scss | 26 ------- css/main.scss | 1 - react/features/base/ui/functions.web.ts | 13 +++- .../components/web/DisplayNameBadge.tsx | 4 +- .../web/StageParticipantNameLabel.tsx | 8 +- .../display-name/components/web/styles.ts | 26 +++++++ .../subtitles/components/web/Captions.tsx | 74 +++++++++++++++++-- react/features/subtitles/constants.ts | 4 + react/features/subtitles/functions.web.ts | 16 ++++ react/features/toolbox/constants.ts | 1 - 10 files changed, 133 insertions(+), 40 deletions(-) delete mode 100644 css/_transcription-subtitles.scss create mode 100644 react/features/display-name/components/web/styles.ts create mode 100644 react/features/subtitles/constants.ts diff --git a/css/_transcription-subtitles.scss b/css/_transcription-subtitles.scss deleted file mode 100644 index 9a125a308168..000000000000 --- a/css/_transcription-subtitles.scss +++ /dev/null @@ -1,26 +0,0 @@ -.transcription-subtitles { - bottom: $newToolbarSize + 40px; - font-size: 16px; - font-weight: 1000; - left: 50%; - max-width: 50vw; - opacity: 0.80; - overflow-wrap: break-word; - pointer-events: none; - position: absolute; - text-shadow: 0px 0px 1px rgba(0,0,0,0.3), - 0px 1px 1px rgba(0,0,0,0.3), - 1px 0px 1px rgba(0,0,0,0.3), - 0px 0px 1px rgba(0,0,0,0.3); - transform: translateX(-50%); - z-index: 7; - - &.lifted { - // Lift subtitle above toolbar+dominant speaker box. - bottom: $newToolbarSize + 36px + 40px; - } - - span { - background: black; - } -} diff --git a/css/main.scss b/css/main.scss index 8d85d145f9ac..dbc4365b3395 100644 --- a/css/main.scss +++ b/css/main.scss @@ -61,7 +61,6 @@ $flagsImagePath: "../images/"; @import 'filmstrip/vertical_filmstrip_overrides'; @import 'unsupported-browser/main'; @import 'deep-linking/main'; -@import 'transcription-subtitles'; @import '_meetings_list.scss'; @import 'navigate_section_list'; @import 'third-party-branding/google'; diff --git a/react/features/base/ui/functions.web.ts b/react/features/base/ui/functions.web.ts index 52277f8caaa6..e3e4cf952a63 100644 --- a/react/features/base/ui/functions.web.ts +++ b/react/features/base/ui/functions.web.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { adaptV4Theme, createTheme } from '@mui/material/styles'; +import { Theme, adaptV4Theme, createTheme } from '@mui/material/styles'; import { ITypography, IPalette as Palette1 } from '../ui/types'; @@ -107,3 +107,14 @@ export function operatesWithEnterKey(element: Element): boolean { return false; } + +/** + * Returns a common spacing from the bottom of the page for floating elements over the video space. + * + * @param {Theme} theme - The current theme. + * @param {boolean} isToolbarVisible - Whether the toolbar is visible or not. + * @returns {number} + */ +export function getVideospaceFloatingElementsBottomSpacing(theme: Theme, isToolbarVisible: boolean) { + return parseInt(isToolbarVisible ? theme.spacing(12) : theme.spacing(6), 10); +} diff --git a/react/features/display-name/components/web/DisplayNameBadge.tsx b/react/features/display-name/components/web/DisplayNameBadge.tsx index 4d1d4b367674..4fcc68820101 100644 --- a/react/features/display-name/components/web/DisplayNameBadge.tsx +++ b/react/features/display-name/components/web/DisplayNameBadge.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { makeStyles } from 'tss-react/mui'; +import { DISPLAY_NAME_VERTICAL_PADDING } from './styles'; + const useStyles = makeStyles()(theme => { const { text01 } = theme.palette; @@ -11,7 +13,7 @@ const useStyles = makeStyles()(theme => { color: text01, maxWidth: '50%', overflow: 'hidden', - padding: '2px 16px', + padding: `${DISPLAY_NAME_VERTICAL_PADDING / 2}px 16px`, textOverflow: 'ellipsis', whiteSpace: 'nowrap' } diff --git a/react/features/display-name/components/web/StageParticipantNameLabel.tsx b/react/features/display-name/components/web/StageParticipantNameLabel.tsx index f8b12f5df8e7..7a8e77416e5f 100644 --- a/react/features/display-name/components/web/StageParticipantNameLabel.tsx +++ b/react/features/display-name/components/web/StageParticipantNameLabel.tsx @@ -10,20 +10,22 @@ import { isWhiteboardParticipant } from '../../../base/participants/functions'; import { withPixelLineHeight } from '../../../base/styles/functions.web'; +import { getVideospaceFloatingElementsBottomSpacing } from '../../../base/ui/functions.web'; import { getLargeVideoParticipant } from '../../../large-video/functions'; import { isToolboxVisible } from '../../../toolbox/functions.web'; import { isLayoutTileView } from '../../../video-layout/functions.web'; import DisplayNameBadge from './DisplayNameBadge'; +import { getStageParticipantTypography } from './styles'; const useStyles = makeStyles()(theme => { return { badgeContainer: { - ...withPixelLineHeight(theme.typography.bodyShortRegularLarge), + ...withPixelLineHeight(getStageParticipantTypography(theme)), alignItems: 'center', display: 'inline-flex', justifyContent: 'center', - marginBottom: theme.spacing(7), + marginBottom: getVideospaceFloatingElementsBottomSpacing(theme, false), transition: 'margin-bottom 0.3s', pointerEvents: 'none', position: 'absolute', @@ -33,7 +35,7 @@ const useStyles = makeStyles()(theme => { zIndex: 1 }, containerElevated: { - marginBottom: theme.spacing(12) + marginBottom: getVideospaceFloatingElementsBottomSpacing(theme, true) } }; }); diff --git a/react/features/display-name/components/web/styles.ts b/react/features/display-name/components/web/styles.ts new file mode 100644 index 000000000000..7b157790133c --- /dev/null +++ b/react/features/display-name/components/web/styles.ts @@ -0,0 +1,26 @@ +import { Theme } from '@mui/material'; + +/** + * The vertical padding for the display name. + */ +export const DISPLAY_NAME_VERTICAL_PADDING = 4; + +/** + * Returns the typography for stage participant display name badge. + * + * @param {Theme} theme - The current theme. + * @returns {ITypographyType} + */ +export function getStageParticipantTypography(theme: Theme) { + return theme.typography.bodyShortRegularLarge; +} + +/** + * Returns the height + padding for stage participant display name badge. + * + * @param {Theme} theme - The current theme. + * @returns {number} + */ +export function getStageParticipantNameLabelHeight(theme: Theme) { + return getStageParticipantTypography(theme).lineHeight ?? 0 + DISPLAY_NAME_VERTICAL_PADDING; +} diff --git a/react/features/subtitles/components/web/Captions.tsx b/react/features/subtitles/components/web/Captions.tsx index 3226318c9c2a..7a7d4b754a7f 100644 --- a/react/features/subtitles/components/web/Captions.tsx +++ b/react/features/subtitles/components/web/Captions.tsx @@ -1,25 +1,85 @@ +import { Theme } from '@mui/material'; import React, { ReactElement } from 'react'; import { connect } from 'react-redux'; +import { withStyles } from 'tss-react/mui'; import { IReduxState } from '../../../app/types'; import { getLocalParticipant } from '../../../base/participants/functions'; +import { getVideospaceFloatingElementsBottomSpacing } from '../../../base/ui/functions.web'; +import { getStageParticipantNameLabelHeight } from '../../../display-name/components/web/styles'; import { getLargeVideoParticipant } from '../../../large-video/functions'; import { isLayoutTileView } from '../../../video-layout/functions.web'; +import { calculateSubtitlesFontSize } from '../../functions.web'; import { AbstractCaptions, type IAbstractCaptionsProps, _abstractMapStateToProps } from '../AbstractCaptions'; - interface IProps extends IAbstractCaptionsProps { + /** + * The height of the visible area. + */ + _clientHeight?: number; + /** * Whether the subtitles container is lifted above the invite box. */ _isLifted: boolean | undefined; + + /** + * An object containing the CSS classes. + */ + classes?: Partial, string>>; } + +const styles = (theme: Theme, props: IProps) => { + const { _isLifted = false, _clientHeight } = props; + const fontSize = calculateSubtitlesFontSize(_clientHeight); + const padding = Math.ceil(0.2 * fontSize); + + // Currently the subtitles position are not affected by the toolbar visibility. + let bottom = getVideospaceFloatingElementsBottomSpacing(theme, true); + + // This is the case where we display the onstage participant display name + // below the subtitles. + if (_isLifted) { + // 10px is the space between the onstage participant display name label and subtitles. We also need + // to add the padding of the subtitles because it will decrease the gap between the label and subtitles. + bottom += getStageParticipantNameLabelHeight(theme) + 10 + padding; + } + + return { + transcriptionSubtitles: { + bottom, + fontSize: `${fontSize}px`, + left: '50%', + maxWidth: '50vw', + overflowWrap: 'break-word' as const, + pointerEvents: 'none' as const, + position: 'absolute' as const, + textShadow: ` + 0px 0px 1px rgba(0,0,0,0.3), + 0px 1px 1px rgba(0,0,0,0.3), + 1px 0px 1px rgba(0,0,0,0.3), + 0px 0px 1px rgba(0,0,0,0.3)`, + transform: 'translateX(-50%)', + zIndex: 7, // The popups are with z-index 8. This z-index has to be lower. + lineHeight: 1.2, + + span: { + color: '#fff', + background: 'black', + + // without this when the text is wrapped on 2+ lines there will be a gap in the background: + padding: `${padding}px 0px` + } + } + }; +}; + /** * React {@code Component} which can display speech-to-text results from * Jigasi as subtitles. @@ -53,12 +113,10 @@ class Captions extends AbstractCaptions { * @returns {ReactElement} - The subtitles container. */ _renderSubtitlesContainer(paragraphs: Array): ReactElement { - const className = this.props._isLifted - ? 'transcription-subtitles lifted' - : 'transcription-subtitles'; + const classes = withStyles.getClasses(this.props); return ( -
+
{ paragraphs }
); @@ -77,11 +135,13 @@ function mapStateToProps(state: IReduxState) { const isTileView = isLayoutTileView(state); const largeVideoParticipant = getLargeVideoParticipant(state); const localParticipant = getLocalParticipant(state); + const { clientHeight } = state['features/base/responsive-ui']; return { ..._abstractMapStateToProps(state), - _isLifted: Boolean(largeVideoParticipant && largeVideoParticipant?.id !== localParticipant?.id && !isTileView) + _isLifted: Boolean(largeVideoParticipant && largeVideoParticipant?.id !== localParticipant?.id && !isTileView), + _clientHeight: clientHeight }; } -export default connect(mapStateToProps)(Captions); +export default connect(mapStateToProps)(withStyles(Captions, styles)); diff --git a/react/features/subtitles/constants.ts b/react/features/subtitles/constants.ts new file mode 100644 index 000000000000..06b291bbe484 --- /dev/null +++ b/react/features/subtitles/constants.ts @@ -0,0 +1,4 @@ +/** + * The minimum font size for subtitles. + */ +export const MIN_SUBTITLES_FONT_SIZE = 16; diff --git a/react/features/subtitles/functions.web.ts b/react/features/subtitles/functions.web.ts index 405df800de08..13e979e27ac2 100644 --- a/react/features/subtitles/functions.web.ts +++ b/react/features/subtitles/functions.web.ts @@ -1,5 +1,7 @@ /* eslint-disable max-params, max-len */ +import { MIN_SUBTITLES_FONT_SIZE } from './constants'; + /** * Logs when about the received transcription chunk. * @@ -17,3 +19,17 @@ export const notifyTranscriptionChunkReceived = (transcriptMessageID: string, la participant, ...text }); + +/** + * Calculates the font size for the subtitles. + * + * @param {number} clientHeight - The height of the visible area of the window. + * @returns {number} + */ +export function calculateSubtitlesFontSize(clientHeight?: number) { + if (typeof clientHeight === 'undefined') { + return MIN_SUBTITLES_FONT_SIZE; + } + + return Math.max(Math.floor(clientHeight * 0.04), MIN_SUBTITLES_FONT_SIZE); +} diff --git a/react/features/toolbox/constants.ts b/react/features/toolbox/constants.ts index dbaada03f509..b70e37f5cec3 100644 --- a/react/features/toolbox/constants.ts +++ b/react/features/toolbox/constants.ts @@ -84,7 +84,6 @@ export const ZINDEX_DIALOG_PORTAL = 302; */ export const SPINNER_COLOR = '#929292'; - /** * The list of all possible UI buttons. * From b747fd3483c66d0a583d92017251430f4fdcc387 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Fri, 23 Aug 2024 15:45:19 -0500 Subject: [PATCH 006/290] feat(subtitles): Move with toolbar. --- .../components/web/StageParticipantNameLabel.tsx | 7 ++++--- .../subtitles/components/web/Captions.tsx | 16 ++++++++++++---- react/features/toolbox/functions.web.ts | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/react/features/display-name/components/web/StageParticipantNameLabel.tsx b/react/features/display-name/components/web/StageParticipantNameLabel.tsx index 7a8e77416e5f..365c78be7d82 100644 --- a/react/features/display-name/components/web/StageParticipantNameLabel.tsx +++ b/react/features/display-name/components/web/StageParticipantNameLabel.tsx @@ -12,7 +12,7 @@ import { import { withPixelLineHeight } from '../../../base/styles/functions.web'; import { getVideospaceFloatingElementsBottomSpacing } from '../../../base/ui/functions.web'; import { getLargeVideoParticipant } from '../../../large-video/functions'; -import { isToolboxVisible } from '../../../toolbox/functions.web'; +import { getTransitionParamsForElementsAboveToolbox, isToolboxVisible } from '../../../toolbox/functions.web'; import { isLayoutTileView } from '../../../video-layout/functions.web'; import DisplayNameBadge from './DisplayNameBadge'; @@ -26,7 +26,7 @@ const useStyles = makeStyles()(theme => { display: 'inline-flex', justifyContent: 'center', marginBottom: getVideospaceFloatingElementsBottomSpacing(theme, false), - transition: 'margin-bottom 0.3s', + transition: `margin-bottom ${getTransitionParamsForElementsAboveToolbox(false)}`, pointerEvents: 'none', position: 'absolute', bottom: 0, @@ -35,7 +35,8 @@ const useStyles = makeStyles()(theme => { zIndex: 1 }, containerElevated: { - marginBottom: getVideospaceFloatingElementsBottomSpacing(theme, true) + marginBottom: getVideospaceFloatingElementsBottomSpacing(theme, true), + transition: `margin-bottom ${getTransitionParamsForElementsAboveToolbox(true)}` } }; }); diff --git a/react/features/subtitles/components/web/Captions.tsx b/react/features/subtitles/components/web/Captions.tsx index 7a7d4b754a7f..670680e858c2 100644 --- a/react/features/subtitles/components/web/Captions.tsx +++ b/react/features/subtitles/components/web/Captions.tsx @@ -8,6 +8,7 @@ import { getLocalParticipant } from '../../../base/participants/functions'; import { getVideospaceFloatingElementsBottomSpacing } from '../../../base/ui/functions.web'; import { getStageParticipantNameLabelHeight } from '../../../display-name/components/web/styles'; import { getLargeVideoParticipant } from '../../../large-video/functions'; +import { getTransitionParamsForElementsAboveToolbox, isToolboxVisible } from '../../../toolbox/functions.web'; import { isLayoutTileView } from '../../../video-layout/functions.web'; import { calculateSubtitlesFontSize } from '../../functions.web'; import { @@ -28,6 +29,11 @@ interface IProps extends IAbstractCaptionsProps { */ _isLifted: boolean | undefined; + /** + * Whether the toolbox is visible or not. + */ + _toolboxVisible: boolean; + /** * An object containing the CSS classes. */ @@ -36,12 +42,12 @@ interface IProps extends IAbstractCaptionsProps { const styles = (theme: Theme, props: IProps) => { - const { _isLifted = false, _clientHeight } = props; + const { _isLifted = false, _clientHeight, _toolboxVisible = false } = props; const fontSize = calculateSubtitlesFontSize(_clientHeight); const padding = Math.ceil(0.2 * fontSize); // Currently the subtitles position are not affected by the toolbar visibility. - let bottom = getVideospaceFloatingElementsBottomSpacing(theme, true); + let bottom = getVideospaceFloatingElementsBottomSpacing(theme, _toolboxVisible); // This is the case where we display the onstage participant display name // below the subtitles. @@ -53,7 +59,7 @@ const styles = (theme: Theme, props: IProps) => { return { transcriptionSubtitles: { - bottom, + bottom: `${bottom}px`, fontSize: `${fontSize}px`, left: '50%', maxWidth: '50vw', @@ -68,6 +74,7 @@ const styles = (theme: Theme, props: IProps) => { transform: 'translateX(-50%)', zIndex: 7, // The popups are with z-index 8. This z-index has to be lower. lineHeight: 1.2, + transition: `bottom ${getTransitionParamsForElementsAboveToolbox(_toolboxVisible)}`, span: { color: '#fff', @@ -140,7 +147,8 @@ function mapStateToProps(state: IReduxState) { return { ..._abstractMapStateToProps(state), _isLifted: Boolean(largeVideoParticipant && largeVideoParticipant?.id !== localParticipant?.id && !isTileView), - _clientHeight: clientHeight + _clientHeight: clientHeight, + _toolboxVisible: isToolboxVisible(state) }; } diff --git a/react/features/toolbox/functions.web.ts b/react/features/toolbox/functions.web.ts index cb6ee90d3aec..4c7187284f02 100644 --- a/react/features/toolbox/functions.web.ts +++ b/react/features/toolbox/functions.web.ts @@ -249,3 +249,17 @@ export function getVisibleButtons({ export function getParticipantMenuButtonsWithNotifyClick(state: IReduxState): Map { return state['features/toolbox'].participantMenuButtonsWithNotifyClick; } + + +/** + * Returns the time, timing function and delay for elements that are position above the toolbar and need to move along + * with the toolbar. + * + * @param {boolean} isToolbarVisible - Whether the toolbar is visible or not. + * @returns {string} + */ +export function getTransitionParamsForElementsAboveToolbox(isToolbarVisible: boolean) { + // The transistion time and delay is different to account for the time when the toolbar is about to hide/show but + // the elements don't have to move. + return isToolbarVisible ? '0.15s ease-in 0.15s' : '0.24s ease-in 0s'; +} From aa122c965283293db812c515dc3eb4f434de383e Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Thu, 29 Aug 2024 15:43:15 -0500 Subject: [PATCH 007/290] fix(subtitles): positioning and padding --- .../subtitles/components/web/Captions.tsx | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/react/features/subtitles/components/web/Captions.tsx b/react/features/subtitles/components/web/Captions.tsx index 670680e858c2..e33b26be03d3 100644 --- a/react/features/subtitles/components/web/Captions.tsx +++ b/react/features/subtitles/components/web/Captions.tsx @@ -29,6 +29,11 @@ interface IProps extends IAbstractCaptionsProps { */ _isLifted: boolean | undefined; + /** + * Whether toolbar is shifted up or not. + */ + _shiftUp: boolean; + /** * Whether the toolbox is visible or not. */ @@ -42,24 +47,32 @@ interface IProps extends IAbstractCaptionsProps { const styles = (theme: Theme, props: IProps) => { - const { _isLifted = false, _clientHeight, _toolboxVisible = false } = props; + const { _isLifted = false, _clientHeight, _shiftUp = false, _toolboxVisible = false } = props; const fontSize = calculateSubtitlesFontSize(_clientHeight); - const padding = Math.ceil(0.2 * fontSize); - // Currently the subtitles position are not affected by the toolbar visibility. - let bottom = getVideospaceFloatingElementsBottomSpacing(theme, _toolboxVisible); + // Normally we would use 0.2 * fontSize in order to cover the background gap from line-height: 1.2 but it seems + // the current font is a little bit larger than it is supposed to be. + const padding = 0.1 * fontSize; + const bottom = getVideospaceFloatingElementsBottomSpacing(theme, _toolboxVisible); + let marginBottom = 0; // This is the case where we display the onstage participant display name // below the subtitles. if (_isLifted) { // 10px is the space between the onstage participant display name label and subtitles. We also need // to add the padding of the subtitles because it will decrease the gap between the label and subtitles. - bottom += getStageParticipantNameLabelHeight(theme) + 10 + padding; + marginBottom += getStageParticipantNameLabelHeight(theme) + 10 + padding; + } + + if (_shiftUp) { + // The toolbar is shifted up with 30px from the css. + marginBottom += 30; } return { transcriptionSubtitles: { bottom: `${bottom}px`, + marginBottom, fontSize: `${fontSize}px`, left: '50%', maxWidth: '50vw', @@ -81,7 +94,8 @@ const styles = (theme: Theme, props: IProps) => { background: 'black', // without this when the text is wrapped on 2+ lines there will be a gap in the background: - padding: `${padding}px 0px` + padding: `${padding}px 8px`, + boxDecorationBreak: 'clone' as const } } }; @@ -148,6 +162,7 @@ function mapStateToProps(state: IReduxState) { ..._abstractMapStateToProps(state), _isLifted: Boolean(largeVideoParticipant && largeVideoParticipant?.id !== localParticipant?.id && !isTileView), _clientHeight: clientHeight, + _shiftUp: state['features/toolbox'].shiftUp, _toolboxVisible: isToolboxVisible(state) }; } From ad6e675b1853449c228be293e8a95b397a852f5c Mon Sep 17 00:00:00 2001 From: damencho Date: Thu, 29 Aug 2024 11:41:41 -0500 Subject: [PATCH 008/290] fix(visitors): When metadata or flag live is missing, consider live. --- resources/prosody-plugins/mod_visitors_component.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/prosody-plugins/mod_visitors_component.lua b/resources/prosody-plugins/mod_visitors_component.lua index 0b33644da79b..4d52ccc0ac3c 100644 --- a/resources/prosody-plugins/mod_visitors_component.lua +++ b/resources/prosody-plugins/mod_visitors_component.lua @@ -312,7 +312,9 @@ local function go_live(room) return; end - if not (room.jitsiMetadata and room.jitsiMetadata.visitors and room.jitsiMetadata.visitors.live) then + -- if missing we assume room is live, only skip if it is marked explicitly as false + if room.jitsiMetadata and room.jitsiMetadata.visitors + and room.jitsiMetadata.visitors.live and room.jitsiMetadata.visitors.live == false then return; end From d755b9decbd37489ab3c0fecfcd9661bcf422ab1 Mon Sep 17 00:00:00 2001 From: damencho Date: Mon, 26 Aug 2024 08:47:31 -0500 Subject: [PATCH 009/290] fix(avatar): Prefer avatar url from jwt identity. --- conference.js | 18 ++++++++++++------ modules/API/API.js | 4 +++- react/features/base/conference/actions.any.ts | 19 +++++++++++++------ react/features/base/conference/functions.ts | 2 ++ 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/conference.js b/conference.js index 197a64a50ce7..4af2765d6c20 100644 --- a/conference.js +++ b/conference.js @@ -111,6 +111,7 @@ import { import { getLocalParticipant, getNormalizedDisplayName, + getParticipantByIdOrUndefined, getVirtualScreenshareParticipantByOwnerId } from './react/features/base/participants/functions'; import { updateSettings } from './react/features/base/settings/actions'; @@ -1775,12 +1776,17 @@ export default { room.addCommandListener( this.commands.defaults.AVATAR_URL, (data, from) => { - APP.store.dispatch( - participantUpdated({ - conference: room, - id: from, - avatarURL: data.value - })); + const participant = getParticipantByIdOrUndefined(APP.store, from); + + // if already set from presence(jwt), skip the command processing + if (!participant?.avatarURL) { + APP.store.dispatch( + participantUpdated({ + conference: room, + id: from, + avatarURL: data.value + })); + } }); room.on( diff --git a/modules/API/API.js b/modules/API/API.js index d4887e3d7a42..10a7c8a1fd14 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -489,7 +489,9 @@ function initCommands() { sendAnalytics(createApiEvent('email.changed')); APP.conference.changeLocalEmail(email); }, - 'avatar-url': avatarUrl => { + 'avatar-url': avatarUrl => { // @deprecated + console.warn('Using command avatarUrl is deprecated. Use context.user.avatar in the jwt.'); + sendAnalytics(createApiEvent('avatar.url.changed')); APP.conference.changeLocalAvatarUrl(avatarUrl); }, diff --git a/react/features/base/conference/actions.any.ts b/react/features/base/conference/actions.any.ts index 64a00b397992..86da22e2f4a8 100644 --- a/react/features/base/conference/actions.any.ts +++ b/react/features/base/conference/actions.any.ts @@ -25,7 +25,7 @@ import { participantSourcesUpdated, participantUpdated } from '../participants/actions'; -import { getNormalizedDisplayName } from '../participants/functions'; +import { getNormalizedDisplayName, getParticipantByIdOrUndefined } from '../participants/functions'; import { IJitsiParticipant } from '../participants/types'; import { toState } from '../redux/functions'; import { @@ -277,11 +277,18 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[ conference.addCommandListener( AVATAR_URL_COMMAND, - (data: { value: string; }, id: string) => dispatch(participantUpdated({ - conference, - id, - avatarURL: data.value - }))); + (data: { value: string; }, id: string) => { + const participant = getParticipantByIdOrUndefined(state, id); + + // if already set from presence(jwt), skip the command processing + if (!participant?.avatarURL) { + return dispatch(participantUpdated({ + conference, + id, + avatarURL: data.value + })); + } + }); conference.addCommandListener( EMAIL_COMMAND, (data: { value: string; }, id: string) => dispatch(participantUpdated({ diff --git a/react/features/base/conference/functions.ts b/react/features/base/conference/functions.ts index f08224d11829..14d86e281562 100644 --- a/react/features/base/conference/functions.ts +++ b/react/features/base/conference/functions.ts @@ -90,7 +90,9 @@ export function commonUserJoinedHandling( } else { const isReplacing = user?.isReplacing(); + // the identity and avatar come from jwt and never change in the presence dispatch(participantJoined({ + avatarURL: user.getIdentity()?.user?.avatar, botType: user.getBotType(), conference, id, From 79322f6a1f4628a851b6eebb4f33f6ce1de98d95 Mon Sep 17 00:00:00 2001 From: Aaron van Meerten Date: Tue, 3 Sep 2024 09:00:44 -0400 Subject: [PATCH 010/290] fix(jicofo): conference request nginx config add expose headers for cors (#15084) * fix(jicofo): conference request nginx config add expose headers for content type --- doc/debian/jitsi-meet/jitsi-meet.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/debian/jitsi-meet/jitsi-meet.example b/doc/debian/jitsi-meet/jitsi-meet.example index 5b96ec90bb60..23e781d1037e 100644 --- a/doc/debian/jitsi-meet/jitsi-meet.example +++ b/doc/debian/jitsi-meet/jitsi-meet.example @@ -154,6 +154,8 @@ server { proxy_pass http://127.0.0.1:8888/conference-request/v1$1; add_header "Cache-Control" "no-cache, no-store"; add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Content-Type'; } location ~ ^/([^/?&:'"]+)/conference-request/v1(\/.*)?$ { rewrite ^/([^/?&:'"]+)/conference-request/v1(\/.*)?$ /conference-request/v1$2; From d2ff136c1598bb3e286fc6510a54d8a4534872d8 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 3 Sep 2024 14:04:32 -0500 Subject: [PATCH 011/290] feat(participants-pane): Fixes actions menu when sharing video. We have actions menu for the video just for the local participant who shared the video. --- .../participants-pane/components/web/MeetingParticipantItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react/features/participants-pane/components/web/MeetingParticipantItem.tsx b/react/features/participants-pane/components/web/MeetingParticipantItem.tsx index 68f78f182621..7533748e0b44 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantItem.tsx +++ b/react/features/participants-pane/components/web/MeetingParticipantItem.tsx @@ -280,7 +280,7 @@ function MeetingParticipantItem({ } - {!overflowDrawer && (_localVideoOwner || _participant?.fakeParticipant) && ( + {!overflowDrawer && (_localVideoOwner && _participant?.fakeParticipant) && ( From 6aa42f985093402f4cac9fcd4faad1f60c8e37be Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 3 Sep 2024 14:07:16 -0500 Subject: [PATCH 012/290] feat(shared-video): Fixes showing thumb on the sharer side. Fixes #15077. --- react/features/shared-video/actions.any.ts | 15 +++--- react/features/shared-video/functions.ts | 28 ++++++++++ react/features/shared-video/middleware.any.ts | 54 ++++++++----------- 3 files changed, 60 insertions(+), 37 deletions(-) diff --git a/react/features/shared-video/actions.any.ts b/react/features/shared-video/actions.any.ts index d2a2fcafede3..6498814068a3 100644 --- a/react/features/shared-video/actions.any.ts +++ b/react/features/shared-video/actions.any.ts @@ -11,7 +11,7 @@ import { } from './actionTypes'; import { ShareVideoConfirmDialog, SharedVideoDialog } from './components'; import { PLAYBACK_START, PLAYBACK_STATUSES } from './constants'; -import { isSharedVideoEnabled } from './functions'; +import { isSharedVideoEnabled, sendShareVideoCommand } from './functions'; /** @@ -120,12 +120,15 @@ export function playSharedVideo(videoUrl: string) { if (conference) { const localParticipant = getLocalParticipant(getState()); - dispatch(setSharedVideoStatus({ - videoUrl, + // we will send the command and will create local video fake participant + // and start playing once we receive ourselves the command + sendShareVideoCommand({ + conference, + id: videoUrl, + localParticipantId: localParticipant?.id, status: PLAYBACK_START, - time: 0, - ownerId: localParticipant?.id - })); + time: 0 + }); } }; } diff --git a/react/features/shared-video/functions.ts b/react/features/shared-video/functions.ts index f29d47f1a8fd..597198c54a1d 100644 --- a/react/features/shared-video/functions.ts +++ b/react/features/shared-video/functions.ts @@ -1,4 +1,5 @@ import { IStateful } from '../base/app/types'; +import { IJitsiConference } from '../base/conference/reducer'; import { getFakeParticipants } from '../base/participants/functions'; import { toState } from '../base/redux/functions'; @@ -6,6 +7,7 @@ import { ALLOW_ALL_URL_DOMAINS, PLAYBACK_START, PLAYBACK_STATUSES, + SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME, YOUTUBE_URL_DOMAIN @@ -146,3 +148,29 @@ export function isURLAllowedForSharedVideo(url: string, return false; } + +/** + * Sends SHARED_VIDEO command. + * + * @param {string} id - The id of the video. + * @param {string} status - The status of the shared video. + * @param {JitsiConference} conference - The current conference. + * @param {string} localParticipantId - The id of the local participant. + * @param {string} time - The seek position of the video. + * @returns {void} + */ +export function sendShareVideoCommand({ id, status, conference, localParticipantId = '', time, muted, volume }: { + conference?: IJitsiConference; id: string; localParticipantId?: string; muted?: boolean; + status: string; time: number; volume?: number; +}) { + conference?.sendCommandOnce(SHARED_VIDEO, { + value: id, + attributes: { + from: localParticipantId, + muted, + state: status, + time, + volume + } + }); +} diff --git a/react/features/shared-video/middleware.any.ts b/react/features/shared-video/middleware.any.ts index e9358102dc4b..6e1662452b2b 100644 --- a/react/features/shared-video/middleware.any.ts +++ b/react/features/shared-video/middleware.any.ts @@ -30,7 +30,7 @@ import { SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants'; -import { isSharedVideoEnabled, isSharingStatus, isURLAllowedForSharedVideo } from './functions'; +import { isSharedVideoEnabled, isSharingStatus, isURLAllowedForSharedVideo, sendShareVideoCommand } from './functions'; import logger from './logger'; @@ -155,6 +155,14 @@ MiddlewareRegistry.register(store => next => action => { APP.API.notifyAudioOrVideoSharingToggled(MEDIA_TYPE.VIDEO, status, ownerId); } + // when setting status we need to send the command for that, but not do it for the start command + // as we are sending the command in playSharedVideo and setting the start status once + // we receive the response, this way we will start the video at the same time when remote participants + // start it, on receiving the command + if (status === 'start') { + break; + } + if (localParticipantId === ownerId) { sendShareVideoCommand({ conference, @@ -224,12 +232,15 @@ function handleSharingVideoStatus(store: IStore, videoUrl: string, if (oldVideoUrl && oldVideoUrl !== videoUrl) { logger.warn( - `User with id: ${localParticipantId} sent videoUrl: ${videoUrl} while we are playing: ${oldVideoUrl}`); + `User with id: ${from} sent videoUrl: ${videoUrl} while we are playing: ${oldVideoUrl}`); return; } - if (state === PLAYBACK_START && !isSharingStatus(oldStatus)) { + // If the video was not started (no participant) we want to create the participant + // this can be triggered by start, but also by paused or playing + // commands (joining late) and getting the current state + if (state === PLAYBACK_START || !isSharingStatus(oldStatus)) { const youtubeId = videoUrl.match(/http/) ? false : videoUrl; const avatarURL = youtubeId ? `https://img.youtube.com/vi/${youtubeId}/0.jpg` : ''; @@ -242,6 +253,15 @@ function handleSharingVideoStatus(store: IStore, videoUrl: string, })); dispatch(pinParticipant(videoUrl)); + + if (localParticipantId === from) { + dispatch(setSharedVideoStatus({ + videoUrl, + status: state, + time: Number(time), + ownerId: localParticipantId + })); + } } if (localParticipantId !== from) { @@ -254,31 +274,3 @@ function handleSharingVideoStatus(store: IStore, videoUrl: string, })); } } - -/* eslint-disable max-params */ - -/** - * Sends SHARED_VIDEO command. - * - * @param {string} id - The id of the video. - * @param {string} status - The status of the shared video. - * @param {JitsiConference} conference - The current conference. - * @param {string} localParticipantId - The id of the local participant. - * @param {string} time - The seek position of the video. - * @returns {void} - */ -function sendShareVideoCommand({ id, status, conference, localParticipantId = '', time, muted, volume }: { - conference?: IJitsiConference; id: string; localParticipantId?: string; muted: boolean; - status: string; time: number; volume: number; -}) { - conference?.sendCommandOnce(SHARED_VIDEO, { - value: id, - attributes: { - from: localParticipantId, - muted, - state: status, - time, - volume - } - }); -} From 98020163ce945b4085d20dd56a01ed85204bf213 Mon Sep 17 00:00:00 2001 From: Sebastian Wieseler Date: Wed, 4 Sep 2024 22:47:40 +0800 Subject: [PATCH 013/290] Update SECURITY.md (#15085) --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 58d25b8ac8c8..a9f668aa9616 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,6 +4,6 @@ We take security very seriously and develop all Jitsi projects to be secure and safe. -If you find (or simply suspect) a security issue in any of the Jitsi projects, please report it to us via [HackerOne](https://hackerone.com/8x8) or send us an email to security@jitsi.org. +If you find (or simply suspect) a security issue in any of the Jitsi projects, please report it to us via [HackerOne](https://hackerone.com/8x8-bounty) or send us an email to security@jitsi.org. **We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.** From 716914394203f593e559279a202c0ecebedac1ef Mon Sep 17 00:00:00 2001 From: damencho Date: Wed, 4 Sep 2024 10:56:14 -0500 Subject: [PATCH 014/290] chore(deps) lib-jitsi-meet@latest https://github.com/jitsi/lib-jitsi-meet/compare/v1852.0.0+526ec25d...v1858.0.0+6771b695 --- package-lock.json | 28 +++++++--------------------- package.json | 2 +- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index d753f5890fa4..c183e116de1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1852.0.0+526ec25d/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1858.0.0+6771b695/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", @@ -12498,8 +12498,8 @@ }, "node_modules/lib-jitsi-meet": { "version": "0.0.0", - "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1852.0.0+526ec25d/lib-jitsi-meet.tgz", - "integrity": "sha512-zFt1dzJfSbLejjZUR0FAsGxQIzF/RnLqFmxnHDACaiEIbZhxmyw/Y4rQjLO7FRrmJNSP37U4cuiTi5m/AwP2PA==", + "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1858.0.0+6771b695/lib-jitsi-meet.tgz", + "integrity": "sha512-buY5U+DOokFfiPY12ZpgiE7B/sAEMAKwMymc8d4eYsgnTnHPihZI+P7EYq8uIgC8S+jh4t2V5L+yWifpb7clFw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -12512,9 +12512,7 @@ "base64-js": "1.3.1", "current-executing-script": "0.1.3", "jquery": "3.6.1", - "lodash.clonedeep": "4.5.0", - "lodash.debounce": "4.0.8", - "lodash.isequal": "4.5.0", + "lodash-es": "4.17.21", "patch-package": "6.5.1", "promise.allsettled": "1.0.4", "sdp-transform": "2.3.0", @@ -12856,11 +12854,6 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", @@ -28013,8 +28006,8 @@ } }, "lib-jitsi-meet": { - "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1852.0.0+526ec25d/lib-jitsi-meet.tgz", - "integrity": "sha512-zFt1dzJfSbLejjZUR0FAsGxQIzF/RnLqFmxnHDACaiEIbZhxmyw/Y4rQjLO7FRrmJNSP37U4cuiTi5m/AwP2PA==", + "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1858.0.0+6771b695/lib-jitsi-meet.tgz", + "integrity": "sha512-buY5U+DOokFfiPY12ZpgiE7B/sAEMAKwMymc8d4eYsgnTnHPihZI+P7EYq8uIgC8S+jh4t2V5L+yWifpb7clFw==", "requires": { "@jitsi/js-utils": "2.2.1", "@jitsi/logger": "2.0.2", @@ -28025,9 +28018,7 @@ "base64-js": "1.3.1", "current-executing-script": "0.1.3", "jquery": "3.6.1", - "lodash.clonedeep": "4.5.0", - "lodash.debounce": "4.0.8", - "lodash.isequal": "4.5.0", + "lodash-es": "4.17.21", "patch-package": "6.5.1", "promise.allsettled": "1.0.4", "sdp-transform": "2.3.0", @@ -28300,11 +28291,6 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", diff --git a/package.json b/package.json index e0c646ce5ad4..cf89351ec0e3 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1852.0.0+526ec25d/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1858.0.0+6771b695/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", From d2afd5e54d3f33d8f946aec68bf94b0c8121373e Mon Sep 17 00:00:00 2001 From: Jaya Allamsetty Date: Wed, 4 Sep 2024 16:26:55 -0400 Subject: [PATCH 015/290] chore(deps) lib-jitsi-meet@latest https://github.com/jitsi/lib-jitsi-meet/compare/v1858.0.0+6771b695...v1859.0.0+9ff77a91 --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c183e116de1f..bfbfc1e43290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1858.0.0+6771b695/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1859.0.0+9ff77a91/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", @@ -12498,8 +12498,8 @@ }, "node_modules/lib-jitsi-meet": { "version": "0.0.0", - "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1858.0.0+6771b695/lib-jitsi-meet.tgz", - "integrity": "sha512-buY5U+DOokFfiPY12ZpgiE7B/sAEMAKwMymc8d4eYsgnTnHPihZI+P7EYq8uIgC8S+jh4t2V5L+yWifpb7clFw==", + "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1859.0.0+9ff77a91/lib-jitsi-meet.tgz", + "integrity": "sha512-G7nNbRaxqoAICXVCkWYDr4hJdC/5rQaDxCi+J+nCImInBV3SKQgqev4r+95qa5OAnWrsjPXwrRldySdkzWhYXg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -28006,8 +28006,8 @@ } }, "lib-jitsi-meet": { - "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1858.0.0+6771b695/lib-jitsi-meet.tgz", - "integrity": "sha512-buY5U+DOokFfiPY12ZpgiE7B/sAEMAKwMymc8d4eYsgnTnHPihZI+P7EYq8uIgC8S+jh4t2V5L+yWifpb7clFw==", + "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1859.0.0+9ff77a91/lib-jitsi-meet.tgz", + "integrity": "sha512-G7nNbRaxqoAICXVCkWYDr4hJdC/5rQaDxCi+J+nCImInBV3SKQgqev4r+95qa5OAnWrsjPXwrRldySdkzWhYXg==", "requires": { "@jitsi/js-utils": "2.2.1", "@jitsi/logger": "2.0.2", diff --git a/package.json b/package.json index cf89351ec0e3..4bcf00c436f3 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1858.0.0+6771b695/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1859.0.0+9ff77a91/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", From 0fa02ff6baff3b58e61a32ebd2db650bfccac10a Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Fri, 6 Sep 2024 09:49:06 -0500 Subject: [PATCH 016/290] fix(devices): Do not select stored devices that are not available. --- react/features/base/settings/functions.web.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/react/features/base/settings/functions.web.ts b/react/features/base/settings/functions.web.ts index a3753c97b302..71423cafbb09 100644 --- a/react/features/base/settings/functions.web.ts +++ b/react/features/base/settings/functions.web.ts @@ -178,18 +178,20 @@ function _getUserSelectedDeviceId(options: { replacement = '' } = options; - // If there is no label at all, there is no need to fall back to checking - // the label for a fuzzy match. - if (!userSelectedDeviceLabel || !userSelectedDeviceId) { - return userSelectedDeviceId; - } + if (userSelectedDeviceId) { + const foundMatchingBasedonDeviceId = availableDevices?.find( + candidate => candidate.deviceId === userSelectedDeviceId); - const foundMatchingBasedonDeviceId = availableDevices?.find( - candidate => candidate.deviceId === userSelectedDeviceId); + // Prioritize matching the deviceId + if (foundMatchingBasedonDeviceId) { + return userSelectedDeviceId; + } + } - // Prioritize matching the deviceId - if (foundMatchingBasedonDeviceId) { - return userSelectedDeviceId; + // If there is no label at all, there is no need to fall back to checking + // the label for a fuzzy match. + if (!userSelectedDeviceLabel) { + return; } const strippedDeviceLabel From b989307c1e66a0e4353fd94efd54d2d2f8c7f08b Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 6 Sep 2024 11:52:25 -0500 Subject: [PATCH 017/290] feat(visitors): Adds option to turn off auto promotion with token. Fixes #14699. --- resources/prosody-plugins/mod_fmuc.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/resources/prosody-plugins/mod_fmuc.lua b/resources/prosody-plugins/mod_fmuc.lua index 496c5148c3ed..e1588ccd73f5 100644 --- a/resources/prosody-plugins/mod_fmuc.lua +++ b/resources/prosody-plugins/mod_fmuc.lua @@ -43,6 +43,9 @@ local local_muc_domain = muc_domain_prefix..'.'..local_domain; local NICK_NS = 'http://jabber.org/protocol/nick'; +-- in certain cases we consider participants with token as moderators, this is the default behavior which can be turned off +local auto_promoted_with_token = module:get_option_boolean('visitors_auto_promoted_with_token', true); + -- we send stats for the total number of rooms, total number of participants and total number of visitors local measure_rooms = module:measure('vnode-rooms', 'amount'); local measure_participants = module:measure('vnode-participants', 'amount'); @@ -251,8 +254,8 @@ module:hook('muc-broadcast-presence', function (event) if room._data.moderator_id == user_id then is_moderator = true; end - elseif session.auth_token then - -- non-vpass and having a token is considered a moderator + elseif session.auth_token and auto_promoted_with_token then + -- non-vpaas and having a token is considered a moderator is_moderator = true; end end From ac720034ab5a9f9359972d1ffa9a21e26eb95bc7 Mon Sep 17 00:00:00 2001 From: Christoph Settgast Date: Sun, 8 Sep 2024 19:12:35 +0200 Subject: [PATCH 018/290] lang: update German translation Signed-off-by: Christoph Settgast --- lang/main-de.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lang/main-de.json b/lang/main-de.json index 081fe1d8d3f1..382318e214e7 100644 --- a/lang/main-de.json +++ b/lang/main-de.json @@ -439,7 +439,10 @@ "shareScreenWarningD2": "müssen Sie Ihre Audiofreigabe stoppen und dann die Bildschirmfreigabe mit der Option \"Audio freigeben\" starten.", "shareScreenWarningH1": "Wenn Sie Ihren Bildschirm freigeben wollen:", "shareScreenWarningTitle": "Sie müssen die Audiofreigabe beenden, bevor Sie den Bildschirm freigeben können", + "shareVideoConfirmPlay": "Sie öffnen dazu eine externe Seite. Möchten Sie fortfahren?", + "shareVideoConfirmPlayTitle": "{{name}} hat mit Ihnen ein Video geteilt.", "shareVideoLinkError": "Bitte einen gültigen Link angeben.", + "shareVideoLinkStopped": "Das Video von {{name}} wurde gestoppt.", "shareVideoTitle": "Video teilen", "shareYourScreen": "Bildschirmfreigabe ein-/ausschalten", "shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.", @@ -786,6 +789,7 @@ "newDeviceAction": "Verwenden", "newDeviceAudioTitle": "Neues Audiogerät erkannt", "newDeviceCameraTitle": "Neue Kamera erkannt", + "nextToSpeak": "Sie sind als Nächstes an der Reihe zu sprechen", "noiseSuppressionDesktopAudioDescription": "Die Rauschunterdrückung kann nicht genutzt werden, wenn der Computersound geteilt wird, bitte zuerst deaktivieren und dann nochmals versuchen.", "noiseSuppressionFailedTitle": "Rauschunterdrückung konnte nicht gestartet werden", "noiseSuppressionStereoDescription": "Rauschunterdrückung unterstützt aktuell keinen Stereoton.", From 15ddf041899da4078953c8454211609b2e0a39df Mon Sep 17 00:00:00 2001 From: Avram Tudor Date: Mon, 9 Sep 2024 11:44:06 +0300 Subject: [PATCH 019/290] fix: correct inconsistencies between disableLocalVideoFlip flag and UI (#15101) Some parts of the ui still showed the setting for flipping the video, even if the flag indicated otherwise Also fixes the case where setting a virtual background ignores the stored localFlipX setting --- react/features/base/tracks/actions.any.ts | 3 ++- .../components/VideoDeviceSelection.web.tsx | 22 +++++++++++----- .../device-selection/functions.web.ts | 2 ++ react/features/settings/actions.web.ts | 3 ++- .../web/video/VideoSettingsContent.tsx | 26 +++++++++++++------ 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/react/features/base/tracks/actions.any.ts b/react/features/base/tracks/actions.any.ts index d8ba48f05332..65e7b2a0e6c5 100644 --- a/react/features/base/tracks/actions.any.ts +++ b/react/features/base/tracks/actions.any.ts @@ -833,6 +833,7 @@ export function toggleCamera() { const tracks = state['features/base/tracks']; const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack; const currentFacingMode = localVideoTrack.getCameraFacingMode(); + const { localFlipX } = state['features/base/settings']; /** * FIXME: Ideally, we should be dispatching {@code replaceLocalTrack} here, @@ -848,7 +849,7 @@ export function toggleCamera() { : CAMERA_FACING_MODE.USER; // Update the flipX value so the environment facing camera is not flipped, before the new track is created. - dispatch(updateSettings({ localFlipX: targetFacingMode === CAMERA_FACING_MODE.USER })); + dispatch(updateSettings({ localFlipX: targetFacingMode === CAMERA_FACING_MODE.USER ? localFlipX : false })); const newVideoTrack = await createLocalTrack('video', null, null, { facingMode: targetFacingMode }); diff --git a/react/features/device-selection/components/VideoDeviceSelection.web.tsx b/react/features/device-selection/components/VideoDeviceSelection.web.tsx index dc604b61c683..e2f3132e51c9 100644 --- a/react/features/device-selection/components/VideoDeviceSelection.web.tsx +++ b/react/features/device-selection/components/VideoDeviceSelection.web.tsx @@ -51,6 +51,11 @@ export interface IProps extends AbstractDialogTabProps, WithTranslation { */ disableDeviceChange: boolean; + /** + * Whether the local video can be flipped or not. + */ + disableLocalVideoFlip: boolean | undefined; + /** * Whether video input dropdown should be enabled or not. */ @@ -203,6 +208,7 @@ class VideoDeviceSelection extends AbstractDialogTab { */ render() { const { + disableLocalVideoFlip, hideAdditionalSettings, hideVideoInputPreview, localFlipX, @@ -225,13 +231,15 @@ class VideoDeviceSelection extends AbstractDialogTab {
{!hideAdditionalSettings && ( <> -
- super._onChange({ localFlipX: !localFlipX }) } /> -
+ {!disableLocalVideoFlip && ( +
+ super._onChange({ localFlipX: !localFlipX }) } /> +
+ )} {this._renderFramerateSelect()} )} diff --git a/react/features/device-selection/functions.web.ts b/react/features/device-selection/functions.web.ts index 920c2abe36b3..e2fd11abd176 100644 --- a/react/features/device-selection/functions.web.ts +++ b/react/features/device-selection/functions.web.ts @@ -105,6 +105,7 @@ export function getVideoDeviceSelectionDialogProps(stateful: IStateful, isDispla const inputDeviceChangeSupported = JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('input'); const userSelectedCamera = getUserSelectedCameraDeviceId(state); const { localFlipX } = state['features/base/settings']; + const { disableLocalVideoFlip } = state['features/base/config']; const hideAdditionalSettings = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage; const framerate = state['features/screen-share'].captureFrameRate ?? SS_DEFAULT_FRAME_RATE; @@ -127,6 +128,7 @@ export function getVideoDeviceSelectionDialogProps(stateful: IStateful, isDispla desktopShareFramerates: SS_SUPPORTED_FRAMERATES, disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(), disableVideoInputSelect, + disableLocalVideoFlip, hasVideoPermission: permissions.video, hideAdditionalSettings, hideVideoInputPreview: !inputDeviceChangeSupported || disablePreviews, diff --git a/react/features/settings/actions.web.ts b/react/features/settings/actions.web.ts index bcc5a5d20005..c14fff75316c 100644 --- a/react/features/settings/actions.web.ts +++ b/react/features/settings/actions.web.ts @@ -304,6 +304,7 @@ export function submitVirtualBackgroundTab(newState: any, isCancel = false) { return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const state = getState(); const track = getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack; + const { localFlipX } = state['features/base/settings']; if (newState.options?.selectedThumbnail) { await dispatch(toggleBackgroundEffect(newState.options, track)); @@ -311,7 +312,7 @@ export function submitVirtualBackgroundTab(newState: any, isCancel = false) { if (!isCancel) { // Set x scale to default value. dispatch(updateSettings({ - localFlipX: true + localFlipX })); virtualBackgroundLogger.info(`Virtual background type: '${ diff --git a/react/features/settings/components/web/video/VideoSettingsContent.tsx b/react/features/settings/components/web/video/VideoSettingsContent.tsx index b69d0be161a0..bfc0eee7ddb8 100644 --- a/react/features/settings/components/web/video/VideoSettingsContent.tsx +++ b/react/features/settings/components/web/video/VideoSettingsContent.tsx @@ -33,6 +33,11 @@ export interface IProps { */ currentCameraDeviceId: string; + /** + * Whether the local video flip is disabled. + */ + disableLocalVideoFlip: boolean | undefined; + /** * Whether or not the local video is flipped. */ @@ -146,6 +151,7 @@ const stopPropagation = (e: React.MouseEvent) => { const VideoSettingsContent = ({ changeFlip, currentCameraDeviceId, + disableLocalVideoFlip, localFlipX, selectBackground, setVideoInputDevice, @@ -310,23 +316,27 @@ const VideoSettingsContent = ({ icon = { IconImage } onClick = { selectBackground } text = { t('virtualBackground.title') } /> } -
- -
+ {!disableLocalVideoFlip && ( +
+ +
+ )} ); }; const mapStateToProps = (state: IReduxState) => { + const { disableLocalVideoFlip } = state['features/base/config']; const { localFlipX } = state['features/base/settings']; return { + disableLocalVideoFlip, localFlipX: Boolean(localFlipX), visibleVirtualBackground: checkBlurSupport() && checkVirtualBackgroundEnabled(state) From 01ef23402ec34686dca9cf5498c7cb01ed2ea38f Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 10 Sep 2024 13:17:19 -0500 Subject: [PATCH 020/290] chore(deps) lib-jitsi-meet@latest https://github.com/jitsi/lib-jitsi-meet/compare/v1859.0.0+9ff77a91...v1862.0.0+95e160b2 --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index bfbfc1e43290..509858a8e914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1859.0.0+9ff77a91/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1862.0.0+95e160b2/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", @@ -12498,8 +12498,8 @@ }, "node_modules/lib-jitsi-meet": { "version": "0.0.0", - "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1859.0.0+9ff77a91/lib-jitsi-meet.tgz", - "integrity": "sha512-G7nNbRaxqoAICXVCkWYDr4hJdC/5rQaDxCi+J+nCImInBV3SKQgqev4r+95qa5OAnWrsjPXwrRldySdkzWhYXg==", + "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1862.0.0+95e160b2/lib-jitsi-meet.tgz", + "integrity": "sha512-er+4R6Xka9O2rER6VsfakRcXSh30PFrK359yKKZsyZi7E7b6of1KLV749KacC0jQrwBA/gZZVsmSsrkwo5G9hg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -28006,8 +28006,8 @@ } }, "lib-jitsi-meet": { - "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1859.0.0+9ff77a91/lib-jitsi-meet.tgz", - "integrity": "sha512-G7nNbRaxqoAICXVCkWYDr4hJdC/5rQaDxCi+J+nCImInBV3SKQgqev4r+95qa5OAnWrsjPXwrRldySdkzWhYXg==", + "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1862.0.0+95e160b2/lib-jitsi-meet.tgz", + "integrity": "sha512-er+4R6Xka9O2rER6VsfakRcXSh30PFrK359yKKZsyZi7E7b6of1KLV749KacC0jQrwBA/gZZVsmSsrkwo5G9hg==", "requires": { "@jitsi/js-utils": "2.2.1", "@jitsi/logger": "2.0.2", diff --git a/package.json b/package.json index 4bcf00c436f3..2b8d07ea7d9e 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1859.0.0+9ff77a91/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1862.0.0+95e160b2/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", From 7718c393191c5d7b92705832845186939e4ffae5 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Tue, 10 Sep 2024 15:33:06 -0500 Subject: [PATCH 021/290] feat(stage-participant-badge): Scale size based on the screen height --- css/filmstrip/_tile_view_overrides.scss | 3 +- react/features/base/ui/Tokens.ts | 7 ++ react/features/base/ui/types.ts | 1 + .../web/StageParticipantNameLabel.tsx | 33 ++++++-- .../display-name/components/web/styles.ts | 84 ++++++++++++++++++- .../subtitles/components/web/Captions.tsx | 2 +- 6 files changed, 120 insertions(+), 10 deletions(-) diff --git a/css/filmstrip/_tile_view_overrides.scss b/css/filmstrip/_tile_view_overrides.scss index 8c53dffa47d6..e24376b52212 100644 --- a/css/filmstrip/_tile_view_overrides.scss +++ b/css/filmstrip/_tile_view_overrides.scss @@ -18,8 +18,7 @@ */ #dominantSpeaker, #largeVideoElementsContainer, - #sharedVideo, - .stage-participant-label { + #sharedVideo { display: none; } diff --git a/react/features/base/ui/Tokens.ts b/react/features/base/ui/Tokens.ts index 159707becb82..87d19ba12f82 100644 --- a/react/features/base/ui/Tokens.ts +++ b/react/features/base/ui/Tokens.ts @@ -189,6 +189,13 @@ export const typography = { letterSpacing: 0.16 }, + bodyShortRegularSmall: { + fontSize: 10, + lineHeight: 16, + fontWeight: font.weightRegular, + letterSpacing: 0 + }, + bodyShortRegular: { fontSize: 14, lineHeight: 20, diff --git a/react/features/base/ui/types.ts b/react/features/base/ui/types.ts index d1bd1c90b4c9..8a3bd8857c61 100644 --- a/react/features/base/ui/types.ts +++ b/react/features/base/ui/types.ts @@ -69,6 +69,7 @@ export interface ITypography { bodyShortBoldLarge: ITypographyType; bodyShortRegular: ITypographyType; bodyShortRegularLarge: ITypographyType; + bodyShortRegularSmall: ITypographyType; heading1: ITypographyType; heading2: ITypographyType; heading3: ITypographyType; diff --git a/react/features/display-name/components/web/StageParticipantNameLabel.tsx b/react/features/display-name/components/web/StageParticipantNameLabel.tsx index 365c78be7d82..c42d36be7471 100644 --- a/react/features/display-name/components/web/StageParticipantNameLabel.tsx +++ b/react/features/display-name/components/web/StageParticipantNameLabel.tsx @@ -16,12 +16,35 @@ import { getTransitionParamsForElementsAboveToolbox, isToolboxVisible } from '.. import { isLayoutTileView } from '../../../video-layout/functions.web'; import DisplayNameBadge from './DisplayNameBadge'; -import { getStageParticipantTypography } from './styles'; +import { + getStageParticipantFontSizeRange, + getStageParticipantNameLabelLineHeight, + getStageParticipantTypography, + scaleFontProperty +} from './styles'; + +interface IOptions { + clientHeight?: number; +} + +const useStyles = makeStyles()((theme, options: IOptions = {}) => { + const typography = { + ...getStageParticipantTypography(theme) + }; + const { clientHeight } = options; + + if (typeof clientHeight === 'number' && clientHeight > 0) { + // We want to show the fontSize and lineHeight configured in theme on a screen with height 1080px. In this case + // the clientHeight will be 960px if there are some titlebars, toolbars, addressbars, etc visible.For any other + // screen size we will decrease/increase the font size based on the screen size. + + typography.fontSize = scaleFontProperty(clientHeight, getStageParticipantFontSizeRange(theme)); + typography.lineHeight = getStageParticipantNameLabelLineHeight(theme, clientHeight); + } -const useStyles = makeStyles()(theme => { return { badgeContainer: { - ...withPixelLineHeight(getStageParticipantTypography(theme)), + ...withPixelLineHeight(typography), alignItems: 'center', display: 'inline-flex', justifyContent: 'center', @@ -47,7 +70,8 @@ const useStyles = makeStyles()(theme => { * @returns {ReactElement|null} */ const StageParticipantNameLabel = () => { - const { classes, cx } = useStyles(); + const clientHeight = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientHeight); + const { classes, cx } = useStyles({ clientHeight }); const largeVideoParticipant = useSelector(getLargeVideoParticipant); const selectedId = largeVideoParticipant?.id; const nameToDisplay = useSelector((state: IReduxState) => getParticipantDisplayName(state, selectedId ?? '')); @@ -68,7 +92,6 @@ const StageParticipantNameLabel = () => { return (
diff --git a/react/features/display-name/components/web/styles.ts b/react/features/display-name/components/web/styles.ts index 7b157790133c..734a4a260346 100644 --- a/react/features/display-name/components/web/styles.ts +++ b/react/features/display-name/components/web/styles.ts @@ -15,12 +15,92 @@ export function getStageParticipantTypography(theme: Theme) { return theme.typography.bodyShortRegularLarge; } +/** + * Returns the range of possible values for the font size for the stage participant display name badge. + * + * @param {Theme} theme - The current theme. + * @returns {ILimits} + */ +export function getStageParticipantFontSizeRange(theme: Theme) { + return { + max: theme.typography.bodyShortRegularLarge.fontSize, + min: theme.typography.bodyShortRegularSmall.fontSize + }; +} + +/** + * Returns the range of possible values for the line height for the stage participant display name badge. + * + * @param {Theme} theme - The current theme. + * @returns {ILimits} + */ +export function getStageParticipantLineHeightRange(theme: Theme) { + return { + max: theme.typography.bodyShortRegularLarge.lineHeight, + min: theme.typography.bodyShortRegularSmall.lineHeight + }; +} + /** * Returns the height + padding for stage participant display name badge. * * @param {Theme} theme - The current theme. + * @param {number} clientHeight - The height of the visible area. * @returns {number} */ -export function getStageParticipantNameLabelHeight(theme: Theme) { - return getStageParticipantTypography(theme).lineHeight ?? 0 + DISPLAY_NAME_VERTICAL_PADDING; +export function getStageParticipantNameLabelHeight(theme: Theme, clientHeight?: number) { + const lineHeight = getStageParticipantNameLabelLineHeight(theme, clientHeight); + + return lineHeight + DISPLAY_NAME_VERTICAL_PADDING; +} + +/** + * Returns the height + padding for stage participant display name badge. + * + * @param {Theme} theme - The current theme. + * @param {number} clientHeight - The height of the visible area. + * @returns {number} + */ +export function getStageParticipantNameLabelLineHeight(theme: Theme, clientHeight?: number) { + return scaleFontProperty(clientHeight, getStageParticipantLineHeightRange(theme)); +} + +interface ILimits { + max: number; + min: number; +} + +/** + * The default clint height limits used by scaleFontProperty. + */ +const DEFAULT_CLIENT_HEIGHT_LIMITS = { + min: 300, + max: 960 +}; + +/** + * Scales a css font property depending on the passed screen size. + * Note: The result will be in the range of the specified propValueLimits. Also if the current screen height is + * more/less than the values in screenLimits parameter the result will be the max/min of the propValuesLimits. + * + * @param {number|undefined} screenHeight - The current screen height. + * @param {ILimits} propValuesLimits - The max and min value for the font property that we are scaling. + * @param {ILimits} screenHeightLimits - The max and min value for screen height. + * @returns {number} - The scaled prop value. + */ +export function scaleFontProperty( + screenHeight: number | undefined, + propValuesLimits: ILimits, + screenHeightLimits: ILimits = DEFAULT_CLIENT_HEIGHT_LIMITS) { + if (typeof screenHeight !== 'number') { + return propValuesLimits.max; + } + + const { max: maxPropSize, min: minPropSize } = propValuesLimits; + const { max: maxHeight, min: minHeight } = screenHeightLimits; + const propSizePerPxHeight = (maxPropSize - minPropSize) / (maxHeight - minHeight); + + return Math.round( + (Math.max(Math.min(screenHeight, maxHeight), minHeight) - minHeight) * propSizePerPxHeight + ) + minPropSize; } diff --git a/react/features/subtitles/components/web/Captions.tsx b/react/features/subtitles/components/web/Captions.tsx index e33b26be03d3..d019c40aa6f7 100644 --- a/react/features/subtitles/components/web/Captions.tsx +++ b/react/features/subtitles/components/web/Captions.tsx @@ -61,7 +61,7 @@ const styles = (theme: Theme, props: IProps) => { if (_isLifted) { // 10px is the space between the onstage participant display name label and subtitles. We also need // to add the padding of the subtitles because it will decrease the gap between the label and subtitles. - marginBottom += getStageParticipantNameLabelHeight(theme) + 10 + padding; + marginBottom += getStageParticipantNameLabelHeight(theme, _clientHeight) + 10 + padding; } if (_shiftUp) { From ab57a2999b76a2e309faf2ebf12915a237106ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Mon, 9 Sep 2024 13:12:52 +0200 Subject: [PATCH 022/290] feat(ios) bump minimum required iOS version to 15.1 RN 0.76 will be doing this change, so let's get ahead. THis puts the iPhione 6S as the baseline model, which was released in September 2015. --- ios/Podfile | 4 ++-- ios/Podfile.lock | 2 +- ios/app/app.xcodeproj/project.pbxproj | 4 ++-- ios/sdk/sdk.xcodeproj/project.pbxproj | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index 692618a66a42..5639c532ca74 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -5,7 +5,7 @@ require Pod::Executable.execute_command('node', ['-p', {paths: [process.argv[1]]}, )', __dir__]).strip -platform :ios, '13.4' +platform :ios, '15.1' workspace 'jitsi-meet' install! 'cocoapods', :deterministic_uuids => false @@ -92,7 +92,7 @@ post_install do |installer| end target.build_configurations.each do |config| config.build_settings['SUPPORTS_MACCATALYST'] = 'NO' - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.4' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.1' config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -no-verify-emitted-module-interface' end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9645f591f627..334c91d103c6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1598,6 +1598,6 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: e5b887426cee15d2a326bdd34afc0282fc0486ad -PODFILE CHECKSUM: 513caf6e662086d8bae95a064c18423f0c07fa01 +PODFILE CHECKSUM: 79119d2af4f01a2584749dc3926902e66572d896 COCOAPODS: 1.15.2 diff --git a/ios/app/app.xcodeproj/project.pbxproj b/ios/app/app.xcodeproj/project.pbxproj index 2bac39a771e8..bbade24b8e3d 100644 --- a/ios/app/app.xcodeproj/project.pbxproj +++ b/ios/app/app.xcodeproj/project.pbxproj @@ -1034,7 +1034,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, ); - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; @@ -1096,7 +1096,7 @@ "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, ); - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = "$(inherited)"; diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index 222940884998..52a0d12b1e64 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -776,7 +776,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "$(inherited)"; @@ -841,7 +841,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = "$(inherited)"; From 5ea2093a407e37ebebb1cda527f637356f5c8a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Mon, 2 Sep 2024 13:58:26 +0200 Subject: [PATCH 023/290] fix(ios) specify supported platforms (iOS, iPadOS) --- ios/app/app.xcodeproj/project.pbxproj | 18 ++++++++++++ ios/sdk/sdk.xcodeproj/project.pbxproj | 42 +++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/ios/app/app.xcodeproj/project.pbxproj b/ios/app/app.xcodeproj/project.pbxproj index bbade24b8e3d..305ea8dc8670 100644 --- a/ios/app/app.xcodeproj/project.pbxproj +++ b/ios/app/app.xcodeproj/project.pbxproj @@ -880,6 +880,11 @@ PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet; PRODUCT_NAME = "jitsi-meet"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -909,6 +914,11 @@ PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet; PRODUCT_NAME = "jitsi-meet"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -940,6 +950,10 @@ PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.broadcast.extension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -975,6 +989,10 @@ PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet.broadcast.extension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index 52a0d12b1e64..8cbbe68a7e2a 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -873,16 +873,24 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; INFOPLIST_FILE = src/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -902,15 +910,23 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; INFOPLIST_FILE = src/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -935,16 +951,24 @@ "JITSI_MEET_SDK_LITE=1", ); INFOPLIST_FILE = "src/Lite-Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios; PRODUCT_NAME = JitsiMeetSDK; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -969,15 +993,23 @@ JITSI_MEET_SDK_LITE, ); INFOPLIST_FILE = "src/Lite-Info.plist"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios; PRODUCT_NAME = JitsiMeetSDK; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_INSTALL_OBJC_HEADER = NO; SWIFT_OBJC_INTERFACE_HEADER_NAME = ""; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; From 4c9234ffecba39034d29cd0d69c19fa1d4869b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Fri, 13 Sep 2024 11:56:42 +0200 Subject: [PATCH 024/290] chore(deps) lib-jitsi-meet@latest https://github.com/jitsi/lib-jitsi-meet/compare/v1862.0.0+95e160b2...v1864.0.0+cf14a33f --- package-lock.json | 12 +++++------- package.json | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 509858a8e914..c80797fbe083 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1862.0.0+95e160b2/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1864.0.0+cf14a33f/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", @@ -12498,8 +12498,8 @@ }, "node_modules/lib-jitsi-meet": { "version": "0.0.0", - "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1862.0.0+95e160b2/lib-jitsi-meet.tgz", - "integrity": "sha512-er+4R6Xka9O2rER6VsfakRcXSh30PFrK359yKKZsyZi7E7b6of1KLV749KacC0jQrwBA/gZZVsmSsrkwo5G9hg==", + "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1864.0.0+cf14a33f/lib-jitsi-meet.tgz", + "integrity": "sha512-90yFgeC2+tiX8lXG13pU/zod2w7UW51Qck6Yz/qjj9xvzhT0yX8zJkqZCGatsOWLf1adlQxBlxfMlzSvfE8q1g==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -12514,7 +12514,6 @@ "jquery": "3.6.1", "lodash-es": "4.17.21", "patch-package": "6.5.1", - "promise.allsettled": "1.0.4", "sdp-transform": "2.3.0", "strophe.js": "1.5.0", "strophejs-plugin-disco": "0.0.2", @@ -28006,8 +28005,8 @@ } }, "lib-jitsi-meet": { - "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1862.0.0+95e160b2/lib-jitsi-meet.tgz", - "integrity": "sha512-er+4R6Xka9O2rER6VsfakRcXSh30PFrK359yKKZsyZi7E7b6of1KLV749KacC0jQrwBA/gZZVsmSsrkwo5G9hg==", + "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1864.0.0+cf14a33f/lib-jitsi-meet.tgz", + "integrity": "sha512-90yFgeC2+tiX8lXG13pU/zod2w7UW51Qck6Yz/qjj9xvzhT0yX8zJkqZCGatsOWLf1adlQxBlxfMlzSvfE8q1g==", "requires": { "@jitsi/js-utils": "2.2.1", "@jitsi/logger": "2.0.2", @@ -28020,7 +28019,6 @@ "jquery": "3.6.1", "lodash-es": "4.17.21", "patch-package": "6.5.1", - "promise.allsettled": "1.0.4", "sdp-transform": "2.3.0", "strophe.js": "1.5.0", "strophejs-plugin-disco": "0.0.2", diff --git a/package.json b/package.json index 2b8d07ea7d9e..e392520b4396 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1862.0.0+95e160b2/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1864.0.0+cf14a33f/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", From 756c4afbdd95eca99e04fe36e7b8f7026e3e6414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Fri, 13 Sep 2024 15:09:58 +0200 Subject: [PATCH 025/290] fix(rn,overlay) skip showing reload dialog while leaving the conference (#15045) * fix(rn,overlay) skip showing reload dialog while leaving the conference --- .../components/native/PageReloadDialog.tsx | 22 ++++++------ react/features/overlay/actions.native.ts | 13 +++++-- react/features/overlay/actions.web.ts | 6 +++- .../web/AbstractPageReloadOverlay.tsx | 35 ++++++++----------- react/features/overlay/middleware.ts | 20 +++++++++-- react/features/overlay/reducer.ts | 5 --- 6 files changed, 58 insertions(+), 43 deletions(-) diff --git a/react/features/base/dialog/components/native/PageReloadDialog.tsx b/react/features/base/dialog/components/native/PageReloadDialog.tsx index 57baab463723..2a85c707fb47 100644 --- a/react/features/base/dialog/components/native/PageReloadDialog.tsx +++ b/react/features/base/dialog/components/native/PageReloadDialog.tsx @@ -18,7 +18,10 @@ import ConfirmDialog from './ConfirmDialog'; * The type of the React {@code Component} props of * {@link PageReloadDialog}. */ -interface IPageReloadDialogProps extends WithTranslation { +interface IProps extends WithTranslation { + conferenceError?: Error; + configError?: Error; + connectionError?: Error; dispatch: IStore['dispatch']; isNetworkFailure: boolean; reason?: string; @@ -37,7 +40,7 @@ interface IPageReloadDialogState { * conference is reloaded. * Shows a warning message and counts down towards the re-load. */ -class PageReloadDialog extends Component { +class PageReloadDialog extends Component { _interval?: number; _timeoutSeconds: number; @@ -48,7 +51,7 @@ class PageReloadDialog extends Component export function abstractMapStateToProps(state: IReduxState) { const { error: configError } = state['features/base/config']; const { error: connectionError } = state['features/base/connection']; - const { fatalError } = state['features/overlay']; - - let reason = fatalError && (fatalError.message || fatalError.name); - - if (!reason) { - const { error: conferenceError } = state['features/base/conference']; - - if (conferenceError) { - reason = `error.conference.${conferenceError.name}`; - } else if (configError) { - reason = `error.config.${configError.name}`; - } else if (connectionError) { - reason = `error.connection.${connectionError.name}`; - } else { - logger.error('No reload reason defined!'); - } + const { error: conferenceError } = state['features/base/conference']; + const error = configError || connectionError || conferenceError; + let reason; + + if (conferenceError) { + reason = `error.conference.${conferenceError.name}`; + } else if (configError) { + reason = `error.config.${configError.name}`; + } else if (connectionError) { + reason = `error.connection.${connectionError.name}`; + } else { + logger.error('No reload reason defined!'); } return { - details: fatalError?.details, - error: fatalError, - isNetworkFailure: - fatalError === configError || fatalError === connectionError, + details: undefined, // TODO: revisit this. + error, + isNetworkFailure: Boolean(configError || connectionError), reason }; } diff --git a/react/features/overlay/middleware.ts b/react/features/overlay/middleware.ts index 58df9eed4901..0a7ae3082607 100644 --- a/react/features/overlay/middleware.ts +++ b/react/features/overlay/middleware.ts @@ -7,6 +7,7 @@ import { import StateListenerRegistry from '../base/redux/StateListenerRegistry'; import { openPageReloadDialog } from './actions'; +import logger from './logger'; /** * Error type. Basically like Error, but augmented with a recoverable property. @@ -94,12 +95,12 @@ StateListenerRegistry.register( return configError || connectionError || conferenceError; }, /* listener */ (error: ErrorType, store: IStore) => { - const state = store.getState(); - if (!error) { return; } + const state = store.getState(); + // eslint-disable-next-line no-negated-condition if (typeof APP !== 'undefined') { APP.API.notifyError({ @@ -107,8 +108,21 @@ StateListenerRegistry.register( ...getErrorExtraInfo(state, error) }); } else if (RN_NO_RELOAD_DIALOG_ERRORS.indexOf(error.name) === -1 && typeof error.recoverable === 'undefined') { + const { error: conferenceError } = state['features/base/conference']; + const { error: configError } = state['features/base/config']; + const { error: connectionError } = state['features/base/connection']; + const conferenceState = state['features/base/conference']; + + if (conferenceState.leaving) { + logger.info(`Ignoring ${error.name} while leaving conference`); + + return; + } + setTimeout(() => { - store.dispatch(openPageReloadDialog()); + logger.info(`Reloading due to error: ${error.name}`, error); + + store.dispatch(openPageReloadDialog(conferenceError, configError, connectionError)); }, 500); } } diff --git a/react/features/overlay/reducer.ts b/react/features/overlay/reducer.ts index 2987d3b69ddd..b997cb583e4d 100644 --- a/react/features/overlay/reducer.ts +++ b/react/features/overlay/reducer.ts @@ -6,11 +6,6 @@ import { MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED } from './actionTypes'; export interface IOverlayState { browser?: string; - fatalError?: { - details: Object; - message?: string; - name?: string; - }; isMediaPermissionPromptVisible?: boolean; } From 262cb0422c93ea35a221bda97b3afe85af6f7170 Mon Sep 17 00:00:00 2001 From: damencho Date: Wed, 11 Sep 2024 18:51:24 -0500 Subject: [PATCH 026/290] fix(breakout-rooms): Fixes reporting virtual jid of main room. When reporting the real jid, nothing matches in jicofo internals and we miss to match the room. --- resources/prosody-plugins/mod_muc_breakout_rooms.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/prosody-plugins/mod_muc_breakout_rooms.lua b/resources/prosody-plugins/mod_muc_breakout_rooms.lua index c3d2850e4fc0..fb763f718041 100644 --- a/resources/prosody-plugins/mod_muc_breakout_rooms.lua +++ b/resources/prosody-plugins/mod_muc_breakout_rooms.lua @@ -526,7 +526,7 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module) name = 'muc#roominfo_breakout_main_room'; label = 'The main room associated with this breakout room'; }); - event.formdata['muc#roominfo_breakout_main_room'] = main_room_jid; + event.formdata['muc#roominfo_breakout_main_room'] = internal_room_jid_match_rewrite(main_room_jid); -- If the main room has a lobby, make it so this breakout room also uses it. if (main_room and main_room._data.lobbyroom and main_room:get_members_only()) then @@ -553,7 +553,7 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module) table.insert(event.form, { name = 'muc#roominfo_breakout_main_room'; label = 'The main room associated with this breakout room'; - value = main_room_jid; + value = internal_room_jid_match_rewrite(main_room_jid); }); end); From b3742a343817e2cd2b2049a26973f2d5415a6999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Fri, 13 Sep 2024 11:06:29 -0500 Subject: [PATCH 027/290] fix(transcriptions,recording): Allows non moderators with features to dial, record or transcribe. (#15074) * fix(transcriptions): Uses dial command to invite transcriber. * fix(transcriptions,recording): Allows non moderators with features to dial, record or transcribe. * sqaush: Make sure filtering works when only is a moderator. It works now and without a token and no features, but being moderator. * squash: Rename constant. * squash: Checks features first before defaulting to moderator when filtering metadata service. * squash: Checks features first before defaulting to moderator in UI. * squash: Fixes lint and one other check. * squash: Moves more logic to is_feature_allowed. * squash: Drops unnecessary check. * squash: Uses constant coming from ljm. * squash: Toggles back captions button on error. * squash: Fix comment. * squash: Reverting back isLiveStreamingButtonVisible. * squash: Fix imports. --- react/features/base/conference/reducer.ts | 1 + react/features/base/jwt/constants.ts | 2 +- react/features/invite/functions.ts | 7 ++- .../LiveStream/AbstractLiveStreamButton.ts | 3 +- .../AbstractStartRecordingDialogContent.tsx | 17 +++--- .../web/StartRecordingDialogContent.tsx | 6 +- react/features/recording/functions.ts | 27 +++----- react/features/recording/hooks.web.ts | 7 +-- react/features/subtitles/middleware.ts | 27 +++++++- react/features/transcribing/functions.ts | 13 +--- .../prosody-plugins/mod_filter_iq_jibri.lua | 29 +++++---- .../prosody-plugins/mod_filter_iq_rayo.lua | 61 ++++++++++++++----- .../mod_muc_transcription_filter.lua | 31 ---------- .../mod_room_metadata_component.lua | 26 ++++++-- .../mod_system_chat_message.lua | 1 - resources/prosody-plugins/util.lib.lua | 14 +++-- 16 files changed, 154 insertions(+), 118 deletions(-) delete mode 100644 resources/prosody-plugins/mod_muc_transcription_filter.lua diff --git a/react/features/base/conference/reducer.ts b/react/features/base/conference/reducer.ts index e0ece7bd2372..3af9c06f470c 100644 --- a/react/features/base/conference/reducer.ts +++ b/react/features/base/conference/reducer.ts @@ -93,6 +93,7 @@ export interface IJitsiConference { getRole: Function; getSpeakerStats: () => ISpeakerStats; getSsrcByTrack: Function; + getTranscriptionStatus: Function; grantOwner: Function; isAVModerationSupported: Function; isE2EEEnabled: Function; diff --git a/react/features/base/jwt/constants.ts b/react/features/base/jwt/constants.ts index 12b4123997d5..7c1d6487ec43 100644 --- a/react/features/base/jwt/constants.ts +++ b/react/features/base/jwt/constants.ts @@ -20,10 +20,10 @@ export const MEET_FEATURES = { /** * A mapping between jwt features and toolbar buttons keys. + * We don't need recording in here, as it will disable the local recording too. */ export const FEATURES_TO_BUTTONS_MAPPING = { 'livestreaming': 'livestreaming', - 'recording': 'recording', 'transcription': 'closedcaptions' }; diff --git a/react/features/invite/functions.ts b/react/features/invite/functions.ts index 872b6bdba876..c1f5c32dd57c 100644 --- a/react/features/invite/functions.ts +++ b/react/features/invite/functions.ts @@ -491,8 +491,9 @@ export function isAddPeopleEnabled(state: IReduxState): boolean { */ export function isDialOutEnabled(state: IReduxState): boolean { const { conference } = state['features/base/conference']; + const isModerator = isLocalParticipantModerator(state); - return isLocalParticipantModerator(state) + return isJwtFeatureEnabled(state, 'outbound-call', isModerator, isModerator) && conference && conference.isSIPCallingSupported(); } @@ -504,9 +505,9 @@ export function isDialOutEnabled(state: IReduxState): boolean { */ export function isSipInviteEnabled(state: IReduxState): boolean { const { sipInviteUrl } = state['features/base/config']; + const isModerator = isLocalParticipantModerator(state); - return isLocalParticipantModerator(state) - && isJwtFeatureEnabled(state, 'sip-outbound-call') + return isJwtFeatureEnabled(state, 'sip-outbound-call', isModerator, isModerator) && Boolean(sipInviteUrl); } diff --git a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts index ffb162d70d65..2a49c74030cb 100644 --- a/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts +++ b/react/features/recording/components/LiveStream/AbstractLiveStreamButton.ts @@ -133,9 +133,8 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) { const liveStreaming = getLiveStreaming(state); visible = isLiveStreamingButtonVisible({ - localParticipantIsModerator: isModerator, + liveStreamingAllowed: isJwtFeatureEnabled(state, 'livestreaming', isModerator, isModerator), liveStreamingEnabled: liveStreaming?.enabled, - liveStreamingEnabledInJwt: isJwtFeatureEnabled(state, 'livestreaming', true), isInBreakoutRoom: isInBreakoutRoom(state) }); } diff --git a/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx b/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx index 6251e6d02fd5..e17fec524150 100644 --- a/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx +++ b/react/features/recording/components/Recording/AbstractStartRecordingDialogContent.tsx @@ -6,6 +6,7 @@ import { sendAnalytics } from '../../../analytics/functions'; import { IReduxState, IStore } from '../../../app/types'; import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry'; import { _abstractMapStateToProps } from '../../../base/dialog/functions'; +import { isJwtFeatureEnabled } from '../../../base/jwt/functions'; import { isLocalParticipantModerator } from '../../../base/participants/functions'; import { authorizeDropbox, updateDropboxToken } from '../../../dropbox/actions'; import { isVpaasMeeting } from '../../../jaas/functions'; @@ -34,11 +35,6 @@ export interface IProps extends WithTranslation { */ _hideStorageWarning: boolean; - /** - * Whether local participant is moderator. - */ - _isModerator: boolean; - /** * Whether local recording is available or not. */ @@ -59,6 +55,11 @@ export interface IProps extends WithTranslation { */ _localRecordingSelfEnabled: boolean; + /** + * Whether to render recording. + */ + _renderRecording: boolean; + /** * The color-schemed stylesheet of this component. */ @@ -412,15 +413,15 @@ class AbstractStartRecordingDialogContent extends Component { */ export function mapStateToProps(state: IReduxState) { const { localRecording, recordingService } = state['features/base/config']; - const _localRecordingAvailable - = !localRecording?.disable && supportsLocalRecording(); + const _localRecordingAvailable = !localRecording?.disable && supportsLocalRecording(); + const isModerator = isLocalParticipantModerator(state); return { ..._abstractMapStateToProps(state), isVpaas: isVpaasMeeting(state), _canStartTranscribing: canAddTranscriber(state), _hideStorageWarning: Boolean(recordingService?.hideStorageWarning), - _isModerator: isLocalParticipantModerator(state), + _renderRecording: isJwtFeatureEnabled(state, 'recording', isModerator, isModerator), _localRecordingAvailable, _localRecordingEnabled: !localRecording?.disable, _localRecordingSelfEnabled: !localRecording?.disableSelfRecording, diff --git a/react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx b/react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx index 5534803f7c26..f3e2c15953fa 100644 --- a/react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx +++ b/react/features/recording/components/Recording/web/StartRecordingDialogContent.tsx @@ -37,9 +37,11 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent { * @returns {React$Component} */ render() { + const _renderRecording = this.props._renderRecording; + return ( - { this.props._isModerator && ( + { _renderRecording && ( <> { this._renderNoIntegrationsContent() } { this._renderFileSharingContent() } @@ -48,7 +50,7 @@ class StartRecordingDialogContent extends AbstractStartRecordingDialogContent { )} { this._renderLocalRecordingContent() } - { this._renderAdvancedOptions() } + { _renderRecording && <> { this._renderAdvancedOptions() } } ); } diff --git a/react/features/recording/functions.ts b/react/features/recording/functions.ts index b7adffa3ea79..ab2e8ccfc622 100644 --- a/react/features/recording/functions.ts +++ b/react/features/recording/functions.ts @@ -267,8 +267,8 @@ export function getRecordButtonProps(state: IReduxState) { if (localRecordingEnabled) { visible = true; - } else if (isModerator) { - visible = recordingEnabled ? isJwtFeatureEnabled(state, 'recording', true) : false; + } else if (isJwtFeatureEnabled(state, 'recording', isModerator, isModerator)) { + visible = recordingEnabled; } // disable the button if the livestreaming is running. @@ -416,29 +416,22 @@ export function registerRecordingAudioFiles(dispatch: IStore['dispatch'], should } /** - * Returns true if the live streaming button should be visible. + * Returns true if the live-streaming button should be visible. * - * @param {boolean} localParticipantIsModerator - True if the local participant is moderator. - * @param {boolean} liveStreamingEnabled - True if the live streaming is enabled. - * @param {boolean} liveStreamingEnabledInJwt - True if the lives treaming feature is enabled in JWT. + * @param {boolean} liveStreamingEnabled - True if the live-streaming is enabled. + * @param {boolean} liveStreamingAllowed - True if the live-streaming feature is enabled in JWT + * or is a moderator if JWT is missing or features are missing in JWT. + * @param {boolean} isInBreakoutRoom - True if in breakout room. * @returns {boolean} */ export function isLiveStreamingButtonVisible({ - localParticipantIsModerator, + liveStreamingAllowed, liveStreamingEnabled, - liveStreamingEnabledInJwt, isInBreakoutRoom }: { isInBreakoutRoom: boolean; + liveStreamingAllowed: boolean; liveStreamingEnabled: boolean; - liveStreamingEnabledInJwt: boolean; - localParticipantIsModerator: boolean; }) { - let visible = false; - - if (localParticipantIsModerator && !isInBreakoutRoom) { - visible = liveStreamingEnabled ? liveStreamingEnabledInJwt : false; - } - - return visible; + return !isInBreakoutRoom && liveStreamingEnabled && liveStreamingAllowed; } diff --git a/react/features/recording/hooks.web.ts b/react/features/recording/hooks.web.ts index 3ae4baf67ed7..2a831611400a 100644 --- a/react/features/recording/hooks.web.ts +++ b/react/features/recording/hooks.web.ts @@ -47,15 +47,14 @@ export function useLiveStreamingButton() { const toolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons); const localParticipantIsModerator = useSelector(isLocalParticipantModerator); const liveStreaming = useSelector(getLiveStreaming); - const liveStreamingEnabledInJwt - = useSelector((state: IReduxState) => isJwtFeatureEnabled(state, 'livestreaming', true)); + const liveStreamingAllowed = useSelector((state: IReduxState) => + isJwtFeatureEnabled(state, 'livestreaming', localParticipantIsModerator, localParticipantIsModerator)); const _isInBreakoutRoom = useSelector(isInBreakoutRoom); if (toolbarButtons?.includes('recording') && isLiveStreamingButtonVisible({ - localParticipantIsModerator, + liveStreamingAllowed, liveStreamingEnabled: liveStreaming?.enabled, - liveStreamingEnabledInJwt, isInBreakoutRoom: _isInBreakoutRoom })) { return livestreaming; diff --git a/react/features/subtitles/middleware.ts b/react/features/subtitles/middleware.ts index 85c8c8b8137c..16d3795bfc7a 100644 --- a/react/features/subtitles/middleware.ts +++ b/react/features/subtitles/middleware.ts @@ -2,6 +2,9 @@ import { AnyAction } from 'redux'; import { IStore } from '../app/types'; import { ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes'; +import { isJwtFeatureEnabled } from '../base/jwt/functions'; +import JitsiMeetJS from '../base/lib-jitsi-meet'; +import { isLocalParticipantModerator } from '../base/participants/functions'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import { @@ -10,9 +13,11 @@ import { } from './actionTypes'; import { removeTranscriptMessage, + setRequestingSubtitles, updateTranscriptMessage } from './actions.any'; import { notifyTranscriptionChunkReceived } from './functions'; +import logger from './logger'; import { ITranscriptMessage } from './types'; @@ -40,6 +45,11 @@ const P_NAME_REQUESTING_TRANSCRIPTION = 'requestingTranscription'; */ const P_NAME_TRANSLATION_LANGUAGE = 'translation_language'; +/** + * The dial command to use for starting a transcriber. + */ +const TRANSCRIBER_DIAL_NUMBER = 'jitsi_meet_transcribe'; + /** * Time after which the rendered subtitles will be removed. */ @@ -229,7 +239,7 @@ function _endpointMessageReceived(store: IStore, next: Function, action: AnyActi * @returns {void} */ function _requestingSubtitlesChange( - { getState }: IStore, + { dispatch, getState }: IStore, enabled: boolean, language?: string | null) { const state = getState(); @@ -239,6 +249,21 @@ function _requestingSubtitlesChange( P_NAME_REQUESTING_TRANSCRIPTION, enabled); + if (enabled && conference?.getTranscriptionStatus() === JitsiMeetJS.constants.transcriptionStatus.OFF) { + const isModerator = isLocalParticipantModerator(state); + const featureAllowed = isJwtFeatureEnabled(getState(), 'transcription', isModerator, isModerator); + + if (featureAllowed) { + conference?.dial(TRANSCRIBER_DIAL_NUMBER) + .catch((e: any) => { + logger.error('Error dialing', e); + + // let's back to the correct state + dispatch(setRequestingSubtitles(false, false)); + }); + } + } + if (enabled && language) { conference?.setLocalParticipantProperty( P_NAME_TRANSLATION_LANGUAGE, diff --git a/react/features/transcribing/functions.ts b/react/features/transcribing/functions.ts index 430dfa9a6479..7641fc44a48a 100644 --- a/react/features/transcribing/functions.ts +++ b/react/features/transcribing/functions.ts @@ -77,15 +77,8 @@ export function isRecorderTranscriptionsRunning(state: IReduxState) { */ export function canAddTranscriber(state: IReduxState) { const { transcription } = state['features/base/config']; - const isJwtTranscribingEnabled = isJwtFeatureEnabled(state, 'transcription', isLocalParticipantModerator(state)); + const isModerator = isLocalParticipantModerator(state); + const isTranscribingAllowed = isJwtFeatureEnabled(state, 'transcription', isModerator, isModerator); - if (!transcription?.enabled) { - return false; - } - - if (isJwtTranscribingEnabled) { - return true; - } - - return false; + return Boolean(transcription?.enabled) && isTranscribingAllowed; } diff --git a/resources/prosody-plugins/mod_filter_iq_jibri.lua b/resources/prosody-plugins/mod_filter_iq_jibri.lua index b56cf54a76a7..be3d82339de5 100644 --- a/resources/prosody-plugins/mod_filter_iq_jibri.lua +++ b/resources/prosody-plugins/mod_filter_iq_jibri.lua @@ -1,5 +1,10 @@ +-- This module is enabled under the main virtual host local st = require "util.stanza"; -local is_feature_allowed = module:require "util".is_feature_allowed; +local jid_bare = require "util.jid".bare; +local util = module:require 'util'; +local is_feature_allowed = util.is_feature_allowed; +local get_room_from_jid = util.get_room_from_jid; +local room_jid_match_rewrite = util.room_jid_match_rewrite; -- filters jibri iq in case of requested from jwt authenticated session that -- has features in the user context, but without feature for recording @@ -10,17 +15,19 @@ module:hook("pre-iq/full", function(event) if jibri then local session = event.origin; local token = session.auth_token; + local room = get_room_from_jid(room_jid_match_rewrite(jid_bare(stanza.attr.to))); + local occupant = room:get_occupant_by_real_jid(stanza.attr.from); + local feature = jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming'; + local is_allowed = is_feature_allowed( + feature, + session.jitsi_meet_context_features, + session.granted_jitsi_meet_context_features, + occupant.role == 'moderator'); - if jibri.attr.action == 'start' then - if token == nil - or not is_feature_allowed(session.jitsi_meet_context_features, - (jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming') - ) then - module:log("info", - "Filtering jibri start recording, stanza:%s", tostring(stanza)); - session.send(st.error_reply(stanza, "auth", "forbidden")); - return true; - end + if jibri.attr.action == 'start' and not is_allowed then + module:log('info', 'Filtering jibri start recording, stanza:%s', tostring(stanza)); + session.send(st.error_reply(stanza, 'auth', 'forbidden')); + return true; end end end diff --git a/resources/prosody-plugins/mod_filter_iq_rayo.lua b/resources/prosody-plugins/mod_filter_iq_rayo.lua index 86bd0cc841fd..535f024f4a5e 100644 --- a/resources/prosody-plugins/mod_filter_iq_rayo.lua +++ b/resources/prosody-plugins/mod_filter_iq_rayo.lua @@ -1,3 +1,4 @@ +-- This module is enabled under the main virtual host local new_throttle = require "util.throttle".create; local st = require "util.stanza"; @@ -69,20 +70,19 @@ module:hook("pre-iq/full", function(event) end end - local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call'; - local is_session_allowed = is_feature_allowed(session.jitsi_meet_context_features, feature); - - -- if current user is not allowed, but was granted moderation by a user - -- that is allowed by its features we want to allow it - local is_granting_session_allowed = false; - if (session.granted_jitsi_meet_context_features) then - is_granting_session_allowed = is_feature_allowed(session.granted_jitsi_meet_context_features, feature); - end + local room_real_jid = room_jid_match_rewrite(roomName); + local room = main_muc_service.get_room_from_jid(room_real_jid); - if (token == nil - or roomName == nil - or not token_util:verify_room(session, room_jid_match_rewrite(roomName)) - or not (is_session_allowed or is_granting_session_allowed)) + local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call'; + local is_session_allowed = is_feature_allowed( + feature, + session.jitsi_meet_context_features, + session.granted_jitsi_meet_context_features, + room:get_affiliation(stanza.attr.from) == 'owner'); + + if roomName == nil + or (token ~= nil and not token_util:verify_room(session, room_real_jid)) + or not is_session_allowed then module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza)); session.send(st.error_reply(stanza, "auth", "forbidden")); @@ -99,8 +99,8 @@ module:hook("pre-iq/full", function(event) group_id = session.granted_jitsi_meet_context_group_id; end - -- now lets check any limits if configured - if limit_outgoing_calls > 0 then + -- now lets check any limits for outgoing calls if configured + if feature == 'outbound-call' and limit_outgoing_calls > 0 then if not session.dial_out_throttle then -- module:log("debug", "Enabling dial-out throttle session=%s.", session); session.dial_out_throttle = new_throttle(limit_outgoing_calls, OUTGOING_CALLS_THROTTLE_INTERVAL); @@ -259,3 +259,34 @@ process_host_module(main_muc_component_host, function(host_module, host) end); end end); + +-- when recording participants may enable and backend transcriptions +-- it is possible that participant is not moderator, but has the features enabled for +-- transcribing, we need to allow that operation +module:hook('jitsi-metadata-allow-moderation', function (event) + local data, key, occupant, session = event.data, event.key, event.actor, event.session; + + if key == 'recording' and data and data.isTranscribingEnabled ~= nil then + -- if it is recording we want to allow setting in metadata if not moderator but features + -- are present + if session.jitsi_meet_context_features + and occupant.role ~= 'moderator' + and is_feature_allowed('transcription', session.jitsi_meet_context_features) + and is_feature_allowed('recording', session.jitsi_meet_context_features) then + local res = {}; + res.isTranscribingEnabled = data.isTranscribingEnabled; + return res; + elseif not session.jitsi_meet_context_features and occupant.role == 'moderator' then + return data; + else + return nil; + end + end + + if occupant.role == 'moderator' then + return data; + end + + return nil; +end); + diff --git a/resources/prosody-plugins/mod_muc_transcription_filter.lua b/resources/prosody-plugins/mod_muc_transcription_filter.lua deleted file mode 100644 index b66e44a1d93b..000000000000 --- a/resources/prosody-plugins/mod_muc_transcription_filter.lua +++ /dev/null @@ -1,31 +0,0 @@ ---This module performs features checking when a transcription is requested. ---If the transcription feature is not allowed, the tag indicating that a ---transcription is being requested will be stripped from the presence stanza. ---The module must be enabled under the muc component. -local is_feature_allowed = module:require "util".is_feature_allowed; - -module:log("info", "Loading mod_muc_transcription_filter!"); -local filtered_tag_name = "jitsi_participant_requestingTranscription"; - -function filter_transcription_tag(event) - local stanza = event.stanza; - local session = event.origin; - if stanza and stanza.name == "presence" then - if not is_feature_allowed(session.jitsi_meet_context_features,'transcription') then - stanza:maptags(function(tag) - if tag and tag.name == filtered_tag_name then - module:log("info", "Removing %s tag from presence stanza!", filtered_tag_name); - return nil; - else - return tag; - end - end) - end - end -end - -module:hook("presence/bare", filter_transcription_tag); -module:hook("presence/full", filter_transcription_tag); -module:hook("presence/host", filter_transcription_tag); - -module:log("info", "Loaded mod_muc_transcription_filter!"); diff --git a/resources/prosody-plugins/mod_room_metadata_component.lua b/resources/prosody-plugins/mod_room_metadata_component.lua index 9459172b3180..2e29222f7b40 100644 --- a/resources/prosody-plugins/mod_room_metadata_component.lua +++ b/resources/prosody-plugins/mod_room_metadata_component.lua @@ -28,7 +28,13 @@ local FORM_KEY = 'muc#roominfo_jitsimetadata'; local muc_component_host = module:get_option_string('muc_component'); if muc_component_host == nil then - module:log("error", "No muc_component specified. No muc to operate on!"); + module:log('error', 'No muc_component specified. No muc to operate on!'); + return; +end + +local muc_domain_base = module:get_option_string('muc_mapper_domain_base'); +if not muc_domain_base then + module:log('warn', 'No muc_domain_base option set.'); return; end @@ -129,11 +135,6 @@ function on_message(event) return false; end - if occupant.role ~= 'moderator' then - module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid); - return false; - end - local jsonData, error = json.decode(messageText); if jsonData == nil then -- invalid JSON module:log("error", "Invalid JSON message: %s error:%s", messageText, error); @@ -145,6 +146,19 @@ function on_message(event) return false; end + if occupant.role ~= 'moderator' then + -- will return a non nil filtered data to use, if it is nil, it is not allowed + local res = module:context(muc_domain_base):fire_event('jitsi-metadata-allow-moderation', + { room = room; actor = occupant; key = jsonData.key ; data = jsonData.data; session = session; }); + + if not res then + module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid); + return false; + end + + jsonData.data = res; + end + room.jitsiMetadata[jsonData.key] = jsonData.data; broadcastMetadata(room); diff --git a/resources/prosody-plugins/mod_system_chat_message.lua b/resources/prosody-plugins/mod_system_chat_message.lua index 79c71e1f201b..ddb37238bc06 100644 --- a/resources/prosody-plugins/mod_system_chat_message.lua +++ b/resources/prosody-plugins/mod_system_chat_message.lua @@ -15,7 +15,6 @@ local get_room_from_jid = util.get_room_from_jid; local st = require "util.stanza"; local json = require "cjson.safe"; -local muc_domain_base = module:get_option_string("muc_mapper_domain_base"); local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", ""); if asapKeyServer then diff --git a/resources/prosody-plugins/util.lib.lua b/resources/prosody-plugins/util.lib.lua index d51f5ae71c3a..d7f07c1a17cf 100644 --- a/resources/prosody-plugins/util.lib.lua +++ b/resources/prosody-plugins/util.lib.lua @@ -250,13 +250,15 @@ end -- Utility function to check whether feature is present and enabled. Allow -- a feature if there are features present in the session(coming from -- the token) and the value of the feature is true. --- If features is not present in the token we skip feature detection and allow --- everything. -function is_feature_allowed(features, ft) - if (features == nil or features[ft] == "true" or features[ft] == true) then - return true; +-- If features are missing but we have granted_features check that +-- if features are missing from the token we check whether it is moderator +function is_feature_allowed(ft, features, granted_features, is_moderator) + if features then + return features[ft] == "true" or features[ft] == true; + elseif granted_features then + return granted_features[ft] == "true" or granted_features[ft] == true; else - return false; + return is_moderator; end end From ede26956e8cd244299987bd398d442f43e639004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Fri, 13 Sep 2024 18:35:34 -0500 Subject: [PATCH 028/290] feat(visitors): Transcriptions for visitors. (#15119) * feat(visitors): Transcriptions for visitors. * squash: Fixes filter iq. * feat: Rewrites room name requests in rayo iq for visitors. * squash: Handles visitors count that request transcriptions and the languages requested. * fix(subtitles): Make sure we show captions button when no features but is transcribing. --- react/features/base/jwt/constants.ts | 9 --- .../toolbox/components/web/Toolbox.tsx | 3 +- react/features/toolbox/constants.ts | 1 + react/features/toolbox/functions.any.ts | 39 ++++++--- .../prosody-plugins/mod_filter_iq_rayo.lua | 68 ++++++++++------ resources/prosody-plugins/mod_fmuc.lua | 80 ++++++++++++++++++- resources/prosody-plugins/mod_visitors.lua | 9 ++- .../mod_visitors_component.lua | 79 +++++++++++++++++- resources/prosody-plugins/util.lib.lua | 31 +++++++ 9 files changed, 264 insertions(+), 55 deletions(-) diff --git a/react/features/base/jwt/constants.ts b/react/features/base/jwt/constants.ts index 7c1d6487ec43..dd1abc8dff6e 100644 --- a/react/features/base/jwt/constants.ts +++ b/react/features/base/jwt/constants.ts @@ -18,15 +18,6 @@ export const MEET_FEATURES = { TRANSCRIPTION: 'transcription' }; -/** - * A mapping between jwt features and toolbar buttons keys. - * We don't need recording in here, as it will disable the local recording too. - */ -export const FEATURES_TO_BUTTONS_MAPPING = { - 'livestreaming': 'livestreaming', - 'transcription': 'closedcaptions' -}; - /** * The JWT validation errors for JaaS. */ diff --git a/react/features/toolbox/components/web/Toolbox.tsx b/react/features/toolbox/components/web/Toolbox.tsx index d5adec8bdb7c..6ec5a73db9b0 100644 --- a/react/features/toolbox/components/web/Toolbox.tsx +++ b/react/features/toolbox/components/web/Toolbox.tsx @@ -99,7 +99,8 @@ export default function Toolbox({ const isDialogVisible = useSelector((state: IReduxState) => Boolean(state['features/base/dialog'].component)); const jwt = useSelector((state: IReduxState) => state['features/base/jwt'].jwt); const localParticipant = useSelector(getLocalParticipant); - const jwtDisabledButtons = getJwtDisabledButtons(jwt, localParticipant?.features); + const jwtDisabledButtons = useSelector((state: IReduxState) => + getJwtDisabledButtons(state, jwt, localParticipant?.features)); const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled); const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons); const toolbarVisible = useSelector(isToolboxVisible); diff --git a/react/features/toolbox/constants.ts b/react/features/toolbox/constants.ts index b70e37f5cec3..8cafff08a57f 100644 --- a/react/features/toolbox/constants.ts +++ b/react/features/toolbox/constants.ts @@ -133,6 +133,7 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [ */ export const VISITORS_MODE_BUTTONS: ToolbarButton[] = [ 'chat', + 'closedcaptions', 'hangup', 'raisehand', 'settings', diff --git a/react/features/toolbox/functions.any.ts b/react/features/toolbox/functions.any.ts index 781958e39fec..e8a180cff343 100644 --- a/react/features/toolbox/functions.any.ts +++ b/react/features/toolbox/functions.any.ts @@ -1,8 +1,8 @@ import { IReduxState } from '../app/types'; -import { FEATURES_TO_BUTTONS_MAPPING } from '../base/jwt/constants'; import { isJwtFeatureEnabledStateless } from '../base/jwt/functions'; import { IGUMPendingState } from '../base/media/types'; import { IParticipantFeatures } from '../base/participants/types'; +import { isTranscribing } from '../transcribing/functions'; /** * Indicates if the audio mute button is disabled or not. @@ -20,21 +20,34 @@ export function isAudioMuteButtonDisabled(state: IReduxState) { /** * Returns the buttons corresponding to features disabled through jwt. * + * @param {IReduxState} state - The state from the Redux store. * @param {string | undefined} jwt - The jwt token. * @param {ILocalParticipant} localParticipantFeatures - The features of the local participant. * @returns {string[]} - The disabled by jwt buttons array. */ -export function getJwtDisabledButtons(jwt: string | undefined, localParticipantFeatures?: IParticipantFeatures) { - return Object.keys(FEATURES_TO_BUTTONS_MAPPING).reduce((acc: string[], current: string) => { - if (!isJwtFeatureEnabledStateless({ - jwt, - localParticipantFeatures, - feature: current, - ifNoToken: true - })) { - acc.push(FEATURES_TO_BUTTONS_MAPPING[current as keyof typeof FEATURES_TO_BUTTONS_MAPPING]); - } +export function getJwtDisabledButtons( + state: IReduxState, + jwt: string | undefined, + localParticipantFeatures?: IParticipantFeatures) { + const acc = []; + + if (!isJwtFeatureEnabledStateless({ + jwt, + localParticipantFeatures, + feature: 'livestreaming', + ifNoToken: true + })) { + acc.push('livestreaming'); + } + + if (!isTranscribing(state) && !isJwtFeatureEnabledStateless({ + jwt, + localParticipantFeatures, + feature: 'transcription', + ifNoToken: true + })) { + acc.push('closedcaptions'); + } - return acc; - }, []); + return acc; } diff --git a/resources/prosody-plugins/mod_filter_iq_rayo.lua b/resources/prosody-plugins/mod_filter_iq_rayo.lua index 535f024f4a5e..add3de112642 100644 --- a/resources/prosody-plugins/mod_filter_iq_rayo.lua +++ b/resources/prosody-plugins/mod_filter_iq_rayo.lua @@ -1,6 +1,7 @@ -- This module is enabled under the main virtual host local new_throttle = require "util.throttle".create; local st = require "util.stanza"; +local jid = require "util.jid"; local token_util = module:require "token/util".new(module); local util = module:require 'util'; @@ -29,6 +30,14 @@ if token_util == nil then return; end +-- this is the main virtual host of the main prosody that this vnode serves +local main_domain = module:get_option_string('main_domain'); +-- only the visitor prosody has main_domain setting +local is_visitor_prosody = main_domain ~= nil; + +-- this is the main virtual host of this vnode +local local_domain = module:get_option_string('muc_mapper_domain_base'); + local um_is_admin = require 'core.usermanager'.is_admin; local function is_admin(jid) return um_is_admin(jid, module.host); @@ -45,6 +54,8 @@ load_config(); -- Header names to use to push extra data extracted from token, if any local OUT_INITIATOR_USER_ATTR_NAME = "X-outbound-call-initiator-user"; local OUT_INITIATOR_GROUP_ATTR_NAME = "X-outbound-call-initiator-group"; +local OUT_ROOM_NAME_ATTR_NAME = "JvbRoomName"; + local OUTGOING_CALLS_THROTTLE_INTERVAL = 60; -- if max_number_outgoing_calls is enabled it will be -- the max number of outgoing calls a user can try for a minute @@ -60,18 +71,33 @@ module:hook("pre-iq/full", function(event) local token = session.auth_token; -- find header with attr name 'JvbRoomName' and extract its value - local headerName = 'JvbRoomName'; local roomName; - for _, child in ipairs(dial.tags) do - if (child.name == 'header' - and child.attr.name == headerName) then - roomName = child.attr.value; - break; + -- Remove any 'header' element if it already exists, so it cannot be spoofed by a client + dial:maptags(function(tag) + if tag.name == "header" + and (tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME + or tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME) then + return nil + elseif tag.name == "header" and tag.attr.name == OUT_ROOM_NAME_ATTR_NAME then + roomName = tag.attr.value; + -- we will remove it as we will add it later, modified + if is_visitor_prosody then + return nil; + end end - end + return tag + end); - local room_real_jid = room_jid_match_rewrite(roomName); + local room_jid = jid.bare(stanza.attr.to); + local room_real_jid = room_jid_match_rewrite(room_jid); local room = main_muc_service.get_room_from_jid(room_real_jid); + local is_sender_in_room = room:get_occupant_jid(stanza.attr.from) ~= nil; + + if not room or not is_sender_in_room then + module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza)); + session.send(st.error_reply(stanza, "auth", "forbidden")); + return true; + end local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call'; local is_session_allowed = is_feature_allowed( @@ -81,6 +107,7 @@ module:hook("pre-iq/full", function(event) room:get_affiliation(stanza.attr.from) == 'owner'); if roomName == nil + or roomName ~= room_jid or (token ~= nil and not token_util:verify_room(session, room_real_jid)) or not is_session_allowed then @@ -119,20 +146,6 @@ module:hook("pre-iq/full", function(event) -- now lets insert token information if any if session and user_id then - -- First remove any 'header' element if it already - -- exists, so it cannot be spoofed by a client - stanza:maptags( - function(tag) - if tag.name == "header" - and (tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME - or tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME) then - return nil - end - return tag - end - ) - - local dial = stanza:get_child('dial', 'urn:xmpp:rayo:1'); -- adds initiator user id from token dial:tag("header", { xmlns = "urn:xmpp:rayo:1", @@ -149,9 +162,18 @@ module:hook("pre-iq/full", function(event) dial:up(); end end + + -- we want to instruct jigasi to enter the main room, so send the correct main room jid + if is_visitor_prosody then + dial:tag("header", { + xmlns = "urn:xmpp:rayo:1", + name = OUT_ROOM_NAME_ATTR_NAME, + value = string.gsub(roomName, local_domain, main_domain) }); + dial:up(); + end end end -end); +end, 1); -- make sure we run before domain mapper --- Finds and returns the number of concurrent outgoing calls for a user -- @param context_user the user id extracted from the token diff --git a/resources/prosody-plugins/mod_fmuc.lua b/resources/prosody-plugins/mod_fmuc.lua index e1588ccd73f5..e0325d63253f 100644 --- a/resources/prosody-plugins/mod_fmuc.lua +++ b/resources/prosody-plugins/mod_fmuc.lua @@ -14,6 +14,7 @@ local jid = require 'util.jid'; local st = require 'util.stanza'; local new_id = require 'util.id'.medium; local filters = require 'util.filters'; +local array = require"util.array"; local util = module:require 'util'; local ends_with = util.ends_with; @@ -24,6 +25,11 @@ local get_focus_occupant = util.get_focus_occupant; local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite; local presence_check_status = util.presence_check_status; +local PARTICIPANT_PROP_RAISE_HAND = 'jitsi_participant_raisedHand'; +local PARTICIPANT_PROP_REQUEST_TRANSCRIPTION = 'jitsi_participant_requestingTranscription'; +local PARTICIPANT_PROP_TRANSLATION_LANG = 'jitsi_participant_translation_language'; +local TRANSCRIPT_DEFAULT_LANG = module:get_option_string('transcriptions_default_language', 'en'); + -- this is the main virtual host of this vnode local local_domain = module:get_option_string('muc_mapper_domain_base'); if not local_domain then @@ -60,6 +66,62 @@ local function is_admin(jid) return um_is_admin(jid, module.host); end +local function send_transcriptions_update(room) + -- let's notify main prosody + local lang_array = array{}; + local count = 0; + + for k, v in pairs(room._transcription_languages) do + if not lang_array[v] then + lang_array:push(v); + end + count = count + 1; + end + + local iq_id = new_id(); + sent_iq_cache:set(iq_id, socket.gettime()); + module:send(st.iq({ + type = 'set', + to = 'visitors.'..main_domain, + from = local_domain, + id = iq_id }) + :tag('visitors', { xmlns = 'jitsi:visitors', + room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) }) + :tag('transcription-languages', { + xmlns = 'jitsi:visitors', + langs = lang_array:sort():concat(','), + count = tostring(count) + }):up()); +end + +local function remove_transcription(room, occupant) + local send_update = false; + if room._transcription_languages then + if room._transcription_languages[occupant.jid] then + send_update = true; + end + room._transcription_languages[occupant.jid] = nil; + end + + if send_update then + send_transcriptions_update(room); + end +end + +-- if lang is nil we will remove it from the list +local function add_transcription(room, occupant, lang) + if not room._transcription_languages then + room._transcription_languages = {}; + end + + local old = room._transcription_languages[occupant.jid]; + room._transcription_languages[occupant.jid] = lang or TRANSCRIPT_DEFAULT_LANG; + + if old ~= room._transcription_languages[occupant.jid] then + send_transcriptions_update(room); + end +end + -- mark all occupants as visitors module:hook('muc-occupant-pre-join', function (event) local occupant, room, origin, stanza = event.occupant, event.room, event.origin, event.stanza; @@ -92,7 +154,7 @@ module:hook('muc-occupant-pre-leave', function (event) -- to main prosody local pr = occupant:get_presence(); - local raiseHand = pr:get_child_text('jitsi_participant_raisedHand'); + local raiseHand = pr:get_child_text(PARTICIPANT_PROP_RAISE_HAND); -- a promotion detected let's send it to main prosody if raiseHand and #raiseHand > 0 then @@ -114,6 +176,7 @@ module:hook('muc-occupant-pre-leave', function (event) module:send(promotion_request); end + remove_transcription(room, occupant); end, 1); -- rate limit is 0 -- Returns the main participants count and the visitors count @@ -201,6 +264,7 @@ end); -- forward visitor presences to jicofo -- detects raise hand in visitors presence, this is request for promotion +-- detects the requested transcription and its language to send updates for it module:hook('muc-broadcast-presence', function (event) local occupant = event.occupant; @@ -230,7 +294,7 @@ module:hook('muc-broadcast-presence', function (event) full_p.attr.to = focus_occupant.jid; room:route_to_occupant(focus_occupant, full_p); - local raiseHand = full_p:get_child_text('jitsi_participant_raisedHand'); + local raiseHand = full_p:get_child_text(PARTICIPANT_PROP_RAISE_HAND); -- a promotion detected let's send it to main prosody if raiseHand then local user_id; @@ -286,6 +350,18 @@ module:hook('muc-broadcast-presence', function (event) module:send(promotion_request); end + -- detect transcription + if full_p:get_child_text(PARTICIPANT_PROP_REQUEST_TRANSCRIPTION) then + local lang = full_p:get_child_text(PARTICIPANT_PROP_TRANSLATION_LANG); + + occupant._transcription_enabled = true; + + add_transcription(room, occupant, lang); + elseif occupant._transcription_enabled then + occupant._transcription_enabled = false; + remove_transcription(room, occupant, nil); + end + return; end); diff --git a/resources/prosody-plugins/mod_visitors.lua b/resources/prosody-plugins/mod_visitors.lua index 80701004e908..d97b6cab5ff5 100644 --- a/resources/prosody-plugins/mod_visitors.lua +++ b/resources/prosody-plugins/mod_visitors.lua @@ -14,6 +14,7 @@ local new_id = require 'util.id'.medium; local util = module:require 'util'; local presence_check_status = util.presence_check_status; local process_host_module = util.process_host_module; +local is_transcriber_jigasi = util.is_transcriber_jigasi; local um_is_admin = require 'core.usermanager'.is_admin; local function is_admin(jid) @@ -185,7 +186,7 @@ process_host_module(main_muc_component_config, function(host_module, host) -- filter focus and configured domains (used for jibri and transcribers) if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil - or ignore_list:contains(jid.host(occupant.bare_jid)) then + or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then return; end @@ -206,7 +207,7 @@ process_host_module(main_muc_component_config, function(host_module, host) -- ignore configured domains (jibri and transcribers) if is_admin(occupant.bare_jid) or visitors_nodes[room.jid] == nil or visitors_nodes[room.jid].nodes == nil - or ignore_list:contains(jid.host(occupant.bare_jid)) then + or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then return; end @@ -249,7 +250,7 @@ process_host_module(main_muc_component_config, function(host_module, host) -- filter focus, ignore configured domains (jibri and transcribers) if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil - or ignore_list:contains(jid.host(occupant.bare_jid)) then + or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then return; end @@ -268,7 +269,7 @@ process_host_module(main_muc_component_config, function(host_module, host) local room, stanza, occupant = event.room, event.stanza, event.occupant; -- filter sending messages from transcribers/jibris to visitors - if not visitors_nodes[room.jid] or ignore_list:contains(jid.host(occupant.bare_jid)) then + if not visitors_nodes[room.jid] then return; end diff --git a/resources/prosody-plugins/mod_visitors_component.lua b/resources/prosody-plugins/mod_visitors_component.lua index 4d52ccc0ac3c..23ae1c897d65 100644 --- a/resources/prosody-plugins/mod_visitors_component.lua +++ b/resources/prosody-plugins/mod_visitors_component.lua @@ -1,5 +1,6 @@ module:log('info', 'Starting visitors_component at %s', module.host); +local array = require "util.array"; local http = require 'net.http'; local jid = require 'util.jid'; local st = require 'util.stanza'; @@ -14,6 +15,7 @@ local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite; local is_vpaas = util.is_vpaas; local is_sip_jibri_join = util.is_sip_jibri_join; local process_host_module = util.process_host_module; +local split_string = util.split_string; local new_id = require 'util.id'.medium; local um_is_admin = require 'core.usermanager'.is_admin; local json = require 'cjson.safe'; @@ -179,7 +181,7 @@ local function connect_vnode_received(room, vnode) room._connected_vnodes = cache.new(16); -- we up to 16 vnodes for this prosody end - room._connected_vnodes:set(vnode..'.meet.jitsi', 'connected'); + room._connected_vnodes:set(vnode..'.meet.jitsi', {}); end local function disconnect_vnode_received(room, vnode) @@ -194,6 +196,34 @@ local function disconnect_vnode_received(room, vnode) end end +-- returns the accumulated data for visitors nodes, count all visitors requesting transcriptions +-- and accumulated languages requested +-- @returns count, languages +function get_visitors_languages(room) + if not room._connected_vnodes then + return; + end + + local count = 0; + local languages = array(); + + -- iterate over visitor nodes we are connected to and accumulate data if we have it + for k, v in room._connected_vnodes:items() do + if v.count then + count = count + v.count; + end + if v.langs then + for k in pairs(v.langs) do + local val = v.langs[k] + if not languages[val] then + languages:push(val); + end + end + end + end + return count, languages:sort():concat(','); +end + -- listens for iq request for promotion and forward it to moderators in the meeting for approval -- or auto-allow it if such the config is set enabling it local function stanza_handler(event) @@ -232,12 +262,17 @@ local function stanza_handler(event) return; end + local from_vnode; + if room._connected_vnodes then + from_vnode = room._connected_vnodes:get(stanza.attr.from); + end + local processed; -- promotion request is coming from visitors and is a set and is over the s2s connection local request_promotion = visitors_iq:get_child('promotion-request'); if request_promotion then - if not (room._connected_vnodes and room._connected_vnodes:get(stanza.attr.from)) then - module:log('warn', 'Received forged promotion-request: %s %s %s', stanza, inspect(room._connected_vnodes), room._connected_vnodes:get(stanza.attr.from)); + if not from_vnode then + module:log('warn', 'Received forged request_promotion message: %s %s',stanza, inspect(room._connected_vnodes)); return true; -- stop processing end @@ -266,6 +301,44 @@ local function stanza_handler(event) end end + -- request to update metadata service for jigasi languages + local transcription_languages = visitors_iq:get_child('transcription-languages'); + + if transcription_languages + and (transcription_languages.attr.langs or transcription_languages.attr.count) then + if not from_vnode then + module:log('warn', 'Received forged transcription_languages message: %s %s',stanza, inspect(room._connected_vnodes)); + return true; -- stop processing + end + if not room.jitsiMetadata then + room.jitsiMetadata = {}; + end + if not room.jitsiMetadata.visitors then + room.jitsiMetadata.visitors = {}; + end + + -- we keep the split by languages array to optimize accumulating languages + from_vnode.langs = split_string(transcription_languages.attr.langs, ','); + from_vnode.count = transcription_languages.attr.count; + + local count, languages = get_visitors_languages(room); + + if room.jitsiMetadata.visitors.transcribingLanguages ~= languages then + room.jitsiMetadata.visitors.transcribingLanguages = languages; + processed = true; + end + + if room.jitsiMetadata.visitors.transcribingCount ~= count then + room.jitsiMetadata.visitors.transcribingCount = count; + processed = true; + end + + if processed then + module:context(muc_domain_prefix..'.'..muc_domain_base) + :fire_event('room-metadata-changed', { room = room; }); + end + end + if not processed then module:log('warn', 'Unknown iq received for %s: %s', module.host, stanza); end diff --git a/resources/prosody-plugins/util.lib.lua b/resources/prosody-plugins/util.lib.lua index d7f07c1a17cf..ce0ed1c068c6 100644 --- a/resources/prosody-plugins/util.lib.lua +++ b/resources/prosody-plugins/util.lib.lua @@ -2,6 +2,7 @@ local jid = require "util.jid"; local timer = require "util.timer"; local http = require "net.http"; local cache = require "util.cache"; +local array = require "util.array"; local http_timeout = 30; local have_async, async = pcall(require, "util.async"); @@ -473,6 +474,23 @@ function is_sip_jigasi(stanza) return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi'); end +function is_transcriber_jigasi(stanza) + local features = stanza:get_child('features'); + if not features then + return false; + end + + for i = 1, #features do + local feature = features[i]; + if feature.attr and feature.attr.var and feature.attr.var == 'http://jitsi.org/protocol/transcriber' then + return true; + end + end + + return false; +end + + function get_sip_jibri_email_prefix(email) if not email then return nil; @@ -537,6 +555,17 @@ function table_shallow_copy(t) return t2 end +-- Splits a string using delimiter +function split_string(str, delimiter) + str = str .. delimiter; + local result = array(); + for w in str:gmatch("(.-)" .. delimiter) do + result:push(w); + end + + return result; +end + return { OUTBOUND_SIP_JIBRI_PREFIXES = OUTBOUND_SIP_JIBRI_PREFIXES; INBOUND_SIP_JIBRI_PREFIXES = INBOUND_SIP_JIBRI_PREFIXES; @@ -546,6 +575,7 @@ return { is_moderated = is_moderated; is_sip_jibri_join = is_sip_jibri_join; is_sip_jigasi = is_sip_jigasi; + is_transcriber_jigasi = is_transcriber_jigasi; is_vpaas = is_vpaas; get_focus_occupant = get_focus_occupant; get_room_from_jid = get_room_from_jid; @@ -560,6 +590,7 @@ return { update_presence_identity = update_presence_identity; http_get_with_retry = http_get_with_retry; ends_with = ends_with; + split_string = split_string; starts_with = starts_with; starts_with_one_of = starts_with_one_of; table_shallow_copy = table_shallow_copy; From 936fa55ce996ad3818519bdd1a17d2db73a6fcc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Sun, 15 Sep 2024 18:39:33 -0500 Subject: [PATCH 029/290] fix(deb): Restart jicofo on new install. Testing clean install on Ubuntu 24.04 seems to end up with jicofo not connected due to the certificate not being validated. --- debian/jitsi-meet-prosody.postinst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/jitsi-meet-prosody.postinst b/debian/jitsi-meet-prosody.postinst index 4f825f77c323..3849fc1fadcb 100644 --- a/debian/jitsi-meet-prosody.postinst +++ b/debian/jitsi-meet-prosody.postinst @@ -252,9 +252,10 @@ case "$1" in if [ "$PROSODY_CONFIG_PRESENT" = "false" ]; then invoke-rc.d prosody restart || true - # In case we had updated the certificates and restarted prosody, let's restart and the bridge if possible + # In case we had updated the certificates and restarted prosody, let's restart and the bridge and jicofo if possible if [ -d /run/systemd/system ] && [ "$CERT_ADDED_TO_TRUST" = "true" ]; then systemctl restart jitsi-videobridge2.service >/dev/null || true + systemctl restart jicofo.service >/dev/null || true fi fi ;; From 085e6dd3b98b6cd349fa4cb375684e5a3d0587ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Mon, 16 Sep 2024 10:01:09 -0500 Subject: [PATCH 030/290] feat(follow-me): Adds option to limit it for recorder only. (#15120) * feat(follow-me): Adds option to limit it for recorder only. * squash: Fix comments. * squash: Fix comments. --- lang/main.json | 1 + react/features/base/conference/actionTypes.ts | 17 ++++++++-- react/features/base/conference/actions.any.ts | 17 ++++++++++ react/features/base/conference/reducer.ts | 8 +++++ react/features/follow-me/actions.ts | 9 +++-- react/features/follow-me/functions.ts | 13 ++++++++ react/features/follow-me/middleware.ts | 7 +++- react/features/follow-me/reducer.ts | 6 +++- react/features/follow-me/subscriber.ts | 1 + react/features/settings/actions.web.ts | 5 +++ .../components/native/ModeratorSection.tsx | 23 ++++++++++++- .../settings/components/web/ModeratorTab.tsx | 33 ++++++++++++++++++- .../components/web/SettingsDialog.tsx | 1 + react/features/settings/functions.any.ts | 6 +++- 14 files changed, 136 insertions(+), 11 deletions(-) diff --git a/lang/main.json b/lang/main.json index b3914fc852b5..27ac1ced890f 100644 --- a/lang/main.json +++ b/lang/main.json @@ -1096,6 +1096,7 @@ "desktopShareWarning": "You need to restart the screen share for the new settings to take effect.", "devices": "Devices", "followMe": "Everyone follows me", + "followMeRecorder": "Recorder follows me", "framesPerSecond": "frames-per-second", "incomingMessage": "Incoming message", "language": "Language", diff --git a/react/features/base/conference/actionTypes.ts b/react/features/base/conference/actionTypes.ts index 0557717c47b8..8ff83f09a413 100644 --- a/react/features/base/conference/actionTypes.ts +++ b/react/features/base/conference/actionTypes.ts @@ -56,7 +56,7 @@ export const CONFERENCE_LEFT = 'CONFERENCE_LEFT'; /** * The type of (redux) action which signals that the conference is out of focus. * For example, if the user navigates to the Chat screen. - * + * * { * type: CONFERENCE_BLURRED, * } @@ -65,7 +65,7 @@ export const CONFERENCE_BLURRED = 'CONFERENCE_BLURRED'; /** * The type of (redux) action which signals that the conference is in focus. - * + * * { * type: CONFERENCE_FOCUSED, * } @@ -258,6 +258,17 @@ export const SEND_TONES = 'SEND_TONES'; */ export const SET_FOLLOW_ME = 'SET_FOLLOW_ME'; +/** + * The type of (redux) action which updates the current known status of the + * Follow Me feature that is used only by the recorder. + * + * { + * type: SET_FOLLOW_ME_RECORDER, + * enabled: boolean + * } + */ +export const SET_FOLLOW_ME_RECORDER = 'SET_FOLLOW_ME_RECORDER'; + /** * The type of (redux) action which sets the obfuscated room name. * @@ -338,7 +349,7 @@ export const SET_START_MUTED_POLICY = 'SET_START_MUTED_POLICY'; /** * The type of (redux) action which updates the assumed bandwidth bps. - * + * * { * type: SET_ASSUMED_BANDWIDTH_BPS, * assumedBandwidthBps: number diff --git a/react/features/base/conference/actions.any.ts b/react/features/base/conference/actions.any.ts index 86da22e2f4a8..38fbed313100 100644 --- a/react/features/base/conference/actions.any.ts +++ b/react/features/base/conference/actions.any.ts @@ -61,6 +61,7 @@ import { SEND_TONES, SET_ASSUMED_BANDWIDTH_BPS, SET_FOLLOW_ME, + SET_FOLLOW_ME_RECORDER, SET_OBFUSCATED_ROOM, SET_PASSWORD, SET_PASSWORD_FAILED, @@ -847,6 +848,22 @@ export function setFollowMe(enabled: boolean) { }; } +/** + * Enables or disables the Follow Me feature used only for the recorder. + * + * @param {boolean} enabled - Whether Follow Me should be enabled and used only by the recorder. + * @returns {{ + * type: SET_FOLLOW_ME_RECORDER, + * enabled: boolean + * }} + */ +export function setFollowMeRecorder(enabled: boolean) { + return { + type: SET_FOLLOW_ME_RECORDER, + enabled + }; +} + /** * Enables or disables the Mute reaction sounds feature. * diff --git a/react/features/base/conference/reducer.ts b/react/features/base/conference/reducer.ts index 3af9c06f470c..39af43c3b80f 100644 --- a/react/features/base/conference/reducer.ts +++ b/react/features/base/conference/reducer.ts @@ -26,6 +26,7 @@ import { P2P_STATUS_CHANGED, SET_ASSUMED_BANDWIDTH_BPS, SET_FOLLOW_ME, + SET_FOLLOW_ME_RECORDER, SET_OBFUSCATED_ROOM, SET_PASSWORD, SET_PENDING_SUBJECT_CHANGE, @@ -160,6 +161,7 @@ export interface IConferenceState { e2eeSupported?: boolean; error?: Error; followMeEnabled?: boolean; + followMeRecorderEnabled?: boolean; joining?: IJitsiConference; leaving?: IJitsiConference; lobbyWaitingForHost?: boolean; @@ -252,6 +254,12 @@ ReducerRegistry.register('features/base/conference', case SET_FOLLOW_ME: return set(state, 'followMeEnabled', action.enabled); + case SET_FOLLOW_ME_RECORDER: + return { ...state, + followMeRecorderEnabled: action.enabled, + followMeEnabled: action.enabled + }; + case SET_START_REACTIONS_MUTED: return set(state, 'startReactionsMuted', action.muted); diff --git a/react/features/follow-me/actions.ts b/react/features/follow-me/actions.ts index c6acbef46c62..2e6e0267c427 100644 --- a/react/features/follow-me/actions.ts +++ b/react/features/follow-me/actions.ts @@ -7,15 +7,18 @@ import { * Sets the current moderator id or clears it. * * @param {?string} id - The Follow Me moderator participant id. + * @param {?boolean} forRecorder - Whether this is command only for recorder. * @returns {{ * type: SET_FOLLOW_ME_MODERATOR, - * id, string + * id: string, + * forRecorder: boolean * }} */ -export function setFollowMeModerator(id?: string) { +export function setFollowMeModerator(id?: string, forRecorder?: boolean) { return { type: SET_FOLLOW_ME_MODERATOR, - id + id, + forRecorder }; } diff --git a/react/features/follow-me/functions.ts b/react/features/follow-me/functions.ts index 25c22f871d37..5030cde18ef8 100644 --- a/react/features/follow-me/functions.ts +++ b/react/features/follow-me/functions.ts @@ -13,3 +13,16 @@ export function isFollowMeActive(stateful: IStateful) { return Boolean(state['features/follow-me'].moderator); } + +/** + * Returns true if follow me is active only for the recorder and false otherwise. + * + * @param {Object|Function} stateful - Object or function that can be resolved + * to the Redux state. + * @returns {boolean} - True if follow me is active and false otherwise. + */ +export function isFollowMeRecorderActive(stateful: IStateful) { + const state = toState(stateful); + + return Boolean(state['features/follow-me'].recorder); +} diff --git a/react/features/follow-me/middleware.ts b/react/features/follow-me/middleware.ts index 76adfee53453..78f2da028cbf 100644 --- a/react/features/follow-me/middleware.ts +++ b/react/features/follow-me/middleware.ts @@ -128,7 +128,7 @@ function _onFollowMeCommand(attributes: any = {}, id: string, store: IStore) { } if (!isFollowMeActive(state)) { - store.dispatch(setFollowMeModerator(id)); + store.dispatch(setFollowMeModerator(id, attributes.recorder)); } // just a command that follow me was turned off @@ -138,6 +138,11 @@ function _onFollowMeCommand(attributes: any = {}, id: string, store: IStore) { return; } + // when recorder flag is on, follow me is handled only on recorder side + if (attributes.recorder && !store.getState()['features/base/config'].iAmRecorder) { + return; + } + const oldState = state['features/follow-me'].state || {}; store.dispatch(setFollowMeState(attributes)); diff --git a/react/features/follow-me/reducer.ts b/react/features/follow-me/reducer.ts index 981b9cfde633..4697879578e6 100644 --- a/react/features/follow-me/reducer.ts +++ b/react/features/follow-me/reducer.ts @@ -8,6 +8,7 @@ import { export interface IFollowMeState { moderator?: string; + recorder?: boolean; state?: { [key: string]: string; }; @@ -24,9 +25,12 @@ ReducerRegistry.register( case SET_FOLLOW_ME_MODERATOR: { let newState = set(state, 'moderator', action.id); - if (!action.id) { + if (action.id) { + newState = set(newState, 'recorder', action.forRecorder); + } else { // clear the state if feature becomes disabled newState = set(newState, 'state', undefined); + newState = set(newState, 'recorder', undefined); } return newState; diff --git a/react/features/follow-me/subscriber.ts b/react/features/follow-me/subscriber.ts index f9606b14e898..78a2592b8d5b 100644 --- a/react/features/follow-me/subscriber.ts +++ b/react/features/follow-me/subscriber.ts @@ -88,6 +88,7 @@ function _getFollowMeState(state: IReduxState) { const stageFilmstrip = isStageFilmstripEnabled(state); return { + recorder: state['features/base/conference'].followMeRecorderEnabled, filmstripVisible: state['features/filmstrip'].visible, maxStageParticipants: stageFilmstrip ? state['features/base/settings'].maxStageParticipants : undefined, nextOnStage: pinnedParticipant?.id, diff --git a/react/features/settings/actions.web.ts b/react/features/settings/actions.web.ts index c14fff75316c..ed4b2d296677 100644 --- a/react/features/settings/actions.web.ts +++ b/react/features/settings/actions.web.ts @@ -5,6 +5,7 @@ import { setTokenAuthUrlSuccess } from '../authentication/actions.web'; import { isTokenAuthEnabled } from '../authentication/functions'; import { setFollowMe, + setFollowMeRecorder, setStartMutedPolicy, setStartReactionsMuted } from '../base/conference/actions'; @@ -165,6 +166,10 @@ export function submitModeratorTab(newState: any) { dispatch(setFollowMe(newState.followMeEnabled)); } + if (newState.followMeRecorderEnabled !== currentState.followMeRecorderEnabled) { + dispatch(setFollowMeRecorder(newState.followMeRecorderEnabled)); + } + if (newState.startReactionsMuted !== currentState.startReactionsMuted) { batch(() => { // updating settings we want to update and backend (notify the rest of the participants) diff --git a/react/features/settings/components/native/ModeratorSection.tsx b/react/features/settings/components/native/ModeratorSection.tsx index 394acf38313f..b4fb0632ed1c 100644 --- a/react/features/settings/components/native/ModeratorSection.tsx +++ b/react/features/settings/components/native/ModeratorSection.tsx @@ -2,7 +2,12 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { IReduxState } from '../../../app/types'; -import { setFollowMe, setStartMutedPolicy, setStartReactionsMuted } from '../../../base/conference/actions'; +import { + setFollowMe, + setFollowMeRecorder, + setStartMutedPolicy, + setStartReactionsMuted +} from '../../../base/conference/actions'; import { updateSettings } from '../../../base/settings/actions'; import Switch from '../../../base/ui/components/native/Switch'; import { getModeratorTabProps } from '../../functions.native'; @@ -14,6 +19,7 @@ const ModeratorSection = () => { const dispatch = useDispatch(); const { followMeEnabled, + followMeRecorderEnabled, startAudioMuted, startVideoMuted, startReactionsMuted @@ -35,6 +41,10 @@ const ModeratorSection = () => { dispatch(setFollowMe(Boolean(enabled))); }, [ dispatch, setFollowMe ]); + const onFollowMeRecorderToggled = useCallback((enabled?: boolean) => { + dispatch(setFollowMeRecorder(Boolean(enabled))); + }, [ dispatch, setFollowMeRecorder ]); + const onStartReactionsMutedToggled = useCallback((enabled?: boolean) => { dispatch(setStartReactionsMuted(Boolean(enabled), true)); dispatch(updateSettings({ soundsReactions: enabled })); @@ -57,6 +67,11 @@ const ModeratorSection = () => { state: followMeEnabled, onChange: onFollowMeToggled }, + { + label: 'settings.followMeRecorder', + state: followMeRecorderEnabled, + onChange: onFollowMeRecorderToggled + }, { label: 'settings.startReactionsMuted', state: startReactionsMuted, @@ -72,7 +87,13 @@ const ModeratorSection = () => { }, [ startAudioMuted, startVideoMuted, followMeEnabled, + followMeRecorderEnabled, disableReactionsModeration, + onStartAudioMutedToggled, + onStartVideoMutedToggled, + onFollowMeToggled, + onFollowMeRecorderToggled, + onStartReactionsMutedToggled, startReactionsMuted ]); return ( diff --git a/react/features/settings/components/web/ModeratorTab.tsx b/react/features/settings/components/web/ModeratorTab.tsx index d690e6dec9b5..5796d969e843 100644 --- a/react/features/settings/components/web/ModeratorTab.tsx +++ b/react/features/settings/components/web/ModeratorTab.tsx @@ -34,6 +34,16 @@ export interface IProps extends AbstractDialogTabProps, WithTranslation { */ followMeEnabled: boolean; + /** + * Whether follow me for recorder is currently active (enabled by some other participant). + */ + followMeRecorderActive: boolean; + + /** + * Whether the user has selected the Follow Me Recorder feature to be enabled. + */ + followMeRecorderEnabled: boolean; + /** * Whether or not the user has selected the Start Audio Muted feature to be * enabled. @@ -92,6 +102,7 @@ class ModeratorTab extends AbstractDialogTab { this._onStartVideoMutedChanged = this._onStartVideoMutedChanged.bind(this); this._onStartReactionsMutedChanged = this._onStartReactionsMutedChanged.bind(this); this._onFollowMeEnabledChanged = this._onFollowMeEnabledChanged.bind(this); + this._onFollowMeRecorderEnabledChanged = this._onFollowMeRecorderEnabledChanged.bind(this); } /** @@ -142,6 +153,17 @@ class ModeratorTab extends AbstractDialogTab { super._onChange({ followMeEnabled: checked }); } + /** + * Callback invoked to select if follow-me for recorder mode should be activated. + * + * @param {Object} e - The key event to handle. + * + * @returns {void} + */ + _onFollowMeRecorderEnabledChanged({ target: { checked } }: React.ChangeEvent) { + super._onChange({ followMeRecorderEnabled: checked }); + } + /** * Implements React's {@link Component#render()}. * @@ -153,6 +175,8 @@ class ModeratorTab extends AbstractDialogTab { disableReactionsModeration, followMeActive, followMeEnabled, + followMeRecorderActive, + followMeRecorderEnabled, startAudioMuted, startVideoMuted, startReactionsMuted, @@ -182,10 +206,17 @@ class ModeratorTab extends AbstractDialogTab { + { !disableReactionsModeration && Date: Sun, 15 Sep 2024 23:04:55 -0500 Subject: [PATCH 031/290] fix: Fixes installing let's encrypt on clean system. When testing on 24.04 fails to create let's encrypt successfully because the webserver is not installed completely. --- debian/control | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/control b/debian/control index c4c0f885e73d..6d23e2c0669b 100644 --- a/debian/control +++ b/debian/control @@ -20,7 +20,8 @@ Description: WebRTC JavaScript video conferences Package: jitsi-meet-web-config Architecture: all -Depends: openssl, nginx | nginx-full | nginx-extras | openresty | apache2, curl +Pre-Depends: nginx | nginx-full | nginx-extras | openresty | apache2 +Depends: openssl, curl Description: Configuration for web serving of Jitsi Meet Jitsi Meet is a WebRTC JavaScript application that uses Jitsi Videobridge to provide high quality, scalable video conferences. From 9f73eb76a3b6886e4ece6cc2a16945858d69fd4f Mon Sep 17 00:00:00 2001 From: damencho Date: Mon, 16 Sep 2024 12:57:01 -0500 Subject: [PATCH 032/290] fix(follow-me): Small UI fixes. Does not allow toggling both follow me and follow me recorder. And make when locally enabled show correct status when follow me recorder is selected. --- .../components/native/ModeratorSection.tsx | 16 +++++++++++++--- .../settings/components/web/ModeratorTab.tsx | 16 ++++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/react/features/settings/components/native/ModeratorSection.tsx b/react/features/settings/components/native/ModeratorSection.tsx index b4fb0632ed1c..b1f614b0dfd3 100644 --- a/react/features/settings/components/native/ModeratorSection.tsx +++ b/react/features/settings/components/native/ModeratorSection.tsx @@ -18,7 +18,9 @@ import FormSection from './FormSection'; const ModeratorSection = () => { const dispatch = useDispatch(); const { + followMeActive, followMeEnabled, + followMeRecorderActive, followMeRecorderEnabled, startAudioMuted, startVideoMuted, @@ -50,29 +52,36 @@ const ModeratorSection = () => { dispatch(updateSettings({ soundsReactions: enabled })); }, [ dispatch, updateSettings, setStartReactionsMuted ]); + const followMeRecorderChecked = followMeRecorderEnabled && !followMeRecorderActive; + const moderationSettings = useMemo(() => { const moderation = [ { + disabled: false, label: 'settings.startAudioMuted', state: startAudioMuted, onChange: onStartAudioMutedToggled }, { + disabled: false, label: 'settings.startVideoMuted', state: startVideoMuted, onChange: onStartVideoMutedToggled }, { + disabled: followMeActive || followMeRecorderActive, label: 'settings.followMe', - state: followMeEnabled, + state: followMeEnabled && !followMeActive && !followMeRecorderChecked, onChange: onFollowMeToggled }, { + disabled: followMeRecorderActive || followMeActive, label: 'settings.followMeRecorder', - state: followMeRecorderEnabled, + state: followMeRecorderChecked, onChange: onFollowMeRecorderToggled }, { + disabled: false, label: 'settings.startReactionsMuted', state: startReactionsMuted, onChange: onStartReactionsMutedToggled @@ -100,12 +109,13 @@ const ModeratorSection = () => { { - moderationSettings.map(({ label, state, onChange }) => ( + moderationSettings.map(({ label, state, onChange, disabled }) => ( )) diff --git a/react/features/settings/components/web/ModeratorTab.tsx b/react/features/settings/components/web/ModeratorTab.tsx index 5796d969e843..60be5b0fa21c 100644 --- a/react/features/settings/components/web/ModeratorTab.tsx +++ b/react/features/settings/components/web/ModeratorTab.tsx @@ -150,7 +150,10 @@ class ModeratorTab extends AbstractDialogTab { * @returns {void} */ _onFollowMeEnabledChanged({ target: { checked } }: React.ChangeEvent) { - super._onChange({ followMeEnabled: checked }); + super._onChange({ + followMeEnabled: checked, + followMeRecorderEnabled: checked ? false : undefined + }); } /** @@ -161,7 +164,10 @@ class ModeratorTab extends AbstractDialogTab { * @returns {void} */ _onFollowMeRecorderEnabledChanged({ target: { checked } }: React.ChangeEvent) { - super._onChange({ followMeRecorderEnabled: checked }); + super._onChange({ + followMeEnabled: checked ? false : undefined, + followMeRecorderEnabled: checked + }); } /** @@ -184,6 +190,8 @@ class ModeratorTab extends AbstractDialogTab { } = this.props; const classes = withStyles.getClasses(this.props); + const followMeRecorderChecked = followMeRecorderEnabled && !followMeRecorderActive; + return (
{ name = 'start-video-muted' onChange = { this._onStartVideoMutedChanged } /> Date: Mon, 16 Sep 2024 19:23:09 +0300 Subject: [PATCH 033/290] feat(react-native-sdk): update podspec file --- react-native-sdk/jitsi-meet-rnsdk.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-native-sdk/jitsi-meet-rnsdk.podspec b/react-native-sdk/jitsi-meet-rnsdk.podspec index f33c0878a67f..c6d8773c2d1b 100644 --- a/react-native-sdk/jitsi-meet-rnsdk.podspec +++ b/react-native-sdk/jitsi-meet-rnsdk.podspec @@ -13,7 +13,7 @@ Pod::Spec.new do |s| s.source = { :git => package['repository']['url'], :tag => s.version } s.requires_arc = true - s.platform = :ios, '12.4' + s.platform = :ios, '15.1' s.preserve_paths = 'ios/**/*' s.source_files = 'ios/**/*.{h,m}' From 8d82c20319139e8b6d060d53c179154ecdcc11c1 Mon Sep 17 00:00:00 2001 From: AHMAD KADRI <52747422+ahmadkadri@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:10:44 +0200 Subject: [PATCH 034/290] Accessibility: keyboard navigation on the toolbar (Context menu) (#15060) Accessibility: keyboard navigation on the toolbar (Context menu) --- react/features/base/config/functions.any.ts | 18 ++++++++++++++++++ .../web/audio/AudioSettingsContent.tsx | 1 + .../web/video/VideoSettingsContent.tsx | 1 + .../toolbox/components/web/DialogPortal.ts | 11 +++++++---- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/react/features/base/config/functions.any.ts b/react/features/base/config/functions.any.ts index 918b9eb7b970..53d4c961538a 100644 --- a/react/features/base/config/functions.any.ts +++ b/react/features/base/config/functions.any.ts @@ -381,3 +381,21 @@ export function getLegalUrls(state: IReduxState) { terms: configLegalUrls?.terms || DEFAULT_TERMS_URL }; } + +/** + * Utility function to debounce the execution of a callback function. + * + * @param {Function} callback - The callback to debounce. + * @param {number} delay - The debounce delay in milliseconds. + * @returns {Function} - A debounced function that delays the execution of the callback. + */ +export function debounce(callback: (...args: any[]) => void, delay: number) { + let timerId: any; + + return (...args: any[]) => { + if (timerId) { + clearTimeout(timerId); + } + timerId = setTimeout(() => callback(...args), delay); + }; +} diff --git a/react/features/settings/components/web/audio/AudioSettingsContent.tsx b/react/features/settings/components/web/audio/AudioSettingsContent.tsx index e29f260a0e23..d96619039828 100644 --- a/react/features/settings/components/web/audio/AudioSettingsContent.tsx +++ b/react/features/settings/components/web/audio/AudioSettingsContent.tsx @@ -281,6 +281,7 @@ const AudioSettingsContent = ({ return (