snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 15:56:32 +05:00
commit 1d2bfcac0c
3159 changed files with 464524 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
/**
* Action type to signal the closing of the custom panel.
*/
export const CUSTOM_PANEL_CLOSE = 'CUSTOM_PANEL_CLOSE';
/**
* Action type to signal the opening of the custom panel.
*/
export const CUSTOM_PANEL_OPEN = 'CUSTOM_PANEL_OPEN';
/**
* Action type to enable or disable the custom panel dynamically.
*/
export const SET_CUSTOM_PANEL_ENABLED = 'SET_CUSTOM_PANEL_ENABLED';
/**
* Action type to set the custom panel width (responsive adjustments).
*/
export const SET_CUSTOM_PANEL_WIDTH = 'SET_CUSTOM_PANEL_WIDTH';
/**
* Action type to set the user-preferred custom panel width (user drag).
*/
export const SET_USER_CUSTOM_PANEL_WIDTH = 'SET_USER_CUSTOM_PANEL_WIDTH';
/**
* Action type to indicate whether the custom panel is being resized.
*/
export const SET_CUSTOM_PANEL_IS_RESIZING = 'SET_CUSTOM_PANEL_IS_RESIZING';

View File

@@ -0,0 +1,83 @@
import {
CUSTOM_PANEL_CLOSE,
CUSTOM_PANEL_OPEN,
SET_CUSTOM_PANEL_ENABLED,
SET_CUSTOM_PANEL_IS_RESIZING,
SET_CUSTOM_PANEL_WIDTH,
SET_USER_CUSTOM_PANEL_WIDTH
} from './actionTypes';
/**
* Action to close the custom panel.
*
* @returns {Object} The action object.
* NOTE: this action is used in the branding files.
*/
export function close() {
return {
type: CUSTOM_PANEL_CLOSE
};
}
/**
* Action to open the custom panel.
*
* @returns {Object} The action object.
*/
export function open() {
return {
type: CUSTOM_PANEL_OPEN
};
}
/**
* Action to enable or disable the custom panel dynamically.
*
* @param {boolean} enabled - Whether the custom panel should be enabled.
* @returns {Object} The action object.
*/
export function setCustomPanelEnabled(enabled: boolean) {
return {
type: SET_CUSTOM_PANEL_ENABLED,
enabled
};
}
/**
* Sets the custom panel width (used for responsive adjustments).
*
* @param {number} width - The new width of the custom panel.
* @returns {Object} The action object.
*/
export function setCustomPanelWidth(width: number) {
return {
type: SET_CUSTOM_PANEL_WIDTH,
width
};
}
/**
* Sets the user-preferred custom panel width (triggered by user drag).
*
* @param {number} width - The new width of the custom panel.
* @returns {Object} The action object.
*/
export function setUserCustomPanelWidth(width: number) {
return {
type: SET_USER_CUSTOM_PANEL_WIDTH,
width
};
}
/**
* Sets whether the user is currently resizing the custom panel.
*
* @param {boolean} resizing - Whether the panel is being resized.
* @returns {Object} The action object.
*/
export function setCustomPanelIsResizing(resizing: boolean) {
return {
type: SET_CUSTOM_PANEL_IS_RESIZING,
resizing
};
}

View File

