lbry-desktop/ui/component/swipeableDrawer/view.jsx
saltrafael b0b2056d78
Allow chat to be resized (#1517)
* Allow drawer to be resized

- Basically re-writes the drag behavior into the Drawer component instead of using SwipeableDrawer, getting more flexibility of where to stop and what to do
- More improvements like the backdrop effect, animations and window resize behavior

* Fix console errors

* Close drawer on unmount so other pages dont load open

* Allow livestream chat to be resized horizontally

* Fix mobile browser size

- address bar etc could be on the way

* Handle popout chat

* Fix pause with floating player
2022-05-18 12:15:12 -04:00

316 lines
9.4 KiB
JavaScript

// @flow
import 'scss/component/_swipeable-drawer.scss';
// $FlowFixMe
import { Global } from '@emotion/react';
// $FlowFixMe
import { grey } from '@mui/material/colors';
import { HEADER_HEIGHT_MOBILE } from 'component/fileRenderFloating/view';
import { PRIMARY_PLAYER_WRAPPER_CLASS, PRIMARY_IMAGE_WRAPPER_CLASS } from 'page/file/view';
import { getMaxLandscapeHeight } from 'component/fileRenderFloating/helper-functions';
import Drawer from '@mui/material/Drawer';
import * as ICONS from 'constants/icons';
import * as React from 'react';
import Button from 'component/button';
import classnames from 'classnames';
const DRAWER_PULLER_HEIGHT = 42;
const TRANSITION_MS = 225;
const TRANSITION_STR = `${TRANSITION_MS}ms cubic-bezier(0, 0, 0.2, 1) 0ms`;
type Props = {
children: Node,
title: any,
hasSubtitle?: boolean,
actions?: any,
// -- redux --
open: boolean,
theme: string,
toggleDrawer: () => void,
};
export default function SwipeableDrawer(props: Props) {
const { title, hasSubtitle, children, open, theme, actions, toggleDrawer } = props;
const drawerRoot = React.useRef();
const backdropRef = React.useRef();
const paperRef = React.useRef();
const pausedByDrawer = React.useRef(false);
const touchPos = React.useRef();
const openPrev = React.useRef(open);
const [playerHeight, setPlayerHeight] = React.useState(getMaxLandscapeHeight());
function handleTouchMove(e) {
const touchPosY = e.touches[0].clientY;
touchPos.current = touchPosY;
const draggingBelowHeader = touchPosY > HEADER_HEIGHT_MOBILE;
if (draggingBelowHeader) {
const root = drawerRoot.current;
if (root) {
root.setAttribute('style', `transform: none !important`);
}
if (paperRef.current) {
paperRef.current.setAttribute('style', `transform: translateY(${touchPosY}px) !important`);
}
// makes the backdrop lighter/darker based on how high/low the drawer is
const backdrop = backdropRef.current;
if (backdrop) {
const isDraggingAboveVideo = touchPosY < playerHeight + HEADER_HEIGHT_MOBILE;
let backdropTop = HEADER_HEIGHT_MOBILE + playerHeight;
// $FlowFixMe
let backdropHeight = document.documentElement.getBoundingClientRect().height - backdropTop;
let opacity = ((touchPosY - HEADER_HEIGHT_MOBILE) / backdropHeight) * -1 + 1;
// increase the backdrop height so it also covers the video when pulling the drawer up
if (isDraggingAboveVideo) {
backdropTop = HEADER_HEIGHT_MOBILE;
backdropHeight = playerHeight;
opacity = ((touchPosY - HEADER_HEIGHT_MOBILE) / backdropHeight) * -1 + 1;
}
backdrop.setAttribute('style', `top: ${backdropTop}px; opacity: ${opacity}`);
}
}
}
function handleTouchEnd() {
// set by touchMove
if (!touchPos.current) return;
const root = drawerRoot.current;
if (root) {
const middleOfVideo = HEADER_HEIGHT_MOBILE + playerHeight / 2;
const drawerMovedFullscreen = touchPos.current < middleOfVideo;
// $FlowFixMe
const restOfPage = document.documentElement.clientHeight - playerHeight - HEADER_HEIGHT_MOBILE;
const draggedBeforeCloseLimit = touchPos.current - playerHeight - HEADER_HEIGHT_MOBILE < restOfPage * 0.2;
const backdrop = backdropRef.current;
if (draggedBeforeCloseLimit) {
const minDrawerHeight = HEADER_HEIGHT_MOBILE + playerHeight;
const positionToStop = drawerMovedFullscreen ? HEADER_HEIGHT_MOBILE : minDrawerHeight;
if (paperRef.current) {
paperRef.current.setAttribute('style', `transform: none !important; transition: transform ${TRANSITION_STR}`);
}
root.setAttribute(
'style',
`transform: translateY(${positionToStop}px) !important; transition: transform ${TRANSITION_STR}`
);
setTimeout(() => {
root.style.height = `calc(100% - ${positionToStop}px)`;
}, TRANSITION_MS);
if (backdrop) {
backdrop.setAttribute('style', 'opacity: 0');
setTimeout(() => {
backdrop.setAttribute('style', `transition: opacity ${TRANSITION_STR}; opacity: 1`);
}, TRANSITION_MS);
}
// Pause video if drawer made fullscreen (above the player)
const playerElement = document.querySelector('.content__viewer--inline');
const videoParent = playerElement && playerElement.querySelector('.video-js');
const isLivestream = videoParent && videoParent.classList.contains('livestreamPlayer');
const videoNode = videoParent && videoParent.querySelector('.vjs-tech');
// $FlowFixMe
const isPlaying = videoNode && !videoNode.paused;
if (videoNode && !isLivestream && isPlaying && drawerMovedFullscreen) {
// $FlowFixMe
videoNode.pause();
pausedByDrawer.current = true;
} else {
handleUnpausePlayer();
}
} else {
handleCloseDrawer();
if (backdrop) {
backdrop.setAttribute('style', 'opacity: 0');
}
}
}
// clear if not being touched anymore
touchPos.current = undefined;
}
function handleUnpausePlayer() {
// Unpause on close and was paused by the drawer
const videoParent = document.querySelector('.video-js');
const videoNode = videoParent && videoParent.querySelector('.vjs-tech');
if (videoNode && pausedByDrawer.current) {
// $FlowFixMe
videoNode.play();
pausedByDrawer.current = false;
}
}
function handleCloseDrawer() {
handleUnpausePlayer();
toggleDrawer();
}
const handleResize = React.useCallback(() => {
const element =
document.querySelector(`.${PRIMARY_IMAGE_WRAPPER_CLASS}`) ||
document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`);
if (!element) return;
const rect = element.getBoundingClientRect();
setPlayerHeight(rect.height);
}, []);
React.useEffect(() => {
// Drawer will follow the cover image on resize, so it's always visible
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]);
// Reset scroll position when opening: avoid broken position where
// the drawer is lower than the video
React.useEffect(() => {
if (open) {
const htmlEl = document.querySelector('html');
if (htmlEl) htmlEl.scrollTop = 0;
}
}, [open]);
React.useEffect(() => {
return () => {
if (openPrev.current) {
handleCloseDrawer();
}
};
// close drawer on unmount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const drawerElemRef = React.useCallback(
(node) => {
if (node) {
const isFullscreenDrawer = node.style.transform.includes(`translateY(${HEADER_HEIGHT_MOBILE}px)`);
const openStateChanged = openPrev.current !== open; // so didn't run because of window resize
if (!isFullscreenDrawer || openStateChanged) {
node.setAttribute(
'style',
`transform: translateY(${HEADER_HEIGHT_MOBILE + playerHeight}px); height: calc(100% - ${
HEADER_HEIGHT_MOBILE + playerHeight
}px);`
);
}
drawerRoot.current = node;
openPrev.current = open;
}
},
[open, playerHeight]
);
return (
<>
<DrawerGlobalStyles open={open} />
<Drawer
ref={drawerElemRef}
anchor="bottom"
open={open}
disableEnforceFocus
ModalProps={{ keepMounted: true, sx: { zIndex: '2' } }}
BackdropProps={{ ref: backdropRef, open, sx: { backgroundColor: 'black' } }}
PaperProps={{ ref: paperRef, sx: { height: `calc(100% - ${DRAWER_PULLER_HEIGHT}px)` } }}
>
{open && (
<div className="swipeable-drawer__header" style={{ top: -DRAWER_PULLER_HEIGHT }}>
<Puller theme={theme} />
<HeaderContents
title={title}
hasSubtitle={hasSubtitle}
actions={actions}
handleClose={handleCloseDrawer}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
/>
</div>
)}
{children}
</Drawer>
</>
);
}
type GlobalStylesProps = {
open: boolean,
};
const DrawerGlobalStyles = (props: GlobalStylesProps) => {
const { open } = props;
return (
<Global
styles={{
'.main-wrapper__inner--filepage': {
overflow: open ? 'hidden' : 'unset',
maxHeight: open ? '100%' : 'unset',
},
}}
/>
);
};
type PullerProps = {
theme: string,
};
const Puller = (props: PullerProps) => {
const { theme } = props;
return (
<span className="swipeable-drawer__puller" style={{ backgroundColor: theme === 'light' ? grey[300] : grey[800] }} />
);
};
type HeaderProps = {
title: any,
hasSubtitle?: boolean,
actions?: any,
handleClose: () => void,
};
const HeaderContents = (props: HeaderProps) => {
const { title, hasSubtitle, actions, handleClose, ...divProps } = props;
return (
<div
className={classnames('swipeable-drawer__header-content', {
'swipeable-drawer__header--with-subtitle': hasSubtitle,
})}
{...divProps}
>
{title}
<div className="swipeable-drawer__header-actions">
{actions}
<Button icon={ICONS.REMOVE} iconSize={16} onClick={handleClose} />
</div>
</div>
);
};