This commit is contained in:
66
react/features/base/responsive-ui/actionTypes.ts
Normal file
66
react/features/base/responsive-ui/actionTypes.ts
Normal 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';
|
||||
|
||||
163
react/features/base/responsive-ui/actions.ts
Normal file
163
react/features/base/responsive-ui/actions.ts
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
28
react/features/base/responsive-ui/constants.ts
Normal file
28
react/features/base/responsive-ui/constants.ts
Normal 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;
|
||||
21
react/features/base/responsive-ui/functions.ts
Normal file
21
react/features/base/responsive-ui/functions.ts
Normal 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;
|
||||
}
|
||||
29
react/features/base/responsive-ui/middleware.native.ts
Normal file
29
react/features/base/responsive-ui/middleware.native.ts
Normal 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;
|
||||
});
|
||||
82
react/features/base/responsive-ui/middleware.web.ts
Normal file
82
react/features/base/responsive-ui/middleware.web.ts
Normal 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;
|
||||
}
|
||||
80
react/features/base/responsive-ui/reducer.ts
Normal file
80
react/features/base/responsive-ui/reducer.ts
Normal 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;
|
||||
});
|
||||
Reference in New Issue
Block a user