@@ -0,0 +1,260 @@
import { throttle } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { isTouchDevice, shouldEnableResize } from '../../../base/environment/utils';
import { setCustomPanelIsResizing, setUserCustomPanelWidth } from '../../actions.web';
import {
CUSTOM_PANEL_DRAG_HANDLE_HEIGHT,
CUSTOM_PANEL_DRAG_HANDLE_OFFSET,
CUSTOM_PANEL_DRAG_HANDLE_WIDTH,
CUSTOM_PANEL_TOUCH_HANDLE_SIZE,
DEFAULT_CUSTOM_PANEL_WIDTH
} from '../../constants';
import { getCustomPanelMaxSize, getCustomPanelOpen, isCustomPanelEnabled } from '../../functions';
import CustomPanelContent from './CustomPanelContent';
interface IStylesProps {
/**
* Whether the panel is currently being resized.
*/
isResizing: boolean;
/**
* Whether the device supports touch.
*/
isTouch: boolean;
/**
* Whether resize is enabled.
*/
resizeEnabled: boolean;
/**
* The current width of the panel.
*/
width: number;
}
const useStyles = makeStyles<IStylesProps>()((theme, { isResizing, isTouch, resizeEnabled, width }) => {
return {
container: {
backgroundColor: theme.palette.ui01,
flexShrink: 0,
overflow: 'hidden',
position: 'relative',
transition: isResizing ? undefined : 'width .16s ease-in-out',
width: `${width}px`,
zIndex: 0,
display: 'flex',
flexDirection: 'column',
height: '100%',
// On non-touch devices (desktop), show handle on hover.
// On touch devices, handle is always visible if resize is enabled.
...(!isTouch && {
'&:hover, &:focus-within': {
'& .customPanelDragHandleContainer': {
visibility: 'visible'
}
}
}),
'@media (max-width: 580px)': {
height: '100dvh',
position: 'fixed',
left: 0,
right: 0,
top: 0,
width: '100%',
zIndex: 301
}
},
contentContainer: {
flex: 1,
overflow: 'hidden',
position: 'relative',
width: '100%',
height: '100%'
},
dragHandleContainer: {
height: '100%',
// Touch devices need larger hit target but positioned to not take extra space.
width: isTouch ? `${CUSTOM_PANEL_TOUCH_HANDLE_SIZE}px` : `${CUSTOM_PANEL_DRAG_HANDLE_WIDTH}px`,
backgroundColor: 'transparent',
position: 'absolute',
cursor: 'col-resize',
display: resizeEnabled ? 'flex' : 'none',
alignItems: 'center',
justifyContent: 'center',
// On touch devices, always visible if resize enabled. On desktop, hidden by default.
visibility: (isTouch && resizeEnabled) ? 'visible' : 'hidden',
// Position on LEFT edge of panel (custom panel is rightmost in layout).
left: isTouch
? `${CUSTOM_PANEL_DRAG_HANDLE_OFFSET
- Math.floor((CUSTOM_PANEL_TOUCH_HANDLE_SIZE - CUSTOM_PANEL_DRAG_HANDLE_WIDTH) / 2)}px`
: `${CUSTOM_PANEL_DRAG_HANDLE_OFFSET}px`,
top: 0,
zIndex: 2,
// Prevent touch scrolling while dragging.
touchAction: 'none',
'&:hover': {
'& .customPanelDragHandle': {
backgroundColor: theme.palette.icon01
}
},
'&.visible': {
visibility: 'visible',
'& .customPanelDragHandle': {
backgroundColor: theme.palette.icon01
}
}
},
dragHandle: {
// Keep the same visual appearance on all devices.
backgroundColor: theme.palette.icon02,
height: `${CUSTOM_PANEL_DRAG_HANDLE_HEIGHT}px`,
width: `${CUSTOM_PANEL_DRAG_HANDLE_WIDTH / 3}px`,
borderRadius: '1px',
// Make more visible when actively shown on touch.
...(isTouch && resizeEnabled && {
backgroundColor: theme.palette.icon01
})
}
};
});
/**
* Custom panel container component that handles resize, close button,
* and renders CustomPanelContent inside it.
*
* @returns {JSX.Element | null} The custom panel or null if not open.
*/
export default function CustomPanel(): JSX.Element | null {
const dispatch = useDispatch();
const enabled = useSelector(isCustomPanelEnabled);
const paneOpen = useSelector(getCustomPanelOpen);
const panelWidth = useSelector((state: IReduxState) =>
state['features/custom-panel']?.width?.current ?? DEFAULT_CUSTOM_PANEL_WIDTH);
const isResizing = useSelector((state: IReduxState) =>
state['features/custom-panel']?.isResizing ?? false);
const maxPanelWidth = useSelector(getCustomPanelMaxSize);
const isTouch = isTouchDevice();
const resizeEnabled = shouldEnableResize();
const { classes, cx } = useStyles({ isResizing, width: panelWidth, isTouch, resizeEnabled });
const [ isMouseDown, setIsMouseDown ] = useState(false);
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
const [ dragPanelWidth, setDragPanelWidth ] = useState<number | null>(null);
/**
* Handles pointer down on the drag handle.
* Supports both mouse and touch events via Pointer Events API.
*
* @param {React.PointerEvent} e - The pointer down event.
* @returns {void}
*/
const onDragHandlePointerDown = useCallback((e: React.PointerEvent) => {
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);
setIsMouseDown(true);
setMousePosition(e.clientX);
setDragPanelWidth(panelWidth);
dispatch(setCustomPanelIsResizing(true));
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
}, [ panelWidth, dispatch ]);
/**
* Handles pointer up to end drag resize.
*
* @returns {void}
*/
const onDragPointerUp = useCallback(() => {
if (isMouseDown) {
setIsMouseDown(false);
dispatch(setCustomPanelIsResizing(false));
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
}, [ isMouseDown, dispatch ]);
/**
* Handles pointer move during drag resize.
* Handle is on the LEFT edge, so dragging left (negative diff) widens the panel.
*
* @param {PointerEvent} e - The pointermove event.
* @returns {void}
*/
const onPanelResize = useCallback(throttle((e: PointerEvent) => {
if (isMouseDown && mousePosition !== null && dragPanelWidth !== null) {
const diff = e.clientX - mousePosition;
// Handle is on LEFT edge: dragging left (negative diff) increases width.
const newWidth = Math.max(
Math.min(dragPanelWidth - diff, maxPanelWidth),
DEFAULT_CUSTOM_PANEL_WIDTH
);
if (newWidth !== panelWidth) {
dispatch(setUserCustomPanelWidth(newWidth));
}
}
}, 50, {
leading: true,
trailing: false
}), [ isMouseDown, mousePosition, dragPanelWidth, panelWidth, maxPanelWidth, dispatch ]);
// Set up global event listeners for drag tracking.
useEffect(() => {
document.addEventListener('pointerup', onDragPointerUp);
document.addEventListener('pointermove', onPanelResize);
return () => {
document.removeEventListener('pointerup', onDragPointerUp);
document.removeEventListener('pointermove', onPanelResize);
};
}, [ onDragPointerUp, onPanelResize ]);
if (!enabled || !paneOpen) {
return null;
}
return (
<div
className = { classes.container }
id = 'custom-panel'>
<div
className = { cx(
classes.dragHandleContainer,
(isMouseDown || isResizing) && 'visible',
'customPanelDragHandleContainer'
) }
onPointerDown = { onDragHandlePointerDown }>
<div className = { cx(classes.dragHandle, 'customPanelDragHandle') } />
</div>
<div className = { classes.contentContainer }>
<CustomPanelContent />
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
/**
* Custom panel button placeholder component.
* This file is overridden by jitsi-meet-branding at build time
* to provide the actual button implementation with custom icon.
*
* @returns {null} This placeholder renders nothing.
*/
const CustomPanelButton = (): null => null;
export default CustomPanelButton;

View File

@@ -0,0 +1,11 @@
/**
* Custom panel content placeholder component.
* This file is overridden by jitsi-meet-branding at build time
* to provide the actual panel content (e.g. iframe).
*
* @returns {null} This placeholder renders nothing.
*/
export default function CustomPanelContent(): null {
return null;
}

View File

@@ -0,0 +1,25 @@
/**
* Default width for the custom panel in pixels.
*/
export const DEFAULT_CUSTOM_PANEL_WIDTH = 315;
/**
* Visual width of the drag handle in pixels.
*/
export const CUSTOM_PANEL_DRAG_HANDLE_WIDTH = 9;
/**
* Visual height of the drag handle in pixels.
*/
export const CUSTOM_PANEL_DRAG_HANDLE_HEIGHT = 100;
/**
* Touch target size for the drag handle on touch devices.
* Provides adequate hit area (44px) for comfortable tapping.
*/
export const CUSTOM_PANEL_TOUCH_HANDLE_SIZE = 44;
/**
* Offset from the panel edge for positioning the drag handle.
*/
export const CUSTOM_PANEL_DRAG_HANDLE_OFFSET = 4;

View File

@@ -0,0 +1,5 @@
/**
* Custom panel functions placeholder.
* Override to add custom panel functionality.
*/
export {}; // we need this just to make TS happy!

View File

@@ -0,0 +1,69 @@
import { IReduxState } from '../app/types';
import { CHAT_SIZE } from '../chat/constants';
import { getParticipantsPaneWidth } from '../participants-pane/functions';
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
import { DEFAULT_CUSTOM_PANEL_WIDTH } from './constants';
export * from './functions.custom';
/**
* Returns whether the custom panel is enabled based on Redux state.
* The feature is disabled by default and can be enabled dynamically via console.
*
* @param {IReduxState} state - The Redux state.
* @returns {boolean} Whether the custom panel is enabled.
*/
export function isCustomPanelEnabled(state: IReduxState): boolean {
return Boolean(state['features/custom-panel']?.enabled);
}
/**
* Returns whether the custom panel is currently open.
*
* @param {IReduxState} state - The Redux state.
* @returns {boolean} Whether the custom panel is open.
*/
export function getCustomPanelOpen(state: IReduxState): boolean {
return Boolean(state['features/custom-panel']?.isOpen);
}
/**
* Returns the current configured width of the custom panel from Redux state.
* Falls back to the default width if no dynamic width is set.
*
* @param {IReduxState} state - The Redux state.
* @returns {number} The panel width in pixels.
*/
export function getCustomPanelConfiguredWidth(state: IReduxState): number {
return state['features/custom-panel']?.width?.current ?? DEFAULT_CUSTOM_PANEL_WIDTH;
}
/**
* Returns the current panel width (0 if closed or disabled).
*
* @param {IReduxState} state - The Redux state.
* @returns {number} The panel width in pixels.
*/
export function getCustomPanelWidth(state: IReduxState): number {
if (!isCustomPanelEnabled(state)) {
return 0;
}
return getCustomPanelOpen(state) ? getCustomPanelConfiguredWidth(state) : 0;
}
/**
* Calculates the maximum width available for the custom panel based on the
* current window size and other open UI panels.
*
* @param {IReduxState} state - The Redux state.
* @returns {number} The maximum width in pixels. Returns 0 if no space is available.
*/
export function getCustomPanelMaxSize(state: IReduxState): number {
const { clientWidth } = state['features/base/responsive-ui'];
const { isOpen: isChatOpen, width: chatWidth } = state['features/chat'];
const chatPanelWidth = isChatOpen ? (chatWidth?.current ?? CHAT_SIZE) : 0;
return Math.max(clientWidth - chatPanelWidth - getParticipantsPaneWidth(state) - VIDEO_SPACE_MIN_SIZE, 0);
}

View File

@@ -0,0 +1,29 @@
import { useSelector } from 'react-redux';
import CustomPanelButton from './components/web/CustomPanelButton';
import { isCustomPanelEnabled } from './functions';
/**
* Configuration for the custom panel toolbar button.
*/
const customPanel = {
key: 'custom-panel',
Content: CustomPanelButton,
group: 5
};
/**
* A hook that returns the custom panel button if the feature is enabled.
* Uses useSelector for reactive updates when the feature is toggled dynamically.
*
* @returns {Object | undefined} The button configuration or undefined if disabled.
*/
export function useCustomPanelButton() {
const enabled = useSelector(isCustomPanelEnabled);
if (enabled) {
return customPanel;
}
return undefined;
}

View File

@@ -0,0 +1,4 @@
/**
* Custom panel middleware placeholder.
* Override to add custom panel functionality.
*/

View File

@@ -0,0 +1,2 @@
import './middleware.custom.web';
import './subscriber.web';

View File

@@ -0,0 +1,123 @@
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
CUSTOM_PANEL_CLOSE,
CUSTOM_PANEL_OPEN,
SET_CUSTOM_PANEL_ENABLED,
SET_CUSTOM_PANEL_IS_RESIZING,
SET_CUSTOM_PANEL_WIDTH,
SET_USER_CUSTOM_PANEL_WIDTH
} from './actionTypes';
import { DEFAULT_CUSTOM_PANEL_WIDTH } from './constants';
/**
* The state of the custom panel feature.
*/
export interface ICustomPanelState {
/**
* Whether the custom panel feature is enabled.
* This can be toggled dynamically via console.
*/
enabled: boolean;
/**
* Whether the custom panel is currently open.
*/
isOpen: boolean;
/**
* Whether the user is currently resizing the custom panel.
*/
isResizing: boolean;
/**
* The width state of the custom panel.
*/
width: {
/**
* The current display width in pixels.
*/
current: number;
/**
* The user-preferred width set via drag resize, or null if not set.
*/
userSet: number | null;
};
}
const DEFAULT_STATE: ICustomPanelState = {
enabled: false,
isOpen: false,
isResizing: false,
width: {
current: DEFAULT_CUSTOM_PANEL_WIDTH,
userSet: null
}
};
/**
* Persist only the width subtree so the user's preferred panel width
* survives page reloads.
*/
PersistenceRegistry.register('features/custom-panel', {
enabled: true,
width: true
});
/**
* Listen for actions that mutate the custom panel state.
*/
ReducerRegistry.register(
'features/custom-panel', (state: ICustomPanelState = DEFAULT_STATE, action): ICustomPanelState => {
switch (action.type) {
case CUSTOM_PANEL_CLOSE:
return {
...state,
isOpen: false
};
case CUSTOM_PANEL_OPEN:
return {
...state,
isOpen: true
};
case SET_CUSTOM_PANEL_ENABLED:
return {
...state,
enabled: action.enabled
};
case SET_CUSTOM_PANEL_WIDTH:
return {
...state,
width: {
...state.width,
current: action.width
}
};
case SET_USER_CUSTOM_PANEL_WIDTH:
return {
...state,
width: {
current: action.width,
userSet: action.width
}
};
case SET_CUSTOM_PANEL_IS_RESIZING:
return {
...state,
isResizing: action.resizing
};
default:
return state;
}
}
);

View File

@@ -0,0 +1,89 @@
// @ts-ignore
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { clientResized } from '../base/responsive-ui/actions';
import { setCustomPanelWidth } from './actions.web';
import { DEFAULT_CUSTOM_PANEL_WIDTH } from './constants';
import { getCustomPanelMaxSize } from './functions';
/**
* Listens for changes in the custom panel open state to recompute available
* video space width. Without this, opening or closing the panel does not
* refresh `videoSpaceWidth`, so tile dimensions stay sized for the previous
* layout until something else triggers a recalculation. Mirrors the pattern
* used by chat and the participants pane.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/custom-panel'].isOpen,
/* listener */ (_isOpen, store) => {
const { innerWidth, innerHeight } = window;
store.dispatch(clientResized(innerWidth, innerHeight));
});
interface IListenerState {
clientWidth: number;
isOpen: boolean;
maxWidth: number;
width: {
current: number;
userSet: number | null;
};
}
/**
* Listens for changes in the client width and custom panel width
* to determine when to adjust the panel size for responsive behavior.
*/
StateListenerRegistry.register(
/* selector */ state => {
return {
clientWidth: state['features/base/responsive-ui']?.clientWidth,
isOpen: state['features/custom-panel'].isOpen,
width: state['features/custom-panel'].width,
maxWidth: getCustomPanelMaxSize(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 panelWidthChanged = false;
if (currentState.clientWidth !== previousState.clientWidth) {
if (userSet !== null) {
// If userSet is set, clamp it within the new bounds.
// This handles the case when the screen gets smaller and
// the user-set width exceeds the max, or when the screen
// gets bigger and we can restore the user-set width.
dispatch(setCustomPanelWidth(
Math.max(Math.min(maxWidth, userSet), DEFAULT_CUSTOM_PANEL_WIDTH)
));
panelWidthChanged = true;
}
// else { // when userSet is null:
// no-op. The custom panel width will be the default one which is the min too.
// }
} else {
// Width changed (not clientWidth) — panel was resized by user.
panelWidthChanged = true;
}
if (panelWidthChanged) {
const { innerWidth, innerHeight } = window;
// Recalculate videoSpaceWidth since it depends on the custom panel width.
dispatch(clientResized(innerWidth, innerHeight));
// Recompute the large video size.
VideoLayout.onResize();
}
}
});