mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
feat(chat): Make chat panel resizeable
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,3 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-browser.shift-right {
|
||||
.participants_pane {
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ $newToolbarSizeWithPadding: calc(#{$newToolbarSize} + 24px);
|
||||
* Chat
|
||||
*/
|
||||
$chatBackgroundColor: #131519;
|
||||
$sidebarWidth: 315px;
|
||||
|
||||
/**
|
||||
* Misc.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
20
react/features/base/responsive-ui/functions.ts
Normal file
20
react/features/base/responsive-ui/functions.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
const [ dragChatWidth, setDragChatWidth ] = useState<number | null>(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()}
|
||||
<div
|
||||
className = { cx(
|
||||
classes.dragHandleContainer,
|
||||
(isMouseDown || _isResizing) && 'visible',
|
||||
'dragHandleContainer'
|
||||
) }
|
||||
onMouseDown = { onDragHandleMouseDown }>
|
||||
<div className = { cx(classes.dragHandle, 'dragHandle') } />
|
||||
</div>
|
||||
</div> : null
|
||||
);
|
||||
};
|
||||
@@ -349,11 +509,13 @@ const Chat = ({
|
||||
* _messages: Array<Object>,
|
||||
* _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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Invoked to send chat messages.
|
||||
*/
|
||||
@@ -123,6 +158,9 @@ class ChatInput extends Component<IProps, IState> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
|
||||
return (
|
||||
<div className = { `chat-input-container${this.state.message.trim().length ? ' populated' : ''}` }>
|
||||
<div id = 'chat-input' >
|
||||
@@ -130,7 +168,7 @@ class ChatInput extends Component<IProps, IState> {
|
||||
<div
|
||||
className = 'smiley-input'>
|
||||
<div
|
||||
className = 'smileys-panel' >
|
||||
className = { classes.smileysPanel } >
|
||||
<SmileysPanel
|
||||
onSmileySelect = { this._onSmileySelect } />
|
||||
</div>
|
||||
@@ -291,14 +329,15 @@ class ChatInput extends Component<IProps, IState> {
|
||||
* }}
|
||||
*/
|
||||
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)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<IChatState>('features/chat', (state = DEFAULT_STATE, action): IChatState => {
|
||||
@@ -217,6 +231,35 @@ ReducerRegistry.register<IChatState>('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;
|
||||
|
||||
0
react/features/chat/subscriber.native.ts
Normal file
0
react/features/chat/subscriber.native.ts
Normal file
72
react/features/chat/subscriber.web.ts
Normal file
72
react/features/chat/subscriber.web.ts
Normal file
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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<IProps, IState> {
|
||||
|
||||
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)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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 <IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ class LargeVideo extends Component<IProps> {
|
||||
_showSubtitles
|
||||
} = this.props;
|
||||
const style = this._getCustomStyles();
|
||||
const className = `videocontainer${_isChatOpen ? ' shift-right' : ''}`;
|
||||
const className = 'videocontainer';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -27,8 +27,18 @@ import { FooterContextMenu } from './FooterContextMenu';
|
||||
import LobbyParticipants from './LobbyParticipants';
|
||||
import MeetingParticipants from './MeetingParticipants';
|
||||
import VisitorsList from './VisitorsList';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
/**
|
||||
* 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<IStylesProps>()((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 (
|
||||
<div className = { cx('participants_pane', classes.participantsPane) }>
|
||||
<div
|
||||
className = { classes.participantsPane }
|
||||
id = 'participants-pane'>
|
||||
<div className = { classes.header }>
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { t('participantsPane.close', 'Close') }
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
isSupported
|
||||
} from '../av-moderation/functions';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import theme from '../base/components/themes/participantsPaneTheme.json';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { INVITE_ENABLED, PARTICIPANTS_ENABLED } from '../base/flags/constants';
|
||||
import { getFeatureFlag } from '../base/flags/functions';
|
||||
@@ -320,3 +321,19 @@ export const isParticipantsPaneEnabled = (stateful: IStateful) => {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' : ''}`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<number, number> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user