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
This commit is contained in:
parent
0cf8c181df
commit
b0b2056d78
4 changed files with 201 additions and 42 deletions
|
@ -375,9 +375,9 @@ export default function FileRenderFloating(props: Props) {
|
|||
|
||||
return (
|
||||
<Draggable
|
||||
onDrag={!isMobile ? handleDragMove : null}
|
||||
onStart={!isMobile ? handleDragStart : null}
|
||||
onStop={!isMobile ? handleDragStop : null}
|
||||
onDrag={!isMobile ? handleDragMove : undefined}
|
||||
onStart={!isMobile ? handleDragStart : undefined}
|
||||
onStop={!isMobile ? handleDragStop : undefined}
|
||||
defaultPosition={position}
|
||||
position={isFloating ? position : { x: 0, y: 0 }}
|
||||
bounds="parent"
|
||||
|
|
|
@ -9,13 +9,15 @@ 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 { SwipeableDrawer as MUIDrawer } from '@mui/material';
|
||||
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,
|
||||
|
@ -31,9 +33,134 @@ type Props = {
|
|||
export default function SwipeableDrawer(props: Props) {
|
||||
const { title, hasSubtitle, children, open, theme, actions, toggleDrawer } = props;
|
||||
|
||||
const [coverHeight, setCoverHeight] = React.useState();
|
||||
const drawerRoot = React.useRef();
|
||||
const backdropRef = React.useRef();
|
||||
const paperRef = React.useRef();
|
||||
|
||||
const videoHeight = coverHeight || getMaxLandscapeHeight() || 0;
|
||||
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 =
|
||||
|
@ -43,19 +170,16 @@ export default function SwipeableDrawer(props: Props) {
|
|||
if (!element) return;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
setCoverHeight(rect.height);
|
||||
setPlayerHeight(rect.height);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Drawer will follow the cover image on resize, so it's always visible
|
||||
if (open) {
|
||||
handleResize();
|
||||
handleResize();
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}
|
||||
}, [handleResize, open]);
|
||||
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
|
||||
|
@ -66,57 +190,85 @@ export default function SwipeableDrawer(props: Props) {
|
|||
}
|
||||
}, [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} videoHeight={videoHeight} />
|
||||
<DrawerGlobalStyles open={open} />
|
||||
|
||||
<MUIDrawer
|
||||
<Drawer
|
||||
ref={drawerElemRef}
|
||||
anchor="bottom"
|
||||
open={open}
|
||||
onClose={toggleDrawer}
|
||||
onOpen={toggleDrawer}
|
||||
hideBackdrop
|
||||
disableEnforceFocus
|
||||
disablePortal
|
||||
disableSwipeToOpen
|
||||
ModalProps={{ keepMounted: true }}
|
||||
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} toggleDrawer={toggleDrawer} />
|
||||
<HeaderContents
|
||||
title={title}
|
||||
hasSubtitle={hasSubtitle}
|
||||
actions={actions}
|
||||
handleClose={handleCloseDrawer}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</MUIDrawer>
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type GlobalStylesProps = {
|
||||
open: boolean,
|
||||
videoHeight: number,
|
||||
};
|
||||
|
||||
const DrawerGlobalStyles = (props: GlobalStylesProps) => {
|
||||
const { open, videoHeight } = props;
|
||||
const { open } = props;
|
||||
|
||||
return (
|
||||
<Global
|
||||
styles={{
|
||||
'.main-wrapper__inner--filepage': {
|
||||
overflow: open ? 'hidden' : 'unset',
|
||||
maxHeight: open ? '100vh' : 'unset',
|
||||
},
|
||||
'.main-wrapper .MuiDrawer-root': {
|
||||
top: `calc(${HEADER_HEIGHT_MOBILE}px + ${videoHeight}px) !important`,
|
||||
},
|
||||
'.main-wrapper .MuiDrawer-root > .MuiPaper-root': {
|
||||
overflow: 'visible',
|
||||
color: 'var(--color-text)',
|
||||
position: 'absolute',
|
||||
height: `calc(100% - ${DRAWER_PULLER_HEIGHT}px)`,
|
||||
maxHeight: open ? '100%' : 'unset',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -139,24 +291,25 @@ type HeaderProps = {
|
|||
title: any,
|
||||
hasSubtitle?: boolean,
|
||||
actions?: any,
|
||||
toggleDrawer: () => void,
|
||||
handleClose: () => void,
|
||||
};
|
||||
|
||||
const HeaderContents = (props: HeaderProps) => {
|
||||
const { title, hasSubtitle, actions, toggleDrawer } = props;
|
||||
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={toggleDrawer} />
|
||||
<Button icon={ICONS.REMOVE} iconSize={16} onClick={handleClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -28,6 +28,9 @@ $recent-msg-button__height: 2rem;
|
|||
@media (min-width: $breakpoint-small) {
|
||||
top: calc(var(--header-height) + var(--spacing-m)) !important;
|
||||
position: sticky;
|
||||
resize: horizontal;
|
||||
overflow: hidden;
|
||||
max-width: 30vw;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
|
@ -154,6 +157,8 @@ $recent-msg-button__height: 2rem;
|
|||
height: 100vh !important;
|
||||
top: 0 !important;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
max-width: unset;
|
||||
|
||||
.livestreamComments__wrapper {
|
||||
height: 95vh !important;
|
||||
|
@ -488,7 +493,6 @@ $recent-msg-button__height: 2rem;
|
|||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: var(--spacing-xs);
|
||||
width: var(--livestream-comments-width);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -578,7 +582,6 @@ $recent-msg-button__height: 2rem;
|
|||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: var(--spacing-xs);
|
||||
width: var(--livestream-comments-width);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -592,7 +592,10 @@ body {
|
|||
max-width: var(--page-max-width--filepage);
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr minmax(0, var(--livestream-comments-width));
|
||||
|
||||
$file-min-width: calc(var(--page-max-width--filepage) - var(--livestream-comments-width));
|
||||
grid-template-columns: minmax($file-min-width, 0) 1fr;
|
||||
|
||||
justify-content: space-between;
|
||||
justify-items: end;
|
||||
gap: var(--spacing-m);
|
||||
|
|
Loading…
Reference in a new issue