init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
/**
* The type of (redux) action which indicates that the client window has been resized.
*
* {
* type: CLIENT_RESIZED
* }
*/
export const CLIENT_RESIZED = 'CLIENT_RESIZED';
/**
* The type of (redux) action which indicates that the insets from the SafeAreaProvider have changed.
*
* {
* type: SAFE_AREA_INSETS_CHANGED,
* insets: Object
* }
*/
export const SAFE_AREA_INSETS_CHANGED = 'SAFE_AREA_INSETS_CHANGED';
/**
* The type of (redux) action which sets the aspect ratio of the app's user
* interface.
*
* {
* type: SET_ASPECT_RATIO,
* aspectRatio: Symbol
* }
*/
export const SET_ASPECT_RATIO = 'SET_ASPECT_RATIO';
/**
* The type of redux action which signals that the reduces UI mode was enabled
* or disabled.
*
* {
* type: SET_REDUCED_UI,
* reducedUI: boolean
* }
*
* @public
*/
export const SET_REDUCED_UI = 'SET_REDUCED_UI';
/**
* The type of (redux) action which tells whether a local or remote participant
* context menu is open.
*
* {
* type: SET_CONTEXT_MENU_OPEN,
* showConnectionInfo: boolean
* }
*/
export const SET_CONTEXT_MENU_OPEN = 'SET_CONTEXT_MENU_OPEN';
/**
* The type of redux action which signals whether we are in narrow layout.
*
* {
* type: SET_NARROW_LAYOUT,
* isNarrow: boolean
* }
*
* @public
*/
export const SET_NARROW_LAYOUT = 'SET_NARROW_LAYOUT';

View File

@@ -0,0 +1,163 @@
import { batch } from 'react-redux';
import { IStore } from '../../app/types';
import { CHAT_SIZE } from '../../chat/constants';
import { getParticipantsPaneWidth } from '../../participants-pane/functions';
import {
CLIENT_RESIZED,
SAFE_AREA_INSETS_CHANGED,
SET_ASPECT_RATIO,
SET_CONTEXT_MENU_OPEN,
SET_NARROW_LAYOUT,
SET_REDUCED_UI
} from './actionTypes';
import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
/**
* Size threshold for determining if we are in reduced UI mode or not.
*
* FIXME The logic to base {@code reducedUI} on a hardcoded width or height is
* very brittle because it's completely disconnected from the UI which wants to
* be rendered and, naturally, it broke on iPad where even the secondary Toolbar
* didn't fit in the height. We do need to measure the actual UI at runtime and
* determine whether and how to render it.
*/
const REDUCED_UI_THRESHOLD = 300;
/**
* Indicates a resize of the window.
*
* @param {number} clientWidth - The width of the window.
* @param {number} clientHeight - The height of the window.
* @returns {Object}
*/
export function clientResized(clientWidth: number, clientHeight: number) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (!clientWidth && !clientHeight) {
return;
}
let availableWidth = clientWidth;
if (navigator.product !== 'ReactNative') {
const state = getState();
const { isOpen: isChatOpen, width } = state['features/chat'];
if (isChatOpen) {
availableWidth -= width?.current ?? CHAT_SIZE;
}
availableWidth -= getParticipantsPaneWidth(state);
}
batch(() => {
dispatch({
type: CLIENT_RESIZED,
clientHeight,
clientWidth,
videoSpaceWidth: availableWidth
});
dispatch(setAspectRatio(availableWidth, clientHeight));
});
};
}
/**
* Sets the aspect ratio of the app's user interface based on specific width and
* height.
*
* @param {number} width - The width of the app's user interface.
* @param {number} height - The height of the app's user interface.
* @returns {{
* type: SET_ASPECT_RATIO,
* aspectRatio: Symbol
* }}
*/
export function setAspectRatio(width: number, height: number) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
// Don't change the aspect ratio if width and height are the same, that
// is, if we transition to a 1:1 aspect ratio.
if (width !== height) {
const aspectRatio
= width < height ? ASPECT_RATIO_NARROW : ASPECT_RATIO_WIDE;
if (aspectRatio
!== getState()['features/base/responsive-ui'].aspectRatio) {
return dispatch({
type: SET_ASPECT_RATIO,
aspectRatio
});
}
}
};
}
/**
* Sets the "reduced UI" property. In reduced UI mode some components will
* be hidden if there is no space to render them.
*
* @param {number} width - Current usable width.
* @param {number} height - Current usable height.
* @returns {{
* type: SET_REDUCED_UI,
* reducedUI: boolean
* }}
*/
export function setReducedUI(width: number, height: number) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const reducedUI = Math.min(width, height) < REDUCED_UI_THRESHOLD;
if (reducedUI !== getState()['features/base/responsive-ui'].reducedUI) {
return dispatch({
type: SET_REDUCED_UI,
reducedUI
});
}
};
}
/**
* Sets whether the local or remote participant context menu is open.
*
* @param {boolean} isOpen - Whether local or remote context menu is open.
* @returns {Object}
*/
export function setParticipantContextMenuOpen(isOpen: boolean) {
return {
type: SET_CONTEXT_MENU_OPEN,
isOpen
};
}
/**
* Sets the insets from the SafeAreaProvider.
*
* @param {Object} insets - The new insets to be set.
* @returns {{
* type: SAFE_AREA_INSETS_CHANGED,
* insets: Object
* }}
*/
export function setSafeAreaInsets(insets: Object) {
return {
type: SAFE_AREA_INSETS_CHANGED,
insets
};
}
/**
* Sets narrow layout.
*
* @param {boolean} isNarrow - Whether is narrow layout.
* @returns {{
* type: SET_NARROW_LAYOUT,
* isNarrow: boolean
* }}
*/
export function setNarrowLayout(isNarrow: boolean) {
return {
type: SET_NARROW_LAYOUT,
isNarrow
};
}

