fix(toolbar): Hide/Show toolbar on tap on mobile web.

* A tap on video space will toggle the toolbar.
* Double tapping on a tile will pin the participant.
This commit is contained in:
Vlad Piersec 2021-10-25 12:16:03 +03:00 committed by vp8x8
parent 667a6eac80
commit 366dc8d11b
6 changed files with 101 additions and 38 deletions

View File

@ -6,6 +6,7 @@ import React from 'react';
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout'; import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
import { getConferenceNameForTitle } from '../../../base/conference'; import { getConferenceNameForTitle } from '../../../base/conference';
import { connect, disconnect } from '../../../base/connection'; import { connect, disconnect } from '../../../base/connection';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux'; import { connect as reactReduxConnect } from '../../../base/redux';
import { setColorAlpha } from '../../../base/util'; import { setColorAlpha } from '../../../base/util';
@ -18,6 +19,7 @@ import { getIsLobbyVisible } from '../../../lobby/functions';
import { ParticipantsPane } from '../../../participants-pane/components/web'; import { ParticipantsPane } from '../../../participants-pane/components/web';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions'; import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin'; import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { toggleToolboxVisible } from '../../../toolbox/actions.any';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web'; import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
import { JitsiPortal, Toolbox } from '../../../toolbox/components/web'; import { JitsiPortal, Toolbox } from '../../../toolbox/components/web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout'; import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
@ -119,6 +121,7 @@ class Conference extends AbstractConference<Props, *> {
_onMouseLeave: Function; _onMouseLeave: Function;
_onMouseMove: Function; _onMouseMove: Function;
_onShowToolbar: Function; _onShowToolbar: Function;
_onVidespaceTouchStart: Function;
_originalOnMouseMove: Function; _originalOnMouseMove: Function;
_originalOnShowToolbar: Function; _originalOnShowToolbar: Function;
_setBackground: Function; _setBackground: Function;
@ -157,6 +160,7 @@ class Conference extends AbstractConference<Props, *> {
// Bind event handler so it is only bound once for every instance. // Bind event handler so it is only bound once for every instance.
this._onFullScreenChange = this._onFullScreenChange.bind(this); this._onFullScreenChange = this._onFullScreenChange.bind(this);
this._onVidespaceTouchStart = this._onVidespaceTouchStart.bind(this);
this._setBackground = this._setBackground.bind(this); this._setBackground = this._setBackground.bind(this);
} }
@ -229,12 +233,14 @@ class Conference extends AbstractConference<Props, *> {
<div <div
className = { _layoutClassName } className = { _layoutClassName }
id = 'videoconference_page' id = 'videoconference_page'
onMouseMove = { this._onShowToolbar } onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }
ref = { this._setBackground }> ref = { this._setBackground }>
<ConferenceInfo /> <ConferenceInfo />
<Notice /> <Notice />
<div id = 'videospace'> <div
id = 'videospace'
onTouchStart = { this._onVidespaceTouchStart }>
<LargeVideo /> <LargeVideo />
{!_isParticipantsPaneVisible {!_isParticipantsPaneVisible
&& <div id = 'notification-participant-list'> && <div id = 'notification-participant-list'>
@ -292,6 +298,16 @@ class Conference extends AbstractConference<Props, *> {
} }
} }
/**
* Handler used for touch start on Video container.
*
* @private
* @returns {void}
*/
_onVidespaceTouchStart() {
this.props.dispatch(toggleToolboxVisible());
}
/** /**
* Updates the Redux state when full screen mode has been enabled or * Updates the Redux state when full screen mode has been enabled or
* disabled. * disabled.

View File

@ -155,6 +155,7 @@ class Filmstrip extends PureComponent <Props> {
this._listItemKey = this._listItemKey.bind(this); this._listItemKey = this._listItemKey.bind(this);
this._onGridItemsRendered = this._onGridItemsRendered.bind(this); this._onGridItemsRendered = this._onGridItemsRendered.bind(this);
this._onListItemsRendered = this._onListItemsRendered.bind(this); this._onListItemsRendered = this._onListItemsRendered.bind(this);
this._onTouchStart = this._onTouchStart.bind(this);
} }
/** /**
@ -500,6 +501,20 @@ class Filmstrip extends PureComponent <Props> {
this._doToggleFilmstrip(); this._doToggleFilmstrip();
} }
_onTouchStart: (SyntheticEvent<HTMLButtonElement>) => void;
/**
* Handler for onTouchStart.
*
* @private
* @param {Object} e - The synthetic event.
* @returns {void}
*/
_onTouchStart(e: SyntheticEvent<HTMLButtonElement>) {
// Don't propagate the touchStart event so the toolbar doesn't get toggled.
e.stopPropagation();
}
/** /**
* Creates a React Element for changing the visibility of the filmstrip when * Creates a React Element for changing the visibility of the filmstrip when
* clicked. * clicked.
@ -520,6 +535,7 @@ class Filmstrip extends PureComponent <Props> {
id = 'toggleFilmstripButton' id = 'toggleFilmstripButton'
onClick = { this._onToolbarToggleFilmstrip } onClick = { this._onToolbarToggleFilmstrip }
onFocus = { this._onTabIn } onFocus = { this._onTabIn }
onTouchStart = { this._onTouchStart }
tabIndex = { 0 }> tabIndex = { 0 }>
<Icon <Icon
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') } aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }

View File

@ -258,6 +258,12 @@ class Thumbnail extends Component<Props, State> {
*/ */
videoMenuTriggerRef: Object; videoMenuTriggerRef: Object;
/**
* Timeout used to detect double tapping.
* It is active while user has tapped once.
*/
_firstTap: ?TimeoutID;
/** /**
* Initializes a new Thumbnail instance. * Initializes a new Thumbnail instance.
* *
@ -281,6 +287,7 @@ class Thumbnail extends Component<Props, State> {
this.timeoutHandle = null; this.timeoutHandle = null;
this.videoMenuTriggerRef = null; this.videoMenuTriggerRef = null;
this._clearDoubleClickTimeout = this._clearDoubleClickTimeout.bind(this);
this._setInstance = this._setInstance.bind(this); this._setInstance = this._setInstance.bind(this);
this._updateAudioLevel = this._updateAudioLevel.bind(this); this._updateAudioLevel = this._updateAudioLevel.bind(this);
this._onCanPlay = this._onCanPlay.bind(this); this._onCanPlay = this._onCanPlay.bind(this);
@ -437,6 +444,18 @@ class Thumbnail extends Component<Props, State> {
this._stopListeningForAudioUpdates(this.props._audioTrack); this._stopListeningForAudioUpdates(this.props._audioTrack);
} }
_clearDoubleClickTimeout: () => void
/**
* Clears the first click timeout.
*
* @returns {void}
*/
_clearDoubleClickTimeout() {
clearTimeout(this._firstTap);
this._firstTap = undefined;
}
/** /**
* Starts listening for audio level updates from the library. * Starts listening for audio level updates from the library.
* *
@ -580,12 +599,21 @@ class Thumbnail extends Component<Props, State> {
_onTouchStart: () => void; _onTouchStart: () => void;
/** /**
* Set showing popover context menu after x miliseconds. * Handler for touch start.
* *
* @returns {void} * @returns {void}
*/ */
_onTouchStart() { _onTouchStart() {
this.timeoutHandle = setTimeout(this._showPopupMenu, SHOW_TOOLBAR_CONTEXT_MENU_AFTER); this.timeoutHandle = setTimeout(this._showPopupMenu, SHOW_TOOLBAR_CONTEXT_MENU_AFTER);
if (this._firstTap) {
this._clearDoubleClickTimeout();
this._onClick();
return;
}
this._firstTap = setTimeout(this._clearDoubleClickTimeout, 300);
} }
_onTouchEnd: () => void; _onTouchEnd: () => void;
@ -790,7 +818,6 @@ class Thumbnail extends Component<Props, State> {
<span <span
className = { containerClassName } className = { containerClassName }
id = 'localVideoContainer' id = 'localVideoContainer'
onClick = { this._onClick }
{ ...(_isMobile { ...(_isMobile
? { ? {
onTouchEnd: this._onTouchEnd, onTouchEnd: this._onTouchEnd,
@ -798,6 +825,7 @@ class Thumbnail extends Component<Props, State> {
onTouchStart: this._onTouchStart onTouchStart: this._onTouchStart
} }
: { : {
onClick: this._onClick,
onMouseEnter: this._onMouseEnter, onMouseEnter: this._onMouseEnter,
onMouseLeave: this._onMouseLeave onMouseLeave: this._onMouseLeave
} }
@ -932,7 +960,7 @@ class Thumbnail extends Component<Props, State> {
<span <span
className = { containerClassName } className = { containerClassName }
id = { `participant_${id}` } id = { `participant_${id}` }
onClick = { this._onClick } onClick = { _isMobile ? undefined : this._onClick }
{ ...(_isMobile { ...(_isMobile
? { ? {
onTouchEnd: this._onTouchEnd, onTouchEnd: this._onTouchEnd,

View File

@ -4,7 +4,8 @@ import type { Dispatch } from 'redux';
import { import {
SET_TOOLBOX_ENABLED, SET_TOOLBOX_ENABLED,
SET_TOOLBOX_VISIBLE SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes'; } from './actionTypes';
/** /**
@ -43,3 +44,24 @@ export function setToolboxVisible(visible: boolean): Object {
}); });
}; };
} }
/**
* Action to toggle the toolbox visibility.
*
* @returns {Function}
*/
export function toggleToolboxVisible() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { toolbarConfig: { alwaysVisible } } = state['features/base/config'];
const { visible } = state['features/toolbox'];
if (visible && alwaysVisible) {
return;
}
dispatch({
type: TOGGLE_TOOLBOX_VISIBLE
});
};
}

View File

@ -1,28 +1,2 @@
// @flow // @flow
import type { Dispatch } from 'redux';
import { TOGGLE_TOOLBOX_VISIBLE } from './actionTypes';
export * from './actions.any'; export * from './actions.any';
/**
* Action to toggle the toolbox visibility.
*
* @returns {Function}
*/
export function toggleToolboxVisible() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { toolbarConfig: { alwaysVisible } } = state['features/base/config'];
const { visible } = state['features/toolbox'];
if (visible && alwaysVisible) {
return;
}
dispatch({
type: TOGGLE_TOOLBOX_VISIBLE
});
};
}

View File

@ -3,6 +3,7 @@
import type { Dispatch } from 'redux'; import type { Dispatch } from 'redux';
import { overwriteConfig } from '../base/config'; import { overwriteConfig } from '../base/config';
import { isMobileBrowser } from '../base/environment/utils';
import { import {
CLEAR_TOOLBOX_TIMEOUT, CLEAR_TOOLBOX_TIMEOUT,
@ -220,7 +221,8 @@ export function setToolbarHovered(hovered: boolean): Object {
} }
/** /**
* Dispatches an action which sets new timeout and clears the previous one. * Dispatches an action which sets new timeout for the toolbox visibility and clears the previous one.
* On mobile browsers the toolbox does not hide on timeout. It is toggled on simple tap.
* *
* @param {Function} handler - Function to be invoked after the timeout. * @param {Function} handler - Function to be invoked after the timeout.
* @param {number} timeoutMS - Delay. * @param {number} timeoutMS - Delay.
@ -231,10 +233,15 @@ export function setToolbarHovered(hovered: boolean): Object {
* }} * }}
*/ */
export function setToolboxTimeout(handler: Function, timeoutMS: number): Object { export function setToolboxTimeout(handler: Function, timeoutMS: number): Object {
return { return function(dispatch) {
type: SET_TOOLBOX_TIMEOUT, if (isMobileBrowser()) {
handler, return;
timeoutMS }
dispatch({
type: SET_TOOLBOX_TIMEOUT,
handler,
timeoutMS
});
}; };
} }