From 0ee2378ff0249dcac5620182efef997e2a8103c3 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Mon, 5 May 2025 16:47:38 -0500 Subject: [PATCH] feat(chat): Make chat panel resizeable --- css/_chat.scss | 26 --- css/_participants-pane.scss | 6 - css/_responsive.scss | 18 -- css/_variables.scss | 1 - css/filmstrip/_tile_view.scss | 12 - modules/UI/videolayout/LargeVideoManager.js | 6 +- react/features/base/responsive-ui/actions.ts | 12 +- .../features/base/responsive-ui/constants.ts | 8 + .../features/base/responsive-ui/functions.ts | 20 ++ react/features/chat/actionTypes.ts | 64 ++++-- react/features/chat/actions.web.ts | 52 ++++- react/features/chat/components/web/Chat.tsx | 188 +++++++++++++++- .../chat/components/web/ChatInput.tsx | 47 +++- react/features/chat/constants.ts | 2 +- react/features/chat/functions.ts | 16 ++ react/features/chat/middleware.ts | 2 + react/features/chat/reducer.ts | 47 +++- react/features/chat/subscriber.native.ts | 0 react/features/chat/subscriber.web.ts | 72 ++++++ .../components/ChromeExtensionBanner.web.tsx | 14 +- .../filmstrip/components/web/Filmstrip.tsx | 207 +++++++++++++++++- .../filmstrip/components/web/styles.ts | 178 --------------- react/features/filmstrip/subscriber.web.ts | 6 - .../large-video/components/LargeVideo.web.tsx | 2 +- .../components/web/ParticipantsPane.tsx | 21 +- react/features/participants-pane/functions.ts | 17 ++ .../components/web/SharedVideo.tsx | 3 +- .../toolbox/components/web/Toolbox.tsx | 3 +- react/features/video-layout/constants.ts | 5 + .../components/VideoQualityLabel.web.tsx | 5 +- react/features/video-quality/selector.ts | 20 ++ .../whiteboard/components/web/Whiteboard.tsx | 4 +- tests/pageobjects/ParticipantsPane.ts | 10 +- 33 files changed, 776 insertions(+), 318 deletions(-) create mode 100644 react/features/base/responsive-ui/functions.ts create mode 100644 react/features/chat/subscriber.native.ts create mode 100644 react/features/chat/subscriber.web.ts delete mode 100644 react/features/filmstrip/components/web/styles.ts diff --git a/css/_chat.scss b/css/_chat.scss index da22d28bb6..b59db4b39b 100644 --- a/css/_chat.scss +++ b/css/_chat.scss @@ -141,32 +141,6 @@ left: 0; } -.smileys-panel { - bottom: 100%; - box-sizing: border-box; - background-color: rgba(0, 0, 0, .6) !important; - height: auto; - display: flex; - overflow: hidden; - position: absolute; - width: calc(#{$sidebarWidth} - 32px); - margin-bottom: 5px; - margin-left: -5px; - - /** - * CSS transitions do not apply for auto dimensions. So to produce the css - * accordion effect for showing and hiding the smiley-panel, while allowing - * for variable panel, height, use a very large max-height and animate off - * of that. - */ - transition: max-height 0.3s; - - #smileysContainer { - background-color: $chatBackgroundColor; - border-top: 1px solid #A4B8D1; - } -} - #smileysContainer .smiley { font-size: 1.625rem; } diff --git a/css/_participants-pane.scss b/css/_participants-pane.scss index c9caf87dca..4ef4d3bd65 100644 --- a/css/_participants-pane.scss +++ b/css/_participants-pane.scss @@ -4,9 +4,3 @@ border-radius: 3px; } } - -.mobile-browser.shift-right { - .participants_pane { - z-index: -1; - } -} diff --git a/css/_responsive.scss b/css/_responsive.scss index 60090deb2e..626af69e2a 100644 --- a/css/_responsive.scss +++ b/css/_responsive.scss @@ -60,21 +60,3 @@ } } } - -.desktop-browser { - &.shift-right { - - @media only screen and (max-width: $verySmallScreen + $sidebarWidth) { - - #videoResolutionLabel { - display: none; - } - .vertical-filmstrip .filmstrip { - display: none; - } - .chrome-extension-banner { - display: none; - } - } - } -} diff --git a/css/_variables.scss b/css/_variables.scss index 1af9c96728..ade184dd71 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -22,7 +22,6 @@ $newToolbarSizeWithPadding: calc(#{$newToolbarSize} + 24px); * Chat */ $chatBackgroundColor: #131519; -$sidebarWidth: 315px; /** * Misc. diff --git a/css/filmstrip/_tile_view.scss b/css/filmstrip/_tile_view.scss index 083b4c083b..d461737086 100644 --- a/css/filmstrip/_tile_view.scss +++ b/css/filmstrip/_tile_view.scss @@ -91,15 +91,3 @@ } } } - -.shift-right .remote-videos > div { - /** - * Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants, - * from which we subtract the chat size. - */ - @media only screen and (max-width: calc(500px + #{$sidebarWidth})) { - video { - object-fit: cover; - } - } -} diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 3895e48740..49064aa315 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -478,9 +478,11 @@ export default class LargeVideoManager { if (isOpen && window.innerWidth > 580) { /** * If chat state is open, we re-compute the container width - * by subtracting the default width of the chat. + * by subtracting the chat width, which may be resized by the user. */ - widthToUse -= CHAT_SIZE; + const chatWidth = state['features/chat'].width?.current ?? CHAT_SIZE; + + widthToUse -= chatWidth; } if (resizableFilmstrip && visible && filmstripWidth.current >= FILMSTRIP_BREAKPOINT) { diff --git a/react/features/base/responsive-ui/actions.ts b/react/features/base/responsive-ui/actions.ts index 1763fd49f2..12d774d185 100644 --- a/react/features/base/responsive-ui/actions.ts +++ b/react/features/base/responsive-ui/actions.ts @@ -2,8 +2,7 @@ import { batch } from 'react-redux'; import { IStore } from '../../app/types'; import { CHAT_SIZE } from '../../chat/constants'; -import { getParticipantsPaneOpen } from '../../participants-pane/functions'; -import theme from '../components/themes/participantsPaneTheme.json'; +import { getParticipantsPaneWidth } from '../../participants-pane/functions'; import { CLIENT_RESIZED, @@ -43,16 +42,13 @@ export function clientResized(clientWidth: number, clientHeight: number) { if (navigator.product !== 'ReactNative') { const state = getState(); - const { isOpen: isChatOpen } = state['features/chat']; - const isParticipantsPaneOpen = getParticipantsPaneOpen(state); + const { isOpen: isChatOpen, width } = state['features/chat']; if (isChatOpen) { - availableWidth -= CHAT_SIZE; + availableWidth -= width?.current ?? CHAT_SIZE; } - if (isParticipantsPaneOpen) { - availableWidth -= theme.participantsPaneWidth; - } + availableWidth -= getParticipantsPaneWidth(state); } batch(() => { diff --git a/react/features/base/responsive-ui/constants.ts b/react/features/base/responsive-ui/constants.ts index dbaa4a12ba..c01056da51 100644 --- a/react/features/base/responsive-ui/constants.ts +++ b/react/features/base/responsive-ui/constants.ts @@ -18,3 +18,11 @@ export const ASPECT_RATIO_WIDE = Symbol('ASPECT_RATIO_WIDE'); * Smallest supported mobile width. */ export const SMALL_MOBILE_WIDTH = '320'; + +/** + * The width for desktop that we start hiding elements from the UI (video quality label, filmstrip, etc). + * This should match the value for $verySmallScreen in _variables.scss. + * + * @type {number} + */ +export const SMALL_DESKTOP_WIDTH = 500; diff --git a/react/features/base/responsive-ui/functions.ts b/react/features/base/responsive-ui/functions.ts new file mode 100644 index 0000000000..fb6cad6eb2 --- /dev/null +++ b/react/features/base/responsive-ui/functions.ts @@ -0,0 +1,20 @@ +import { IStateful } from '../app/types'; +import { isMobileBrowser } from '../environment/utils'; +import { toState } from '../redux/functions'; +import { SMALL_DESKTOP_WIDTH } from './constants'; + +/** + * Determines if the screen is narrow with the chat panel open. If the function returns true video quality label, + * filmstrip, etc will be hidden. + * + * @param {IStateful} stateful - The stateful object representing the application state. + * @returns {boolean} - True if the screen is narrow with the chat panel open, otherwise `false`. + */ +export function isNarrowScreenWithChatOpen(stateful: IStateful) { + const state = toState(stateful); + const isDesktopBrowser = !isMobileBrowser(); + const { isOpen, width } = state['features/chat']; + const { clientWidth } = state['features/base/responsive-ui']; + + return isDesktopBrowser && isOpen && (width?.current + SMALL_DESKTOP_WIDTH) > clientWidth; +} diff --git a/react/features/chat/actionTypes.ts b/react/features/chat/actionTypes.ts index 90bfc9622e..4b8bd690b9 100644 --- a/react/features/chat/actionTypes.ts +++ b/react/features/chat/actionTypes.ts @@ -115,23 +115,51 @@ export const SET_FOCUSED_TAB = 'SET_FOCUSED_TAB'; * type: SET_LOBBY_CHAT_RECIPIENT * } */ - export const SET_LOBBY_CHAT_RECIPIENT = 'SET_LOBBY_CHAT_RECIPIENT'; +export const SET_LOBBY_CHAT_RECIPIENT = 'SET_LOBBY_CHAT_RECIPIENT'; - /** - * The type of action sets the state of lobby messaging status. - * - * { - * type: SET_LOBBY_CHAT_ACTIVE_STATE - * payload: boolean - * } - */ - export const SET_LOBBY_CHAT_ACTIVE_STATE = 'SET_LOBBY_CHAT_ACTIVE_STATE'; +/** + * The type of action sets the state of lobby messaging status. + * + * { + * type: SET_LOBBY_CHAT_ACTIVE_STATE + * payload: boolean + * } + */ +export const SET_LOBBY_CHAT_ACTIVE_STATE = 'SET_LOBBY_CHAT_ACTIVE_STATE'; - /** - * The type of action removes the lobby messaging from participant. - * - * { - * type: REMOVE_LOBBY_CHAT_PARTICIPANT - * } - */ - export const REMOVE_LOBBY_CHAT_PARTICIPANT = 'REMOVE_LOBBY_CHAT_PARTICIPANT'; +/** + * The type of action removes the lobby messaging from participant. + * + * { + * type: REMOVE_LOBBY_CHAT_PARTICIPANT + * } + */ +export const REMOVE_LOBBY_CHAT_PARTICIPANT = 'REMOVE_LOBBY_CHAT_PARTICIPANT'; + +/** + * The type of action which signals to set the width of the chat panel. + * + * { + * type: SET_CHAT_WIDTH, + * width: number + * } + */ +export const SET_CHAT_WIDTH = 'SET_CHAT_WIDTH'; + +/** + * The type of action which sets the width for the chat panel (user resized). + * { + * type: SET_USER_CHAT_WIDTH, + * width: number + * } + */ +export const SET_USER_CHAT_WIDTH = 'SET_USER_CHAT_WIDTH'; + +/** + * The type of action which sets whether the user is resizing the chat panel or not. + * { + * type: SET_CHAT_IS_RESIZING, + * resizing: boolean + * } + */ +export const SET_CHAT_IS_RESIZING = 'SET_CHAT_IS_RESIZING'; diff --git a/react/features/chat/actions.web.ts b/react/features/chat/actions.web.ts index 15b4ba422c..2cdf57f4e2 100644 --- a/react/features/chat/actions.web.ts +++ b/react/features/chat/actions.web.ts @@ -2,7 +2,12 @@ import VideoLayout from '../../../modules/UI/videolayout/VideoLayout'; import { IStore } from '../app/types'; -import { OPEN_CHAT } from './actionTypes'; +import { + OPEN_CHAT, + SET_CHAT_IS_RESIZING, + SET_CHAT_WIDTH, + SET_USER_CHAT_WIDTH +} from './actionTypes'; import { closeChat } from './actions.any'; export * from './actions.any'; @@ -45,3 +50,48 @@ export function toggleChat() { VideoLayout.onResize(); }; } + +/** + * Sets the chat panel's width. + * + * @param {number} width - The new width of the chat panel. + * @returns {{ + * type: SET_CHAT_WIDTH, + * width: number + * }} + */ +export function setChatWidth(width: number) { + return { + type: SET_CHAT_WIDTH, + width + }; +} + +/** + * Sets the chat panel's width and the user preferred width. + * + * @param {number} width - The new width of the chat panel. + * @returns {{ + * type: SET_USER_CHAT_WIDTH, + * width: number + * }} + */ +export function setUserChatWidth(width: number) { + return { + type: SET_USER_CHAT_WIDTH, + width + }; +} + +/** + * Sets whether the user is resizing the chat panel or not. + * + * @param {boolean} resizing - Whether the user is resizing or not. + * @returns {Object} + */ +export function setChatIsResizing(resizing: boolean) { + return { + type: SET_CHAT_IS_RESIZING, + resizing + }; +} diff --git a/react/features/chat/components/web/Chat.tsx b/react/features/chat/components/web/Chat.tsx index 085ecc27a8..efd7703238 100644 --- a/react/features/chat/components/web/Chat.tsx +++ b/react/features/chat/components/web/Chat.tsx @@ -1,16 +1,16 @@ -import React, { useCallback } from 'react'; -import { connect } from 'react-redux'; +import React, { useCallback, useEffect, useState } from 'react'; +import { connect, useSelector } from 'react-redux'; import { makeStyles } from 'tss-react/mui'; +import { throttle } from 'lodash-es'; import { IReduxState } from '../../../app/types'; import { translate } from '../../../base/i18n/functions'; import { getLocalParticipant } from '../../../base/participants/functions'; -import { withPixelLineHeight } from '../../../base/styles/functions.web'; import Tabs from '../../../base/ui/components/web/Tabs'; import { arePollsDisabled } from '../../../conference/functions.any'; import PollsPane from '../../../polls/components/web/PollsPane'; import { isCCTabEnabled } from '../../../subtitles/functions.any'; -import { sendMessage, setFocusedTab, toggleChat } from '../../actions.web'; +import { setChatIsResizing, setUserChatWidth, sendMessage, setFocusedTab, toggleChat } from '../../actions.web'; import { CHAT_SIZE, ChatTabs, SMALL_WIDTH_THRESHOLD } from '../../constants'; import { IChatProps as AbstractProps } from '../../types'; @@ -21,6 +21,7 @@ import DisplayNameForm from './DisplayNameForm'; import KeyboardAvoider from './KeyboardAvoider'; import MessageContainer from './MessageContainer'; import MessageRecipient from './MessageRecipient'; +import { getChatMaxSize } from '../../functions'; interface IProps extends AbstractProps { @@ -49,6 +50,11 @@ interface IProps extends AbstractProps { */ _isPollsEnabled: boolean; + /** + * Whether the user is currently resizing the chat panel. + */ + _isResizing: boolean; + /** * Number of unread poll messages. */ @@ -84,19 +90,30 @@ interface IProps extends AbstractProps { * Whether or not to block chat access with a nickname input form. */ _showNamePrompt: boolean; + + /** + * The current width of the chat panel. + */ + _width: number; } -const useStyles = makeStyles()(theme => { +const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme, { _isResizing, width }) => { return { container: { backgroundColor: theme.palette.ui01, flexShrink: 0, overflow: 'hidden', position: 'relative', - transition: 'width .16s ease-in-out', - width: `${CHAT_SIZE}px`, + transition: _isResizing ? undefined : 'width .16s ease-in-out', + width: `${width}px`, zIndex: 300, + '&:hover, &:focus-within': { + '& .dragHandleContainer': { + visibility: 'visible' + } + }, + '@media (max-width: 580px)': { height: '100dvh', position: 'fixed', @@ -123,7 +140,8 @@ const useStyles = makeStyles()(theme => { alignItems: 'center', boxSizing: 'border-box', color: theme.palette.text01, - ...withPixelLineHeight(theme.typography.heading6), + ...theme.typography.heading6, + fontWeight: theme.typography.heading6.fontWeight as any, '.jitsi-icon': { cursor: 'pointer' @@ -146,6 +164,48 @@ const useStyles = makeStyles()(theme => { pollsPanel: { // extract header + tabs height height: 'calc(100% - 110px)' + }, + + resizableChat: { + flex: 1, + display: 'flex', + flexDirection: 'column', + width: '100%' + }, + + dragHandleContainer: { + height: '100%', + width: '9px', + backgroundColor: 'transparent', + position: 'absolute', + cursor: 'col-resize', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + visibility: 'hidden', + right: '4px', + top: 0, + + '&:hover': { + '& .dragHandle': { + backgroundColor: theme.palette.icon01 + } + }, + + '&.visible': { + visibility: 'visible', + + '& .dragHandle': { + backgroundColor: theme.palette.icon01 + } + } + }, + + dragHandle: { + backgroundColor: theme.palette.icon02, + height: '100px', + width: '3px', + borderRadius: '1px' } }; }); @@ -156,6 +216,7 @@ const Chat = ({ _isPollsEnabled, _isCCTabEnabled, _focusedTab, + _isResizing, _messages, _nbUnreadMessages, _nbUnreadPolls, @@ -164,10 +225,100 @@ const Chat = ({ _onToggleChatTab, _onTogglePollsTab, _showNamePrompt, + _width, dispatch, t }: IProps) => { - const { classes, cx } = useStyles(); + const { classes, cx } = useStyles({ _isResizing, width: _width }); + const [ isMouseDown, setIsMouseDown ] = useState(false); + const [ mousePosition, setMousePosition ] = useState(null); + const [ dragChatWidth, setDragChatWidth ] = useState(null); + const maxChatWidth = useSelector(getChatMaxSize); + + /** + * Handles mouse down on the drag handle. + * + * @param {MouseEvent} e - The mouse down event. + * @returns {void} + */ + const onDragHandleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Store the initial mouse position and chat width + setIsMouseDown(true); + setMousePosition(e.clientX); + setDragChatWidth(_width); + + // Indicate that resizing is in progress + dispatch(setChatIsResizing(true)); + + // Add visual feedback that we're dragging + document.body.style.cursor = 'col-resize'; + + // Disable text selection during resize + document.body.style.userSelect = 'none'; + + console.log('Chat resize: Mouse down', { clientX: e.clientX, initialWidth: _width }); + }, [ _width, dispatch ]); + + /** + * Drag handle mouse up handler. + * + * @returns {void} + */ + const onDragMouseUp = useCallback(() => { + if (isMouseDown) { + setIsMouseDown(false); + dispatch(setChatIsResizing(false)); + + // Restore cursor and text selection + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + + console.log('Chat resize: Mouse up'); + } + }, [ isMouseDown, dispatch ]); + + /** + * Handles drag handle mouse move. + * + * @param {MouseEvent} e - The mousemove event. + * @returns {void} + */ + const onChatResize = useCallback(throttle((e: MouseEvent) => { + // console.log('Chat resize: Mouse move', { clientX: e.clientX, isMouseDown, mousePosition, _width }); + if (isMouseDown && mousePosition !== null && dragChatWidth !== null) { + // For chat panel resizing on the left edge: + // - Dragging left (decreasing X coordinate) should make the panel wider + // - Dragging right (increasing X coordinate) should make the panel narrower + const diff = e.clientX - mousePosition; + + const newWidth = Math.max( + Math.min(dragChatWidth + diff, maxChatWidth), + CHAT_SIZE + ); + + // Update the width only if it has changed + if (newWidth !== _width) { + dispatch(setUserChatWidth(newWidth)); + } + } + }, 50, { + leading: true, + trailing: false + }), [ isMouseDown, mousePosition, dragChatWidth, _width, maxChatWidth, dispatch ]); + + // Set up event listeners when component mounts + useEffect(() => { + document.addEventListener('mouseup', onDragMouseUp); + document.addEventListener('mousemove', onChatResize); + + return () => { + document.removeEventListener('mouseup', onDragMouseUp); + document.removeEventListener('mousemove', onChatResize); + }; + }, [ onDragMouseUp, onChatResize ]); /** * Sends a text message. @@ -329,6 +480,15 @@ const Chat = ({ isCCTabEnabled = { _isCCTabEnabled } isPollsEnabled = { _isPollsEnabled } /> : renderChat()} +
+
+
: null ); }; @@ -349,11 +509,13 @@ const Chat = ({ * _messages: Array, * _nbUnreadMessages: number, * _nbUnreadPolls: number, - * _showNamePrompt: boolean + * _showNamePrompt: boolean, + * _width: number, + * _isResizing: boolean * }} */ function _mapStateToProps(state: IReduxState, _ownProps: any) { - const { isOpen, focusedTab, messages, nbUnreadMessages } = state['features/chat']; + const { isOpen, focusedTab, messages, nbUnreadMessages, width, isResizing } = state['features/chat']; const { nbUnreadPolls } = state['features/polls']; const _localParticipant = getLocalParticipant(state); @@ -366,7 +528,9 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) { _messages: messages, _nbUnreadMessages: nbUnreadMessages, _nbUnreadPolls: nbUnreadPolls, - _showNamePrompt: !_localParticipant?.name + _showNamePrompt: !_localParticipant?.name, + _width: width?.current || CHAT_SIZE, + _isResizing: isResizing }; } diff --git a/react/features/chat/components/web/ChatInput.tsx b/react/features/chat/components/web/ChatInput.tsx index 2e6f06d562..6ebc23061b 100644 --- a/react/features/chat/components/web/ChatInput.tsx +++ b/react/features/chat/components/web/ChatInput.tsx @@ -1,6 +1,8 @@ +import { Theme } from '@mui/material'; import React, { Component, RefObject } from 'react'; import { WithTranslation } from 'react-i18next'; import { connect } from 'react-redux'; +import { withStyles } from 'tss-react/mui'; import { IReduxState, IStore } from '../../../app/types'; import { isMobileBrowser } from '../../../base/environment/utils'; @@ -11,6 +13,31 @@ import Input from '../../../base/ui/components/web/Input'; import { areSmileysDisabled, isSendGroupChatDisabled } from '../../functions'; import SmileysPanel from './SmileysPanel'; +import { CHAT_SIZE } from '../../constants'; + + +const styles = (_theme: Theme, { _chatWidth }: IProps) => { + return { + smileysPanel: { + bottom: '100%', + boxSizing: 'border-box' as const, + backgroundColor: 'rgba(0, 0, 0, .6) !important', + height: 'auto', + display: 'flex' as const, + overflow: 'hidden', + position: 'absolute' as const, + width: `${_chatWidth - 32}px`, + marginBottom: '5px', + marginLeft: '-5px', + transition: 'max-height 0.3s', + + '& #smileysContainer': { + backgroundColor: '#131519', + borderTop: '1px solid #A4B8D1' + } + } + }; +}; /** * The type of the React {@code Component} props of {@link ChatInput}. @@ -22,6 +49,9 @@ interface IProps extends WithTranslation { */ _areSmileysDisabled: boolean; + + _chatWidth: number; + /** * Whether sending group chat messages is disabled. */ @@ -32,6 +62,11 @@ interface IProps extends WithTranslation { */ _privateMessageRecipientId?: string; + /** + * An object containing the CSS classes. + */ + classes?: Partial, string>>; + /** * Invoked to send chat messages. */ @@ -123,6 +158,9 @@ class ChatInput extends Component { * @returns {ReactElement} */ override render() { + const classes = withStyles.getClasses(this.props); + + return (
@@ -130,7 +168,7 @@ class ChatInput extends Component {
+ className = { classes.smileysPanel } >
@@ -291,14 +329,15 @@ class ChatInput extends Component { * }} */ const mapStateToProps = (state: IReduxState) => { - const { privateMessageRecipient } = state['features/chat']; + const { privateMessageRecipient, width } = state['features/chat']; const isGroupChatDisabled = isSendGroupChatDisabled(state); return { _areSmileysDisabled: areSmileysDisabled(state), _privateMessageRecipientId: privateMessageRecipient?.id, - _isSendGroupChatDisabled: isGroupChatDisabled + _isSendGroupChatDisabled: isGroupChatDisabled, + _chatWidth: width.current ?? CHAT_SIZE, }; }; -export default translate(connect(mapStateToProps)(ChatInput)); +export default translate(connect(mapStateToProps)(withStyles(ChatInput, styles))); diff --git a/react/features/chat/constants.ts b/react/features/chat/constants.ts index 64fca88076..c20f0fa0ad 100644 --- a/react/features/chat/constants.ts +++ b/react/features/chat/constants.ts @@ -4,7 +4,7 @@ export const CHAR_LIMIT = 500; /** - * The size of the chat. Equal to $sidebarWidth SCSS variable. + * The initial size of the chat. */ export const CHAT_SIZE = 315; diff --git a/react/features/chat/functions.ts b/react/features/chat/functions.ts index e98099d7d6..fc52a54310 100644 --- a/react/features/chat/functions.ts +++ b/react/features/chat/functions.ts @@ -14,6 +14,8 @@ import { escapeRegexp } from '../base/util/helpers'; import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants'; import { IMessage } from './types'; +import { getParticipantsPaneWidth } from '../participants-pane/functions'; +import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants'; /** * An ASCII emoticon regexp array to find and replace old-style ASCII @@ -209,3 +211,17 @@ export function isSendGroupChatDisabled(state: IReduxState) { return !isJwtFeatureEnabled(state, MEET_FEATURES.SEND_GROUPCHAT, false); } + +/** + * Calculates the maximum width available for the chat panel based on the current window size + * and other UI elements. + * + * @param {IReduxState} state - The Redux state containing the application's current state. + * @returns {number} The maximum width in pixels available for the chat panel. Returns 0 if there + * is no space available. + */ +export function getChatMaxSize(state: IReduxState) { + const { clientWidth } = state['features/base/responsive-ui']; + + return Math.max(clientWidth - getParticipantsPaneWidth(state) - VIDEO_SPACE_MIN_SIZE, 0); +} diff --git a/react/features/chat/middleware.ts b/react/features/chat/middleware.ts index 150f3a7de9..b2323321b8 100644 --- a/react/features/chat/middleware.ts +++ b/react/features/chat/middleware.ts @@ -34,6 +34,8 @@ import { ENDPOINT_REACTION_NAME } from '../reactions/constants'; import { getReactionMessageFromBuffer, isReactionsEnabled } from '../reactions/functions.any'; import { showToolbox } from '../toolbox/actions'; +import './subscriber'; + import { ADD_MESSAGE, CLOSE_CHAT, diff --git a/react/features/chat/reducer.ts b/react/features/chat/reducer.ts index 55940aa316..a0753957d9 100644 --- a/react/features/chat/reducer.ts +++ b/react/features/chat/reducer.ts @@ -10,11 +10,15 @@ import { EDIT_MESSAGE, OPEN_CHAT, REMOVE_LOBBY_CHAT_PARTICIPANT, + SET_CHAT_IS_RESIZING, + SET_CHAT_WIDTH, + SET_FOCUSED_TAB, SET_LOBBY_CHAT_ACTIVE_STATE, SET_LOBBY_CHAT_RECIPIENT, SET_PRIVATE_MESSAGE_RECIPIENT, - SET_FOCUSED_TAB + SET_USER_CHAT_WIDTH } from './actionTypes'; +import { CHAT_SIZE } from './constants'; import { IMessage } from './types'; import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes'; @@ -27,7 +31,12 @@ const DEFAULT_STATE = { privateMessageRecipient: undefined, lobbyMessageRecipient: undefined, isLobbyChatActive: false, - focusedTab: ChatTabs.CHAT + focusedTab: ChatTabs.CHAT, + isResizing: false, + width: { + current: CHAT_SIZE, + userSet: null + } }; export interface IChatState { @@ -35,6 +44,7 @@ export interface IChatState { groupChatWithPermissions: boolean; isLobbyChatActive: boolean; isOpen: boolean; + isResizing: boolean; lastReadMessage?: IMessage; lobbyMessageRecipient?: { id: string; @@ -43,6 +53,10 @@ export interface IChatState { messages: IMessage[]; nbUnreadMessages: number; privateMessageRecipient?: IParticipant; + width: { + current: number; + userSet: number | null; + }; } ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action): IChatState => { @@ -217,6 +231,35 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, ac focusedTab: action.tabId, nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages }; + + case SET_CHAT_WIDTH: { + return { + ...state, + width: { + ...state.width, + current: action.width + } + }; + } + + case SET_USER_CHAT_WIDTH: { + const { width } = action; + + return { + ...state, + width: { + current: width, + userSet: width + } + }; + } + + case SET_CHAT_IS_RESIZING: { + return { + ...state, + isResizing: action.resizing + }; + } } return state; diff --git a/react/features/chat/subscriber.native.ts b/react/features/chat/subscriber.native.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/react/features/chat/subscriber.web.ts b/react/features/chat/subscriber.web.ts new file mode 100644 index 0000000000..4de805b6cf --- /dev/null +++ b/react/features/chat/subscriber.web.ts @@ -0,0 +1,72 @@ +// @ts-ignore +import VideoLayout from '../../../modules/UI/videolayout/VideoLayout'; +import StateListenerRegistry from '../base/redux/StateListenerRegistry'; +import { clientResized } from '../base/responsive-ui/actions'; +import { setChatWidth } from './actions.web'; +import { CHAT_SIZE } from './constants'; +import { getChatMaxSize } from './functions'; + + +// import { setChatWidth } from './actions.web'; + +interface IListenerState { + clientWidth: number; + isOpen: boolean; + maxWidth: number; + width: { + current: number; + userSet: number | null; + }; +} + +/** + * Listens for changes in the client width to determine when to resize the chat panel. + */ +StateListenerRegistry.register( + /* selector */ state => { + return { + clientWidth: state['features/base/responsive-ui']?.clientWidth, + isOpen: state['features/chat'].isOpen, + width: state['features/chat'].width, + maxWidth: getChatMaxSize(state) + }; + }, + /* listener */ ( + currentState: IListenerState, + { dispatch }, + previousState: IListenerState + ) => { + if (currentState.isOpen + && (currentState.clientWidth !== previousState.clientWidth + || currentState.width !== previousState.width)) { + const { userSet = 0 } = currentState.width; + const { maxWidth } = currentState; + let chatPanelWidthChanged = false; + + if (currentState.clientWidth !== previousState.clientWidth) { + if (userSet !== null) { + // if userSet is set, we need to check if it is within the bounds and potentially adjust it. + // This is in the case when screen gets smaller and the user set width is more than the maxWidth + // and we need to set it to the maxWidth. And also when the user set width has been larger than + // the maxWidth and we have reduced the current width to the maxWidth but now the screen gets bigger + // and we can increase the current width. + dispatch(setChatWidth(Math.max(Math.min(maxWidth, userSet), CHAT_SIZE))); + chatPanelWidthChanged = true; + } // else { // when userSet is null: + // no-op. The chat panel width will be the default one which is the min too. + // } + } else { // currentState.width !== previousState.width + chatPanelWidthChanged = true; + } + + if (chatPanelWidthChanged) { + const { innerWidth, innerHeight } = window; + + // Since the videoSpaceWidth relies on the chat panel width, we need to adjust it when the chat panel size changes + dispatch(clientResized(innerWidth, innerHeight)); + } + + // Recompute the large video size when chat is open and window resizes + VideoLayout.onResize(); + } + }); diff --git a/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.tsx b/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.tsx index f12f530333..2f05980941 100644 --- a/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.tsx +++ b/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.tsx @@ -19,6 +19,7 @@ import { IconCloseLarge } from '../../base/icons/svg'; import { browser } from '../../base/lib-jitsi-meet'; import { isVpaasMeeting } from '../../jaas/functions'; import logger from '../logger'; +import { isNarrowScreenWithChatOpen } from '../../base/responsive-ui/functions'; const emptyObject = {}; @@ -57,6 +58,11 @@ interface IProps extends WithTranslation { * Whether it's a vpaas meeting or not. */ isVpaas: boolean; + + /** + * Whether the screen is too small to show the banner or not. + */ + notEnoughAvailableWidth: boolean; } /** @@ -253,6 +259,11 @@ class ChromeExtensionBanner extends PureComponent { return null; } + + if (!this.props.notEnoughAvailableWidth) { + return null; + } + const { bannerCfg, t } = this.props; const mainClassNames = this.props.conference ? 'chrome-extension-banner chrome-extension-banner__pos_in_meeting' @@ -333,7 +344,8 @@ const _mapStateToProps = (state: IReduxState) => { bannerCfg: state['features/base/config'].chromeExtensionBanner || emptyObject, conference: getCurrentConference(state), iAmRecorder: Boolean(state['features/base/config'].iAmRecorder), - isVpaas: isVpaasMeeting(state) + isVpaas: isVpaasMeeting(state), + notEnoughAvailableWidth: isNarrowScreenWithChatOpen(state) }; }; diff --git a/react/features/filmstrip/components/web/Filmstrip.tsx b/react/features/filmstrip/components/web/Filmstrip.tsx index a47183efd0..0c2b4a30f1 100644 --- a/react/features/filmstrip/components/web/Filmstrip.tsx +++ b/react/features/filmstrip/components/web/Filmstrip.tsx @@ -47,12 +47,204 @@ import { import AudioTracksContainer from './AudioTracksContainer'; import Thumbnail from './Thumbnail'; import ThumbnailWrapper from './ThumbnailWrapper'; -import { styles } from './styles'; +import { Theme } from '@mui/material'; +import { isNarrowScreenWithChatOpen } from '../../../base/responsive-ui/functions'; + + +const BACKGROUND_COLOR = 'rgba(51, 51, 51, .5)'; + +/** + * Creates the styles for the component. + * + * @param {Object} theme - The current theme. + * @param {IProps} props - The component props. + * @returns {Object} + */ +function styles(theme: Theme, props: IProps) { + const result = { + toggleFilmstripContainer: { + display: 'flex', + flexWrap: 'nowrap' as const, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: BACKGROUND_COLOR, + width: '32px', + height: '24px', + position: 'absolute' as const, + borderRadius: '4px', + top: 'calc(-24px - 2px)', + left: 'calc(50% - 16px)', + opacity: 0, + transition: 'opacity .3s', + zIndex: 1, + + '&:hover, &:focus-within': { + backgroundColor: theme.palette.ui02 + } + }, + + toggleFilmstripButton: { + fontSize: '14px', + lineHeight: 1.2, + textAlign: 'center' as const, + background: 'transparent', + height: 'auto', + width: '100%', + padding: 0, + margin: 0, + border: 'none', + + '-webkit-appearance': 'none', + + '& svg': { + fill: theme.palette.icon01 + } + }, + + toggleVerticalFilmstripContainer: { + transform: 'rotate(-90deg)', + left: 'calc(-24px - 2px - 4px)', + top: 'calc(50% - 12px)' + }, + + toggleTopPanelContainer: { + transform: 'rotate(180deg)', + bottom: 'calc(-24px - 6px)', + top: 'auto' + }, + + toggleTopPanelContainerHidden: { + visibility: 'hidden' as const + }, + + filmstrip: { + transition: 'background .2s ease-in-out, right 1s, bottom 1s, top 1s, height .3s ease-in', + right: 0, + bottom: 0, + + '&:hover, &:focus-within': { + '& .resizable-filmstrip': { + backgroundColor: BACKGROUND_COLOR + }, + + '& .filmstrip-hover': { + backgroundColor: BACKGROUND_COLOR + }, + + '& .toggleFilmstripContainer': { + opacity: 1 + }, + + '& .dragHandleContainer': { + visibility: 'visible' as const + } + }, + + '.horizontal-filmstrip &.hidden': { + bottom: '-50px', + + '&:hover': { + backgroundColor: 'transparent' + } + }, + + '&.hidden': { + '& .toggleFilmstripContainer': { + opacity: 1 + } + } + }, + + filmstripBackground: { + backgroundColor: theme.palette.uiBackground, + + '&:hover, &:focus-within': { + backgroundColor: theme.palette.uiBackground + } + }, + + resizableFilmstripContainer: { + display: 'flex', + position: 'relative' as const, + flexDirection: 'row' as const, + alignItems: 'center', + height: '100%', + width: '100%', + transition: 'background .2s ease-in-out' as const, + + '& .avatar-container': { + maxWidth: 'initial', + maxHeight: 'initial' + }, + + '&.top-panel-filmstrip': { + flexDirection: 'column' as const + } + }, + + dragHandleContainer: { + height: '100%', + width: '9px', + backgroundColor: 'transparent', + position: 'relative' as const, + cursor: 'col-resize', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + visibility: 'hidden' as const, + + '&:hover': { + '& .dragHandle': { + backgroundColor: theme.palette.icon01 + } + }, + + '&.visible': { + visibility: 'visible' as const, + + '& .dragHandle': { + backgroundColor: theme.palette.icon01 + } + }, + + '&.top-panel': { + order: 2, + width: '100%', + height: '9px', + cursor: 'row-resize', + + '& .dragHandle': { + height: '3px', + width: '100px' + } + } + }, + + dragHandle: { + backgroundColor: theme.palette.icon02, + height: '100px', + width: '3px', + borderRadius: '1px' + } + }; + + if (props._isNarrowScreenWithChatOpen) { + result.filmstrip = { + ...result.filmstrip, + '& .vertical-filmstrip': { + display: 'none' as const + } + } as typeof result.filmstrip; + } + + return result; +} + /** * The type of the React {@code Component} props of {@link Filmstrip}. */ -interface IProps extends WithTranslation { +export interface IProps extends WithTranslation { /** * Additional CSS class names top add to the root. @@ -104,6 +296,11 @@ interface IProps extends WithTranslation { */ _isFilmstripButtonEnabled: boolean; + /** + * Whether the available space is when the chat is open. The filmstrip will be hidden if true. + */ + _isNarrowScreenWithChatOpen: boolean; + /** * Whether or not the toolbox is displayed. */ @@ -384,7 +581,7 @@ class Filmstrip extends PureComponent { } } - let toolbar = null; + let toolbar: React.ReactNode = null; if (!this.props._iAmRecorder && this.props._isFilmstripButtonEnabled && _currentLayout !== LAYOUTS.TILE_VIEW @@ -889,7 +1086,6 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { const { localScreenShare } = state['features/base/participants']; const reduceHeight = state['features/toolbox'].visible && toolbarButtons?.length; const remoteVideosVisible = shouldRemoteVideosBeVisible(state); - const { isOpen: shiftRight } = state['features/chat']; const disableSelfView = getHideSelfView(state); const { videoSpaceWidth, clientHeight } = state['features/base/responsive-ui']; const filmstripDisabled = isFilmstripDisabled(state); @@ -910,7 +1106,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { const videosClassName = `filmstrip__videos${isVisible ? '' : ' hidden'}${_hasScroll ? ' has-scroll' : ''}`; const className = `${remoteVideosVisible || ownProps._verticalViewGrid ? '' : 'hide-videos'} ${ shouldReduceHeight ? 'reduce-height' : '' - } ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${isVisible ? '' : 'hidden'}`.trim(); + } ${collapseTileView ? 'collapse' : ''} ${isVisible ? '' : 'hidden'}`.trim(); const _currentLayout = getCurrentLayout(state); const _isVerticalFilmstrip = _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW @@ -925,6 +1121,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { _hasScroll, _iAmRecorder: Boolean(iAmRecorder), _isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state), + _isNarrowScreenWithChatOpen: isNarrowScreenWithChatOpen(state), _isToolboxVisible: isToolboxVisible(state), _isVerticalFilmstrip, _localScreenShareId: localScreenShare?.id, diff --git a/react/features/filmstrip/components/web/styles.ts b/react/features/filmstrip/components/web/styles.ts deleted file mode 100644 index 87fa2bcc00..0000000000 --- a/react/features/filmstrip/components/web/styles.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Theme } from '@mui/material'; - -const BACKGROUND_COLOR = 'rgba(51, 51, 51, .5)'; - -/** - * Creates the styles for the component. - * - * @param {Object} theme - The current theme. - * @returns {Object} - */ -export const styles = (theme: Theme) => { - return { - toggleFilmstripContainer: { - display: 'flex', - flexWrap: 'nowrap' as const, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: BACKGROUND_COLOR, - width: '32px', - height: '24px', - position: 'absolute' as const, - borderRadius: '4px', - top: 'calc(-24px - 2px)', - left: 'calc(50% - 16px)', - opacity: 0, - transition: 'opacity .3s', - zIndex: 1, - - '&:hover, &:focus-within': { - backgroundColor: theme.palette.ui02 - } - }, - - toggleFilmstripButton: { - fontSize: '14px', - lineHeight: 1.2, - textAlign: 'center' as const, - background: 'transparent', - height: 'auto', - width: '100%', - padding: 0, - margin: 0, - border: 'none', - - '-webkit-appearance': 'none', - - '& svg': { - fill: theme.palette.icon01 - } - }, - - toggleVerticalFilmstripContainer: { - transform: 'rotate(-90deg)', - left: 'calc(-24px - 2px - 4px)', - top: 'calc(50% - 12px)' - }, - - toggleTopPanelContainer: { - transform: 'rotate(180deg)', - bottom: 'calc(-24px - 6px)', - top: 'auto' - }, - - toggleTopPanelContainerHidden: { - visibility: 'hidden' as const - }, - - filmstrip: { - transition: 'background .2s ease-in-out, right 1s, bottom 1s, top 1s, height .3s ease-in', - right: 0, - bottom: 0, - - '&:hover, &:focus-within': { - '& .resizable-filmstrip': { - backgroundColor: BACKGROUND_COLOR - }, - - '& .filmstrip-hover': { - backgroundColor: BACKGROUND_COLOR - }, - - '& .toggleFilmstripContainer': { - opacity: 1 - }, - - '& .dragHandleContainer': { - visibility: 'visible' as const - } - }, - - '.horizontal-filmstrip &.hidden': { - bottom: '-50px', - - '&:hover': { - backgroundColor: 'transparent' - } - }, - - '&.hidden': { - '& .toggleFilmstripContainer': { - opacity: 1 - } - } - }, - - filmstripBackground: { - backgroundColor: theme.palette.uiBackground, - - '&:hover, &:focus-within': { - backgroundColor: theme.palette.uiBackground - } - }, - - resizableFilmstripContainer: { - display: 'flex', - position: 'relative' as const, - flexDirection: 'row' as const, - alignItems: 'center', - height: '100%', - width: '100%', - transition: 'background .2s ease-in-out', - - '& .avatar-container': { - maxWidth: 'initial', - maxHeight: 'initial' - }, - - '&.top-panel-filmstrip': { - flexDirection: 'column' - } - }, - - dragHandleContainer: { - height: '100%', - width: '9px', - backgroundColor: 'transparent', - position: 'relative' as const, - cursor: 'col-resize', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - visibility: 'hidden' as const, - - '&:hover': { - '& .dragHandle': { - backgroundColor: theme.palette.icon01 - } - }, - - '&.visible': { - visibility: 'visible', - - '& .dragHandle': { - backgroundColor: theme.palette.icon01 - } - }, - - '&.top-panel': { - order: 2, - width: '100%', - height: '9px', - cursor: 'row-resize', - - '& .dragHandle': { - height: '3px', - width: '100px' - } - } - }, - - dragHandle: { - backgroundColor: theme.palette.icon02, - height: '100px', - width: '3px', - borderRadius: '1px' - } - }; -}; diff --git a/react/features/filmstrip/subscriber.web.ts b/react/features/filmstrip/subscriber.web.ts index 3d7e55f96f..8892802d41 100644 --- a/react/features/filmstrip/subscriber.web.ts +++ b/react/features/filmstrip/subscriber.web.ts @@ -105,12 +105,6 @@ StateListenerRegistry.register( /* listener */ (isChatOpen, store) => { const { innerWidth, innerHeight } = window; - if (isChatOpen) { - document.body.classList.add('shift-right'); - } else { - document.body.classList.remove('shift-right'); - } - store.dispatch(clientResized(innerWidth, innerHeight)); }); diff --git a/react/features/large-video/components/LargeVideo.web.tsx b/react/features/large-video/components/LargeVideo.web.tsx index b931953132..3bd896d78e 100644 --- a/react/features/large-video/components/LargeVideo.web.tsx +++ b/react/features/large-video/components/LargeVideo.web.tsx @@ -209,7 +209,7 @@ class LargeVideo extends Component { _showSubtitles } = this.props; const style = this._getCustomStyles(); - const className = `videocontainer${_isChatOpen ? ' shift-right' : ''}`; + const className = 'videocontainer'; return (
{ +/** + * Interface representing the properties used for styles. + * + * @property {boolean} [isMobileBrowser] - Indicates whether the application is being accessed from a mobile browser. + * @property {boolean} [isChatOpen] - Specifies whether the chat panel is currently open. + */ +interface IStylesProps { + isChatOpen?: boolean; +} +const useStyles = makeStyles()((theme, { isChatOpen }) => { return { participantsPane: { backgroundColor: theme.palette.ui01, @@ -37,7 +47,7 @@ const useStyles = makeStyles()(theme => { position: 'relative', transition: 'width .16s ease-in-out', width: '315px', - zIndex: 0, + zIndex: isMobileBrowser() && isChatOpen ? -1 : 0, display: 'flex', flexDirection: 'column', fontWeight: 600, @@ -114,7 +124,8 @@ const useStyles = makeStyles()(theme => { }); const ParticipantsPane = () => { - const { classes, cx } = useStyles(); + const isChatOpen = useSelector((state: IReduxState) => state['features/chat'].isOpen); + const { classes } = useStyles({ isChatOpen }); const paneOpen = useSelector(getParticipantsPaneOpen); const isBreakoutRoomsSupported = useSelector((state: IReduxState) => state['features/base/conference']) .conference?.getBreakoutRooms()?.isSupported(); @@ -163,7 +174,9 @@ const ParticipantsPane = () => { } return ( -
+
{ return Boolean(getFeatureFlag(state, PARTICIPANTS_ENABLED, true) && enabled); }; + +/** + * Returns the width of the participants pane based on its open state. + * + * @param {IReduxState} state - The Redux state object containing the application state. + * @returns {number} - The width of the participants pane in pixels when open, or 0 when closed. + */ +export function getParticipantsPaneWidth(state: IReduxState) { + const { isOpen } = getState(state); + + if (isOpen) { + return theme.participantsPaneWidth; + } + + return 0; +} diff --git a/react/features/shared-video/components/web/SharedVideo.tsx b/react/features/shared-video/components/web/SharedVideo.tsx index 897535b0e0..487676ad71 100644 --- a/react/features/shared-video/components/web/SharedVideo.tsx +++ b/react/features/shared-video/components/web/SharedVideo.tsx @@ -165,6 +165,7 @@ function _mapStateToProps(state: IReduxState) { const { videoUrl } = state['features/shared-video']; const { clientHeight, videoSpaceWidth } = state['features/base/responsive-ui']; const { visible, isResizing } = state['features/filmstrip']; + const { isResizing: isChatResizing } = state['features/chat']; const onStage = getLargeVideoParticipant(state)?.fakeParticipant === FakeParticipant.SharedVideo; const isVideoShared = isVideoPlaying(state); @@ -174,7 +175,7 @@ function _mapStateToProps(state: IReduxState) { filmstripVisible: visible, filmstripWidth: getVerticalViewMaxWidth(state), isEnabled: isSharedVideoEnabled(state), - isResizing, + isResizing: isResizing || isChatResizing, isVideoShared, onStage, videoUrl diff --git a/react/features/toolbox/components/web/Toolbox.tsx b/react/features/toolbox/components/web/Toolbox.tsx index 503b3fadec..b44efffc98 100644 --- a/react/features/toolbox/components/web/Toolbox.tsx +++ b/react/features/toolbox/components/web/Toolbox.tsx @@ -88,7 +88,6 @@ export default function Toolbox({ = useSelector((state: IReduxState) => state['features/toolbox'].buttonsWithNotifyClick); const reduxToolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons); const toolbarButtonsToUse = toolbarButtons || reduxToolbarButtons; - const chatOpen = useSelector((state: IReduxState) => state['features/chat'].isOpen); const isDialogVisible = useSelector((state: IReduxState) => Boolean(state['features/base/dialog'].component)); const localParticipant = useSelector(getLocalParticipant); const transcribing = useSelector(isTranscribing); @@ -220,7 +219,7 @@ export default function Toolbox({ const rootClassNames = `new-toolbox ${toolbarVisible ? 'visible' : ''} ${ - toolbarButtonsToUse.length ? '' : 'no-buttons'} ${chatOpen ? 'shift-right' : ''}`; + toolbarButtonsToUse.length ? '' : 'no-buttons'}`; const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu'; const containerClassName = `toolbox-content${isMobile || isNarrowLayout ? ' toolbox-content-mobile' : ''}`; diff --git a/react/features/video-layout/constants.ts b/react/features/video-layout/constants.ts index eb8598bedc..c2b77c7c99 100644 --- a/react/features/video-layout/constants.ts +++ b/react/features/video-layout/constants.ts @@ -22,3 +22,8 @@ export const LAYOUT_CLASSNAMES = { [LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip', [LAYOUTS.STAGE_FILMSTRIP_VIEW]: 'stage-filmstrip' }; + +/** + * The minimum width of the video space. Used for calculating the maximum chat size. + */ +export const VIDEO_SPACE_MIN_SIZE = 500; diff --git a/react/features/video-quality/components/VideoQualityLabel.web.tsx b/react/features/video-quality/components/VideoQualityLabel.web.tsx index fd7efacdf8..c6bb8f6b4d 100644 --- a/react/features/video-quality/components/VideoQualityLabel.web.tsx +++ b/react/features/video-quality/components/VideoQualityLabel.web.tsx @@ -8,9 +8,9 @@ import { IconPerformance } from '../../base/icons/svg'; import Label from '../../base/label/components/web/Label'; import { COLORS } from '../../base/label/constants'; import Tooltip from '../../base/tooltip/components/Tooltip'; -import { shouldDisplayTileView } from '../../video-layout/functions.web'; import VideoQualityDialog from './VideoQualityDialog.web'; +import { shouldDisplayVideoQualityLabel } from '../selector'; /** * React {@code Component} responsible for displaying a label that indicates @@ -23,8 +23,7 @@ import VideoQualityDialog from './VideoQualityDialog.web'; */ const VideoQualityLabel = () => { const _audioOnly = useSelector((state: IReduxState) => state['features/base/audio-only'].enabled); - const _visible = useSelector((state: IReduxState) => !(shouldDisplayTileView(state) - || interfaceConfig.VIDEO_QUALITY_LABEL_DISABLED)); + const _visible = useSelector(shouldDisplayVideoQualityLabel); const dispatch = useDispatch(); const { t } = useTranslation(); diff --git a/react/features/video-quality/selector.ts b/react/features/video-quality/selector.ts index 95647acf8f..f0acf7b86d 100644 --- a/react/features/video-quality/selector.ts +++ b/react/features/video-quality/selector.ts @@ -1,4 +1,6 @@ import { IReduxState } from '../app/types'; +import { isNarrowScreenWithChatOpen } from '../base/responsive-ui/functions'; +import { shouldDisplayTileView } from '../video-layout/functions.any'; /** * Selects the thumbnail height to the quality level mapping from the config. @@ -9,3 +11,21 @@ import { IReduxState } from '../app/types'; export function getMinHeightForQualityLvlMap(state: IReduxState): Map { return state['features/video-quality'].minHeightForQualityLvl; } + +/** + * Determines whether the video quality label should be displayed. + * + * @param {IReduxState} state - The current Redux state of the application. + * @returns {boolean} - True if the video quality label should be displayed, otherwise false. + */ +export function shouldDisplayVideoQualityLabel(state: IReduxState): boolean { + const hideVideoQualityLabel + = shouldDisplayTileView(state) + || interfaceConfig.VIDEO_QUALITY_LABEL_DISABLED + + // Hide the video quality label for desktop browser if the chat is open and there isn't enough space + // to display it. + || isNarrowScreenWithChatOpen(state); + + return !hideVideoQualityLabel; +} diff --git a/react/features/whiteboard/components/web/Whiteboard.tsx b/react/features/whiteboard/components/web/Whiteboard.tsx index 9d2eee8bff..91d679c48f 100644 --- a/react/features/whiteboard/components/web/Whiteboard.tsx +++ b/react/features/whiteboard/components/web/Whiteboard.tsx @@ -50,7 +50,9 @@ const Whiteboard = (props: WithTranslation): JSX.Element => { const isVisible = useSelector(isWhiteboardVisible); const isInTileView = useSelector(shouldDisplayTileView); const { clientHeight, videoSpaceWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']); - const { visible: filmstripVisible, isResizing } = useSelector((state: IReduxState) => state['features/filmstrip']); + const { visible: filmstripVisible, isResizing: isFilmstripResizing } = useSelector((state: IReduxState) => state['features/filmstrip']); + const isChatResizing = useSelector((state: IReduxState) => state['features/chat'].isResizing); + const isResizing = isFilmstripResizing || isChatResizing; const filmstripWidth: number = useSelector(getVerticalViewMaxWidth); const collabDetails = useSelector(getCollabDetails); const collabServerUrl = useSelector(getCollabServerUrl); diff --git a/tests/pageobjects/ParticipantsPane.ts b/tests/pageobjects/ParticipantsPane.ts index 188ece3ff9..36ec97fd67 100644 --- a/tests/pageobjects/ParticipantsPane.ts +++ b/tests/pageobjects/ParticipantsPane.ts @@ -6,9 +6,9 @@ import AVModerationMenu from './AVModerationMenu'; import BasePageObject from './BasePageObject'; /** - * Classname of the closed/hidden participants pane + * ID of the closed/hidden participants pane */ -const PARTICIPANTS_PANE = 'participants_pane'; +const PARTICIPANTS_PANE = 'participants-pane'; const INVITE = 'Invite someone'; @@ -27,7 +27,7 @@ export default class ParticipantsPane extends BasePageObject { * Checks if the pane is open. */ isOpen() { - return this.participant.driver.$(`.${PARTICIPANTS_PANE}`).isExisting(); + return this.participant.driver.$(`#${PARTICIPANTS_PANE}`).isExisting(); } /** @@ -36,7 +36,7 @@ export default class ParticipantsPane extends BasePageObject { async open() { await this.participant.getToolbar().clickParticipantsPaneButton(); - const pane = this.participant.driver.$(`.${PARTICIPANTS_PANE}`); + const pane = this.participant.driver.$(`#${PARTICIPANTS_PANE}`); await pane.waitForExist(); await pane.waitForStable(); @@ -49,7 +49,7 @@ export default class ParticipantsPane extends BasePageObject { async close() { await this.participant.getToolbar().clickCloseParticipantsPaneButton(); - await this.participant.driver.$(`.${PARTICIPANTS_PANE}`).waitForDisplayed({ reverse: true }); + await this.participant.driver.$(`#${PARTICIPANTS_PANE}`).waitForDisplayed({ reverse: true }); } /**