mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
feat(draggable-panels): Enable touch-screen support
Adds touch-screen support for resizing filmstrip and chat panels to enable tablet and touch-laptop users to adjust panel widths. Previously, drag handles only worked with mouse hover, making panels non-resizable on touch devices. Changes: - Implement Pointer Events API for unified mouse/touch handling - Add touch device detection with screen size threshold - Make drag handles always visible on touch devices with padding for easier tapping - Maintain identical visual layout between touch and non-touch versions Touch devices with sufficiently large screens now have fully functional drag handles with appropriate hit targets while smaller devices remain disabled to preserve mobile UX.
This commit is contained in:
3
globals.native.d.ts
vendored
3
globals.native.d.ts
vendored
@@ -25,6 +25,8 @@ interface IWindow {
|
||||
self: any;
|
||||
top: any;
|
||||
|
||||
matchMedia: (query: string) => { matches: boolean; };
|
||||
|
||||
onerror: (event: string, source: any, lineno: any, colno: any, e: Error) => void;
|
||||
onunhandledrejection: (event: any) => void;
|
||||
|
||||
@@ -41,6 +43,7 @@ interface IWindow {
|
||||
interface INavigator {
|
||||
product: string;
|
||||
userAgent: string;
|
||||
maxTouchPoints: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MIN_FILMSTRIP_RESIZE_WIDTH } from '../../filmstrip/constants';
|
||||
import Platform from '../react/Platform';
|
||||
|
||||
/**
|
||||
@@ -30,3 +31,51 @@ export function isIpadMobileBrowser() {
|
||||
return isIosMobileBrowser() && Platform.isPad;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the current device has touch capability.
|
||||
* This includes smartphones, tablets, and laptops with touch screens.
|
||||
*
|
||||
* @returns {boolean} True if the device supports touch events.
|
||||
*/
|
||||
export function isTouchDevice(): boolean {
|
||||
// Check for touch support using multiple methods for better reliability
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check maxTouchPoints (most reliable for modern browsers)
|
||||
if ('maxTouchPoints' in navigator) {
|
||||
return navigator.maxTouchPoints > 0;
|
||||
}
|
||||
|
||||
// Fallback to older touch events check
|
||||
if ('ontouchstart' in window) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for touch-specific media query
|
||||
if (window.matchMedia && window.matchMedia('(pointer: coarse)').matches) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if resize functionality should be enabled based on device capabilities
|
||||
* and screen size. On touch devices, resize is only enabled for larger screens (tablets+).
|
||||
* On non-touch devices (desktop), resize is always enabled.
|
||||
*
|
||||
* @returns {boolean} True if resize functionality should be available to the user.
|
||||
*/
|
||||
export function shouldEnableResize(): boolean {
|
||||
const hasTouch = isTouchDevice();
|
||||
|
||||
// On non-touch devices (desktop), always enable resize
|
||||
if (!hasTouch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// On touch devices, only enable if screen is large enough (tablets, not phones)
|
||||
return window?.innerWidth >= MIN_FILMSTRIP_RESIZE_WIDTH;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { connect, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { isTouchDevice, shouldEnableResize } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconInfo, IconMessage, IconShareDoc, IconSubtitles } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant, getRemoteParticipants, isPrivateChatEnabledSelf } from '../../../base/participants/functions';
|
||||
@@ -23,7 +24,16 @@ import {
|
||||
setUserChatWidth,
|
||||
toggleChat
|
||||
} from '../../actions.web';
|
||||
import { CHAT_SIZE, ChatTabs, OPTION_GROUPCHAT, SMALL_WIDTH_THRESHOLD } from '../../constants';
|
||||
import {
|
||||
CHAT_DRAG_HANDLE_HEIGHT,
|
||||
CHAT_DRAG_HANDLE_OFFSET,
|
||||
CHAT_DRAG_HANDLE_WIDTH,
|
||||
CHAT_SIZE,
|
||||
CHAT_TOUCH_HANDLE_SIZE,
|
||||
ChatTabs,
|
||||
OPTION_GROUPCHAT,
|
||||
SMALL_WIDTH_THRESHOLD
|
||||
} from '../../constants';
|
||||
import { getChatMaxSize, getFocusedTab, isChatDisabled } from '../../functions';
|
||||
import { IChatProps as AbstractProps } from '../../types';
|
||||
|
||||
@@ -99,7 +109,12 @@ interface IProps extends AbstractProps {
|
||||
_width: number;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme, { _isResizing, width }) => {
|
||||
const useStyles = makeStyles<{
|
||||
_isResizing: boolean;
|
||||
isTouch: boolean;
|
||||
resizeEnabled: boolean;
|
||||
width: number;
|
||||
}>()((theme, { _isResizing, isTouch, resizeEnabled, width }) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.palette.ui01,
|
||||
@@ -110,11 +125,15 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
|
||||
width: `${width}px`,
|
||||
zIndex: 300,
|
||||
|
||||
'&:hover, &:focus-within': {
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible'
|
||||
// On non-touch devices (desktop), show handle on hover
|
||||
// On touch devices, handle is always visible if resize is enabled
|
||||
...(!isTouch && {
|
||||
'&:hover, &:focus-within': {
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible'
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
'@media (max-width: 580px)': {
|
||||
height: '100dvh',
|
||||
@@ -178,16 +197,23 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
|
||||
|
||||
dragHandleContainer: {
|
||||
height: '100%',
|
||||
width: '9px',
|
||||
// Touch devices need larger hit target but positioned to not take extra space
|
||||
width: isTouch ? `${CHAT_TOUCH_HANDLE_SIZE}px` : `${CHAT_DRAG_HANDLE_WIDTH}px`,
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
cursor: 'col-resize',
|
||||
display: 'flex',
|
||||
display: resizeEnabled ? 'flex' : 'none', // Hide if resize not enabled
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
visibility: 'hidden',
|
||||
right: '4px',
|
||||
// On touch devices, always visible if resize enabled. On desktop, hidden by default
|
||||
visibility: (isTouch && resizeEnabled) ? 'visible' : 'hidden',
|
||||
// Position touch handle centered on offset from edge, maintaining same gap as non-touch
|
||||
right: isTouch
|
||||
? `${CHAT_DRAG_HANDLE_OFFSET - Math.floor((CHAT_TOUCH_HANDLE_SIZE - CHAT_DRAG_HANDLE_WIDTH) / 2)}px`
|
||||
: `${CHAT_DRAG_HANDLE_OFFSET}px`,
|
||||
top: 0,
|
||||
// Prevent touch scrolling while dragging
|
||||
touchAction: 'none',
|
||||
|
||||
'&:hover': {
|
||||
'& .dragHandle': {
|
||||
@@ -205,10 +231,15 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
|
||||
},
|
||||
|
||||
dragHandle: {
|
||||
// Keep the same visual appearance on all devices
|
||||
backgroundColor: theme.palette.icon02,
|
||||
height: '100px',
|
||||
width: '3px',
|
||||
borderRadius: '1px'
|
||||
height: `${CHAT_DRAG_HANDLE_HEIGHT}px`,
|
||||
width: `${CHAT_DRAG_HANDLE_WIDTH / 3}px`,
|
||||
borderRadius: '1px',
|
||||
// Make more visible when actively shown
|
||||
...(isTouch && resizeEnabled && {
|
||||
backgroundColor: theme.palette.icon01
|
||||
})
|
||||
},
|
||||
|
||||
privateMessageRecipientsList: {
|
||||
@@ -240,7 +271,10 @@ const Chat = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { classes, cx } = useStyles({ _isResizing, width: _width });
|
||||
// Detect touch capability and screen size for resize functionality
|
||||
const isTouch = isTouchDevice();
|
||||
const resizeEnabled = shouldEnableResize();
|
||||
const { classes, cx } = useStyles({ _isResizing, width: _width, isTouch, resizeEnabled });
|
||||
const [ isMouseDown, setIsMouseDown ] = useState(false);
|
||||
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
|
||||
const [ dragChatWidth, setDragChatWidth ] = useState<number | null>(null);
|
||||
@@ -276,16 +310,21 @@ const Chat = ({
|
||||
}, [ participants, defaultRemoteDisplayName, t, notifyTimestamp ]);
|
||||
|
||||
/**
|
||||
* Handles mouse down on the drag handle.
|
||||
* Handles pointer down on the drag handle.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @param {MouseEvent} e - The mouse down event.
|
||||
* @param {React.PointerEvent} e - The pointer down event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragHandleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
const onDragHandlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Store the initial mouse position and chat width
|
||||
// Capture the pointer to ensure we receive all pointer events
|
||||
// even if the pointer moves outside the element
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
|
||||
// Store the initial pointer position and chat width
|
||||
setIsMouseDown(true);
|
||||
setMousePosition(e.clientX);
|
||||
setDragChatWidth(_width);
|
||||
@@ -293,7 +332,7 @@ const Chat = ({
|
||||
// Indicate that resizing is in progress
|
||||
dispatch(setChatIsResizing(true));
|
||||
|
||||
// Add visual feedback that we're dragging
|
||||
// Add visual feedback that we're dragging (cursor for mouse, not visible on touch)
|
||||
document.body.style.cursor = 'col-resize';
|
||||
|
||||
// Disable text selection during resize
|
||||
@@ -301,11 +340,12 @@ const Chat = ({
|
||||
}, [ _width, dispatch ]);
|
||||
|
||||
/**
|
||||
* Drag handle mouse up handler.
|
||||
* Drag handle pointer up handler.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragMouseUp = useCallback(() => {
|
||||
const onDragPointerUp = useCallback(() => {
|
||||
if (isMouseDown) {
|
||||
setIsMouseDown(false);
|
||||
dispatch(setChatIsResizing(false));
|
||||
@@ -317,12 +357,13 @@ const Chat = ({
|
||||
}, [ isMouseDown, dispatch ]);
|
||||
|
||||
/**
|
||||
* Handles drag handle mouse move.
|
||||
* Handles drag handle pointer move.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @param {MouseEvent} e - The mousemove event.
|
||||
* @param {PointerEvent} e - The pointermove event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onChatResize = useCallback(throttle((e: MouseEvent) => {
|
||||
const onChatResize = useCallback(throttle((e: PointerEvent) => {
|
||||
if (isMouseDown && mousePosition !== null && dragChatWidth !== null) {
|
||||
// For chat panel resizing on the left edge:
|
||||
// - Dragging left (decreasing X coordinate) should make the panel wider
|
||||
@@ -346,14 +387,14 @@ const Chat = ({
|
||||
|
||||
// Set up event listeners when component mounts
|
||||
useEffect(() => {
|
||||
document.addEventListener('mouseup', onDragMouseUp);
|
||||
document.addEventListener('mousemove', onChatResize);
|
||||
document.addEventListener('pointerup', onDragPointerUp);
|
||||
document.addEventListener('pointermove', onChatResize);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', onDragMouseUp);
|
||||
document.removeEventListener('mousemove', onChatResize);
|
||||
document.removeEventListener('pointerup', onDragPointerUp);
|
||||
document.removeEventListener('pointermove', onChatResize);
|
||||
};
|
||||
}, [ onDragMouseUp, onChatResize ]);
|
||||
}, [ onDragPointerUp, onChatResize ]);
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
@@ -590,7 +631,7 @@ const Chat = ({
|
||||
(isMouseDown || _isResizing) && 'visible',
|
||||
'dragHandleContainer'
|
||||
) }
|
||||
onMouseDown = { onDragHandleMouseDown }>
|
||||
onPointerDown = { onDragHandlePointerDown }>
|
||||
<div className = { cx(classes.dragHandle, 'dragHandle') } />
|
||||
</div>
|
||||
</div> : null
|
||||
|
||||
@@ -33,6 +33,23 @@ export const MESSAGE_TYPE_REMOTE = 'remote';
|
||||
|
||||
export const SMALL_WIDTH_THRESHOLD = 580;
|
||||
|
||||
/**
|
||||
* Drag handle dimensions for resizable chat.
|
||||
*/
|
||||
export const CHAT_DRAG_HANDLE_WIDTH = 9;
|
||||
export const CHAT_DRAG_HANDLE_HEIGHT = 100;
|
||||
|
||||
/**
|
||||
* Touch target size for chat drag handle on touch devices.
|
||||
* Provides adequate hit area (44px) for comfortable tapping.
|
||||
*/
|
||||
export const CHAT_TOUCH_HANDLE_SIZE = 44;
|
||||
|
||||
/**
|
||||
* Offset from edge for positioning the chat drag handle.
|
||||
*/
|
||||
export const CHAT_DRAG_HANDLE_OFFSET = 4;
|
||||
|
||||
|
||||
/**
|
||||
* Lobby message type.
|
||||
|
||||
@@ -10,7 +10,7 @@ import { withStyles } from 'tss-react/mui';
|
||||
import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { isMobileBrowser, isTouchDevice, shouldEnableResize } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
@@ -32,12 +32,17 @@ import {
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
DEFAULT_FILMSTRIP_WIDTH,
|
||||
DRAG_HANDLE_HEIGHT,
|
||||
DRAG_HANDLE_TOP_PANEL_HEIGHT,
|
||||
DRAG_HANDLE_TOP_PANEL_WIDTH,
|
||||
DRAG_HANDLE_WIDTH,
|
||||
FILMSTRIP_TYPE,
|
||||
MIN_STAGE_VIEW_HEIGHT,
|
||||
MIN_STAGE_VIEW_WIDTH,
|
||||
TILE_HORIZONTAL_MARGIN,
|
||||
TILE_VERTICAL_MARGIN,
|
||||
TOP_FILMSTRIP_HEIGHT
|
||||
TOP_FILMSTRIP_HEIGHT,
|
||||
TOUCH_DRAG_HANDLE_PADDING
|
||||
} from '../../constants';
|
||||
import {
|
||||
getVerticalViewMaxWidth,
|
||||
@@ -61,6 +66,11 @@ const BACKGROUND_COLOR = 'rgba(51, 51, 51, .5)';
|
||||
* @returns {Object}
|
||||
*/
|
||||
function styles(theme: Theme, props: IProps) {
|
||||
const { _topPanelFilmstrip: isTopPanel } = props;
|
||||
|
||||
const _isTouchDevice = isTouchDevice();
|
||||
const resizeEnabled = shouldEnableResize();
|
||||
|
||||
const result = {
|
||||
toggleFilmstripContainer: {
|
||||
display: 'flex',
|
||||
@@ -122,23 +132,27 @@ function styles(theme: Theme, props: IProps) {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
|
||||
'&:hover, &:focus-within': {
|
||||
'& .resizable-filmstrip': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
// On touch devices, handle is always visible via base styles, so no hover needed.
|
||||
// On desktop, show handle on hover/focus.
|
||||
...(!_isTouchDevice && {
|
||||
'&:hover, &:focus-within': {
|
||||
'& .resizable-filmstrip': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
|
||||
'& .filmstrip-hover': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
'& .filmstrip-hover': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
},
|
||||
|
||||
'& .toggleFilmstripContainer': {
|
||||
opacity: 1
|
||||
},
|
||||
'& .toggleFilmstripContainer': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible' as const
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible' as const
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
'.horizontal-filmstrip &.hidden': {
|
||||
bottom: '-50px',
|
||||
@@ -187,14 +201,25 @@ function styles(theme: Theme, props: IProps) {
|
||||
|
||||
dragHandleContainer: {
|
||||
height: '100%',
|
||||
width: '9px',
|
||||
width: `${DRAG_HANDLE_WIDTH}px`,
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative' as const,
|
||||
cursor: 'col-resize',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
visibility: 'hidden' as const,
|
||||
// On touch devices, always visible if resize enabled. On desktop, hidden by default
|
||||
visibility: (_isTouchDevice && resizeEnabled) ? 'visible' as const : 'hidden' as const,
|
||||
marginLeft: 0,
|
||||
marginTop: 0,
|
||||
// Touch devices get padding for easier tapping
|
||||
// Vertical filmstrip: left/right padding. Top panel: top/bottom padding.
|
||||
paddingLeft: _isTouchDevice && !isTopPanel ? `${TOUCH_DRAG_HANDLE_PADDING}px` : 0,
|
||||
paddingRight: _isTouchDevice && !isTopPanel ? `${TOUCH_DRAG_HANDLE_PADDING}px` : 0,
|
||||
paddingTop: _isTouchDevice && isTopPanel ? `${TOUCH_DRAG_HANDLE_PADDING}px` : 0,
|
||||
paddingBottom: _isTouchDevice && isTopPanel ? `${TOUCH_DRAG_HANDLE_PADDING}px` : 0,
|
||||
// Prevent touch scrolling while dragging
|
||||
touchAction: 'none',
|
||||
|
||||
'&:hover': {
|
||||
'& .dragHandle': {
|
||||
@@ -213,20 +238,21 @@ function styles(theme: Theme, props: IProps) {
|
||||
'&.top-panel': {
|
||||
order: 2,
|
||||
width: '100%',
|
||||
height: '9px',
|
||||
height: `${DRAG_HANDLE_WIDTH}px`,
|
||||
cursor: 'row-resize',
|
||||
|
||||
'& .dragHandle': {
|
||||
height: '3px',
|
||||
width: '100px'
|
||||
height: `${DRAG_HANDLE_TOP_PANEL_HEIGHT}px`,
|
||||
width: `${DRAG_HANDLE_TOP_PANEL_WIDTH}px`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
dragHandle: {
|
||||
// Keep the same visual appearance on all devices
|
||||
backgroundColor: theme.palette.icon02,
|
||||
height: '100px',
|
||||
width: '3px',
|
||||
height: `${DRAG_HANDLE_HEIGHT}px`,
|
||||
width: `${DRAG_HANDLE_WIDTH / 3}px`,
|
||||
borderRadius: '1px'
|
||||
}
|
||||
};
|
||||
@@ -313,6 +339,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_isToolboxVisible: Boolean;
|
||||
|
||||
/**
|
||||
* Whether the device has touch capability.
|
||||
*/
|
||||
_isTouchDevice?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the current layout is vertical filmstrip.
|
||||
*/
|
||||
@@ -353,6 +384,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_resizableFilmstrip: boolean;
|
||||
|
||||
/**
|
||||
* Whether resize functionality should be enabled based on device and screen size.
|
||||
*/
|
||||
_resizeEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* The number of rows in tile view.
|
||||
*/
|
||||
@@ -486,8 +522,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
this._onGridItemsRendered = this._onGridItemsRendered.bind(this);
|
||||
this._onListItemsRendered = this._onListItemsRendered.bind(this);
|
||||
this._onToggleButtonTouch = this._onToggleButtonTouch.bind(this);
|
||||
this._onDragHandleMouseDown = this._onDragHandleMouseDown.bind(this);
|
||||
this._onDragMouseUp = this._onDragMouseUp.bind(this);
|
||||
this._onDragHandlePointerDown = this._onDragHandlePointerDown.bind(this);
|
||||
this._onDragHandleClick = this._onDragHandleClick.bind(this);
|
||||
this._onDragHandleTouchStart = this._onDragHandleTouchStart.bind(this);
|
||||
this._onDragPointerUp = this._onDragPointerUp.bind(this);
|
||||
this._onFilmstripResize = this._onFilmstripResize.bind(this);
|
||||
|
||||
this._throttledResize = throttle(
|
||||
@@ -511,10 +549,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
handler: this._onShortcutToggleFilmstrip
|
||||
}));
|
||||
|
||||
document.addEventListener('mouseup', this._onDragMouseUp);
|
||||
document.addEventListener('pointerup', this._onDragPointerUp);
|
||||
|
||||
// @ts-ignore
|
||||
document.addEventListener('mousemove', this._throttledResize);
|
||||
document.addEventListener('pointermove', this._throttledResize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,10 +563,10 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
override componentWillUnmount() {
|
||||
this.props.dispatch(unregisterShortcut('F'));
|
||||
|
||||
document.removeEventListener('mouseup', this._onDragMouseUp);
|
||||
document.removeEventListener('pointerup', this._onDragPointerUp);
|
||||
|
||||
// @ts-ignore
|
||||
document.removeEventListener('mousemove', this._throttledResize);
|
||||
document.removeEventListener('pointermove', this._throttledResize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -665,7 +703,9 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
(isMouseDown || _alwaysShowResizeBar) && 'visible',
|
||||
_topPanelFilmstrip && 'top-panel')
|
||||
}
|
||||
onMouseDown = { this._onDragHandleMouseDown }>
|
||||
onClick = { this._onDragHandleClick }
|
||||
onPointerDown = { this._onDragHandlePointerDown }
|
||||
onTouchStart = { this._onDragHandleTouchStart }>
|
||||
<div className = { clsx(classes.dragHandle, 'dragHandle') } />
|
||||
</div>
|
||||
{filmstrip}
|
||||
@@ -678,14 +718,23 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse down on the drag handle.
|
||||
* Handles pointer down on the drag handle.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @param {MouseEvent} e - The mouse down event.
|
||||
* @param {React.PointerEvent} e - The pointer down event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragHandleMouseDown(e: React.MouseEvent) {
|
||||
_onDragHandlePointerDown(e: React.PointerEvent) {
|
||||
const { _topPanelFilmstrip, _topPanelHeight, _verticalFilmstripWidth } = this.props;
|
||||
|
||||
// Prevent toolbar from appearing and stop event propagation
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Capture the pointer to ensure we receive all pointer events
|
||||
// even if the pointer moves outside the element
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
|
||||
this.setState({
|
||||
isMouseDown: true,
|
||||
mousePosition: _topPanelFilmstrip ? e.clientY : e.clientX,
|
||||
@@ -696,11 +745,33 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag handle mouse up handler.
|
||||
* Prevents click events on drag handle from triggering toolbar.
|
||||
*
|
||||
* @param {React.MouseEvent} e - The click event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragHandleClick(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents touch start events on drag handle from triggering toolbar.
|
||||
*
|
||||
* @param {React.TouchEvent} e - The touch start event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragHandleTouchStart(e: React.TouchEvent) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag handle pointer up handler.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDragMouseUp() {
|
||||
_onDragPointerUp() {
|
||||
if (this.state.isMouseDown) {
|
||||
this.setState({
|
||||
isMouseDown: false
|
||||
@@ -710,12 +781,13 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles drag handle mouse move.
|
||||
* Handles drag handle pointer move.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @param {MouseEvent} e - The mousemove event.
|
||||
* @param {PointerEvent} e - The pointermove event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFilmstripResize(e: React.MouseEvent) {
|
||||
_onFilmstripResize(e: PointerEvent) {
|
||||
if (this.state.isMouseDown) {
|
||||
const {
|
||||
dispatch,
|
||||
@@ -1148,4 +1220,4 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
};
|
||||
}
|
||||
|
||||
export default withStyles(translate(connect(_mapStateToProps)(Filmstrip)), styles);
|
||||
export default translate(connect(_mapStateToProps)(withStyles(Filmstrip, styles)));
|
||||
|
||||
@@ -269,6 +269,19 @@ export const MIN_STAGE_VIEW_HEIGHT = 700;
|
||||
*/
|
||||
export const MIN_STAGE_VIEW_WIDTH = 800;
|
||||
|
||||
/**
|
||||
* Drag handle dimensions for resizable filmstrip.
|
||||
*/
|
||||
export const DRAG_HANDLE_WIDTH = 9;
|
||||
export const DRAG_HANDLE_HEIGHT = 100;
|
||||
export const DRAG_HANDLE_TOP_PANEL_HEIGHT = 3;
|
||||
export const DRAG_HANDLE_TOP_PANEL_WIDTH = 100;
|
||||
|
||||
/**
|
||||
* Touch padding added to each side of drag handle for easier tapping on touch devices.
|
||||
*/
|
||||
export const TOUCH_DRAG_HANDLE_PADDING = 6;
|
||||
|
||||
/**
|
||||
* Horizontal margin used for the vertical filmstrip.
|
||||
*/
|
||||
@@ -298,3 +311,9 @@ export const MAX_ACTIVE_PARTICIPANTS = 6;
|
||||
* Top filmstrip default height.
|
||||
*/
|
||||
export const TOP_FILMSTRIP_HEIGHT = 180;
|
||||
|
||||
/**
|
||||
* Minimum screen width needed for functional filmstrip resizing.
|
||||
* Calculated as stage minimum + filmstrip minimum (800px + 120px = 920px).
|
||||
*/
|
||||
export const MIN_FILMSTRIP_RESIZE_WIDTH = MIN_STAGE_VIEW_WIDTH + DEFAULT_FILMSTRIP_WIDTH;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Theme } from '@mui/material/styles';
|
||||
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { isMobileBrowser } from '../base/environment/utils';
|
||||
import { isTouchDevice, shouldEnableResize } from '../base/environment/utils';
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
DEFAULT_LOCAL_TILE_ASPECT_RATIO,
|
||||
DISPLAY_AVATAR,
|
||||
DISPLAY_VIDEO,
|
||||
DRAG_HANDLE_WIDTH,
|
||||
FILMSTRIP_GRID_BREAKPOINT,
|
||||
FILMSTRIP_TYPE,
|
||||
INDICATORS_TOOLTIP_POSITION,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
|
||||
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
|
||||
TILE_VIEW_GRID_VERTICAL_MARGIN,
|
||||
TOUCH_DRAG_HANDLE_PADDING,
|
||||
VERTICAL_VIEW_HORIZONTAL_MARGIN
|
||||
} from './constants';
|
||||
|
||||
@@ -621,6 +623,7 @@ export function getIndicatorsTooltipPosition(thumbnailType?: string) {
|
||||
|
||||
/**
|
||||
* Returns whether or not the filmstrip is resizable.
|
||||
* On touch devices, resize is only enabled for larger screens (tablets, not phones).
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean}
|
||||
@@ -629,7 +632,7 @@ export function isFilmstripResizable(state: IReduxState) {
|
||||
const { filmstrip } = state['features/base/config'];
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
|
||||
return !filmstrip?.disableResizable && !isMobileBrowser()
|
||||
return !filmstrip?.disableResizable && shouldEnableResize()
|
||||
&& (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW || _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
|
||||
}
|
||||
|
||||
@@ -662,8 +665,13 @@ export function getVerticalViewMaxWidth(state: IReduxState) {
|
||||
|
||||
// Adding 4px for the border-right and margin-right.
|
||||
// On non-resizable filmstrip add 4px for the left margin and border.
|
||||
// Also adding 7px for the scrollbar. Also adding 9px for the drag handle.
|
||||
maxWidth += (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? 9 : 4);
|
||||
// Also adding 7px for the scrollbar.
|
||||
// Drag handle: DRAG_HANDLE_WIDTH + padding (TOUCH_DRAG_HANDLE_PADDING on each side for touch)
|
||||
const dragHandleWidth = isTouchDevice()
|
||||
? DRAG_HANDLE_WIDTH + (TOUCH_DRAG_HANDLE_PADDING * 2)
|
||||
: DRAG_HANDLE_WIDTH;
|
||||
|
||||
maxWidth += (_verticalViewGrid ? 0 : 11) + (_resizableFilmstrip ? dragHandleWidth : 4);
|
||||
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user