View File

@@ -0,0 +1,64 @@
import React, { useCallback, useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface IProps {
/**
* Any nested components.
*/
children: React.ReactNode;
/**
* The "onLayout" handler.
*/
onDimensionsChanged?: Function;
/**
* The safe are insets handler.
*/
onSafeAreaInsetsChanged?: Function;
}
/**
* A {@link View} which captures the 'onLayout' event and calls a prop with the
* component size.
*
* @param {IProps} props - The read-only properties with which the new
* instance is to be initialized.
* @returns {Component} - Renders the root view and it's children.
*/
export default function DimensionsDetector(props: IProps) {
const { top = 0, right = 0, bottom = 0, left = 0 } = useSafeAreaInsets();
const { children, onDimensionsChanged, onSafeAreaInsetsChanged } = props;
useEffect(() => {
onSafeAreaInsetsChanged?.({
top,
right,
bottom,
left
});
}, [ onSafeAreaInsetsChanged, top, right, bottom, left ]);
/**
* Handles the "on layout" View's event and calls the onDimensionsChanged
* prop.
*
* @param {Object} event - The "on layout" event object/structure passed
* by react-native.
* @private
* @returns {void}
*/
const onLayout = useCallback(({ nativeEvent: { layout: { height, width } } }) => {
onDimensionsChanged?.(width, height);
}, [ onDimensionsChanged ]);
return (
<View
onLayout = { onLayout }
style = { StyleSheet.absoluteFillObject } >
{ children }
</View>
);
}

View File

@@ -0,0 +1,28 @@
/**
* The aspect ratio constant which indicates that the width (of whatever the
* aspect ratio constant is used for) is smaller than the height.
*
* @type {Symbol}
*/
export const ASPECT_RATIO_NARROW = Symbol('ASPECT_RATIO_NARROW');
/**
* The aspect ratio constant which indicates that the width (of whatever the
* aspect ratio constant is used for) is larger than the height.
*
* @type {Symbol}
*/
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;

View File

@@ -0,0 +1,21 @@
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;
}

View File

@@ -0,0 +1,29 @@
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { CLIENT_RESIZED } from './actionTypes';
import { setAspectRatio, setReducedUI } from './actions';
/**
* Middleware that handles window dimension changes and updates the aspect ratio and
* reduced UI modes accordingly.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(({ dispatch }) => next => action => {
const result = next(action);
switch (action.type) {
case CLIENT_RESIZED: {
const { clientWidth: width, clientHeight: height } = action;
dispatch(setAspectRatio(width, height));
dispatch(setReducedUI(width, height));
break;
}
}
return result;
});

View File

@@ -0,0 +1,82 @@
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
import { CONFERENCE_JOINED } from '../conference/actionTypes';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { clientResized } from './actions';
/**
* Dimensions change handler.
*/
let handler: undefined | ((this: Window, ev: UIEvent) => any);
/**
* Middleware that handles window dimension changes.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_UNMOUNT: {
_appWillUnmount();
break;
}
case APP_WILL_MOUNT:
_appWillMount(store);
break;
case CONFERENCE_JOINED: {
const { clientHeight = 0, clientWidth = 0, videoSpaceWidth = 0 } = store.getState()['features/base/responsive-ui'];
if (!clientHeight && !clientWidth && !videoSpaceWidth) {
const {
innerHeight,
innerWidth
} = window;
store.dispatch(clientResized(innerWidth, innerHeight));
}
break;
}
}
return result;
});
/**
* Notifies this feature that the action {@link APP_WILL_MOUNT} is being
* dispatched within a specific redux {@code store}.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @private
* @returns {void}
*/
function _appWillMount(store: IStore) {
handler = () => {
const {
innerHeight,
innerWidth
} = window;
store.dispatch(clientResized(innerWidth, innerHeight));
};
window.addEventListener('resize', handler);
}
/**
* Notifies this feature that the action {@link APP_WILL_UNMOUNT} is being
* dispatched within a specific redux {@code store}.
*
* @private
* @returns {void}
*/
function _appWillUnmount() {
handler && window.removeEventListener('resize', handler);
handler = undefined;
}

View File

@@ -0,0 +1,80 @@
import ReducerRegistry from '../redux/ReducerRegistry';
import { set } from '../redux/functions';
import {
CLIENT_RESIZED,
SAFE_AREA_INSETS_CHANGED,
SET_ASPECT_RATIO,
SET_CONTEXT_MENU_OPEN,
SET_NARROW_LAYOUT,
SET_REDUCED_UI
} from './actionTypes';
import { ASPECT_RATIO_NARROW } from './constants';
const {
innerHeight = 0,
innerWidth = 0
} = window;
/**
* The default/initial redux state of the feature base/responsive-ui.
*/
const DEFAULT_STATE = {
aspectRatio: ASPECT_RATIO_NARROW,
clientHeight: innerHeight,
clientWidth: innerWidth,
isNarrowLayout: false,
reducedUI: false,
contextMenuOpened: false,
videoSpaceWidth: innerWidth
};
export interface IResponsiveUIState {
aspectRatio: Symbol;
clientHeight: number;
clientWidth: number;
contextMenuOpened: boolean;
isNarrowLayout: boolean;
reducedUI: boolean;
safeAreaInsets?: {
bottom: number;
left: number;
right: number;
top: number;
};
videoSpaceWidth: number;
}
ReducerRegistry.register<IResponsiveUIState>('features/base/responsive-ui',
(state = DEFAULT_STATE, action): IResponsiveUIState => {
switch (action.type) {
case CLIENT_RESIZED: {
return {
...state,
clientWidth: action.clientWidth,
clientHeight: action.clientHeight,
videoSpaceWidth: action.videoSpaceWidth
};
}
case SAFE_AREA_INSETS_CHANGED:
return {
...state,
safeAreaInsets: action.insets
};
case SET_ASPECT_RATIO:
return set(state, 'aspectRatio', action.aspectRatio);
case SET_REDUCED_UI:
return set(state, 'reducedUI', action.reducedUI);
case SET_CONTEXT_MENU_OPEN:
return set(state, 'contextMenuOpened', action.isOpen);
case SET_NARROW_LAYOUT:
return set(state, 'isNarrowLayout', action.isNarrow);
}
return state;
});