Create swipeableDrawer component

- CommentsList needs to return a title with comment amounts
- mobile player dimensions needed to fill in the cover
- hid livestream header for now until figuring out a better presentation
- ~colum-reverse~ was causing problems with MUI's drawer scrolling, so reversed the chat order and made it ~column~ by default
- Hid bottom expand navigation if component not yet opened
- some other style changes in the middle
This commit is contained in:
Rafael 2022-02-02 09:44:33 -03:00 committed by Thomas Zarebczan
parent ce11a4b9c1
commit baf51f9c32
21 changed files with 432 additions and 96 deletions

View file

@ -4,6 +4,7 @@
.*/node_modules/react-plastic/.* .*/node_modules/react-plastic/.*
.*/node_modules/raf-schd/.* .*/node_modules/raf-schd/.*
.*/node_modules/react-beautiful-dnd/.* .*/node_modules/react-beautiful-dnd/.*
.*/node_modules/@emotion/.*
[include] [include]

View file

@ -49,6 +49,7 @@ type Props = {
commentsAreExpanded?: boolean, commentsAreExpanded?: boolean,
fetchReacts: (Array<string>) => Promise<any>, fetchReacts: (Array<string>) => Promise<any>,
doResolveUris: (Array<string>) => void, doResolveUris: (Array<string>) => void,
setCommentListTitle?: (string) => void,
fetchTopLevelComments: (string, number, number, number) => void, fetchTopLevelComments: (string, number, number, number) => void,
fetchComment: (string) => void, fetchComment: (string) => void,
resetComments: (string) => void, resetComments: (string) => void,
@ -77,6 +78,7 @@ function CommentList(props: Props) {
commentsAreExpanded, commentsAreExpanded,
fetchReacts, fetchReacts,
doResolveUris, doResolveUris,
setCommentListTitle,
fetchTopLevelComments, fetchTopLevelComments,
fetchComment, fetchComment,
resetComments, resetComments,
@ -84,13 +86,13 @@ function CommentList(props: Props) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isMediumScreen = useIsMediumScreen(); const isMediumScreen = useIsMediumScreen();
const desktopView = !isMobile && !isMediumScreen;
const spinnerRef = React.useRef(); const spinnerRef = React.useRef();
const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST; const DEFAULT_SORT = ENABLE_COMMENT_REACTIONS ? SORT_BY.POPULARITY : SORT_BY.NEWEST;
const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT); const [sort, setSort] = usePersistedState('comment-sort-by', DEFAULT_SORT);
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [commentsToDisplay, setCommentsToDisplay] = React.useState(topLevelComments); const [commentsToDisplay, setCommentsToDisplay] = React.useState(topLevelComments);
const hasDefaultExpansion = commentsAreExpanded || desktopView; const hasDefaultExpansion = commentsAreExpanded || !isMediumScreen || isMobile;
const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion); const [expandedComments, setExpandedComments] = React.useState(hasDefaultExpansion);
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0; const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
const channelId = getChannelIdFromClaim(claim); const channelId = getChannelIdFromClaim(claim);
@ -99,6 +101,11 @@ function CommentList(props: Props) {
const isResolvingComments = topLevelComments && resolvedComments.length !== topLevelComments.length; const isResolvingComments = topLevelComments && resolvedComments.length !== topLevelComments.length;
const alreadyResolved = !isResolvingComments && resolvedComments.length !== 0; const alreadyResolved = !isResolvingComments && resolvedComments.length !== 0;
const canDisplayComments = commentsToDisplay && commentsToDisplay.length === topLevelComments.length; const canDisplayComments = commentsToDisplay && commentsToDisplay.length === topLevelComments.length;
const title =
(totalComments === 0 && __('Leave a comment')) ||
(totalComments === 1 && __('1 comment')) ||
__('%total_comments% comments', { total_comments: totalComments });
if (setCommentListTitle) setCommentListTitle(title);
// Display comments immediately if not fetching reactions // Display comments immediately if not fetching reactions
// If not, wait to show comments until reactions are fetched // If not, wait to show comments until reactions are fetched
@ -283,11 +290,7 @@ function CommentList(props: Props) {
return ( return (
<Card <Card
className="card--enable-overflow" className="card--enable-overflow"
title={ title={!isMobile && title}
(totalComments === 0 && __('Leave a comment')) ||
(totalComments === 1 && __('1 comment')) ||
__('%total_comments% comments', { total_comments: totalComments })
}
titleActions={ titleActions={
<> <>
{totalComments > 1 && ENABLE_COMMENT_REACTIONS && ( {totalComments > 1 && ENABLE_COMMENT_REACTIONS && (
@ -310,8 +313,8 @@ function CommentList(props: Props) {
<ul <ul
className={classnames({ className={classnames({
comments: desktopView || expandedComments, comments: isMediumScreen || expandedComments,
'comments--contracted': !desktopView && !expandedComments, 'comments--contracted': !isMediumScreen && !expandedComments,
})} })}
> >
{readyToDisplayComments && pinnedComments && getCommentElems(pinnedComments)} {readyToDisplayComments && pinnedComments && getCommentElems(pinnedComments)}

View file

@ -8,10 +8,12 @@ import {
import { selectPlayingUri, makeSelectFileRenderModeForUri } from 'redux/selectors/content'; import { selectPlayingUri, makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import { selectCostInfoForUri } from 'lbryinc'; import { selectCostInfoForUri } from 'lbryinc';
import { doPlayUri } from 'redux/actions/content'; import { doPlayUri } from 'redux/actions/content';
import { doSetMobilePlayerDimensions } from 'redux/actions/app';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { getChannelIdFromClaim } from 'util/claim'; import { getChannelIdFromClaim } from 'util/claim';
import { selectActiveLivestreamForChannel } from 'redux/selectors/livestream'; import { selectActiveLivestreamForChannel } from 'redux/selectors/livestream';
import FileRenderMobile from './view'; import FileRenderMobile from './view';
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
const select = (state, props) => { const select = (state, props) => {
const playingUri = selectPlayingUri(state); const playingUri = selectPlayingUri(state);
@ -34,11 +36,14 @@ const select = (state, props) => {
activeLivestreamForChannel: channelClaimId && selectActiveLivestreamForChannel(state, channelClaimId), activeLivestreamForChannel: channelClaimId && selectActiveLivestreamForChannel(state, channelClaimId),
claimId, claimId,
channelClaimId, channelClaimId,
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
playingUri,
}; };
}; };
const perform = { const perform = {
doPlayUri, doPlayUri,
doSetMobilePlayerDimensions,
}; };
export default withRouter(connect(select, perform)(FileRenderMobile)); export default withRouter(connect(select, perform)(FileRenderMobile));

View file

@ -11,6 +11,7 @@ import LivestreamIframeRender from 'component/livestreamLayout/iframe-render';
const PRIMARY_PLAYER_WRAPPER_CLASS = 'file-page__video-container'; const PRIMARY_PLAYER_WRAPPER_CLASS = 'file-page__video-container';
export const INLINE_PLAYER_WRAPPER_CLASS = 'inline-player__wrapper'; export const INLINE_PLAYER_WRAPPER_CLASS = 'inline-player__wrapper';
export const HEADER_HEIGHT_MOBILE = 56;
// **************************************************************************** // ****************************************************************************
// **************************************************************************** // ****************************************************************************
@ -27,7 +28,10 @@ type Props = {
previousListUri: string, previousListUri: string,
activeLivestreamForChannel?: any, activeLivestreamForChannel?: any,
channelClaimId?: any, channelClaimId?: any,
playingUri?: PlayingUri,
mobilePlayerDimensions?: any,
doPlayUri: (string) => void, doPlayUri: (string) => void,
doSetMobilePlayerDimensions: (height: number, width: number) => void,
}; };
export default function FileRenderMobile(props: Props) { export default function FileRenderMobile(props: Props) {
@ -43,7 +47,10 @@ export default function FileRenderMobile(props: Props) {
previousListUri, previousListUri,
activeLivestreamForChannel, activeLivestreamForChannel,
channelClaimId, channelClaimId,
playingUri,
mobilePlayerDimensions,
doPlayUri, doPlayUri,
doSetMobilePlayerDimensions,
} = props; } = props;
const { push } = useHistory(); const { push } = useHistory();
@ -58,6 +65,7 @@ export default function FileRenderMobile(props: Props) {
const canViewFile = isFree || claimWasPurchased; const canViewFile = isFree || claimWasPurchased;
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || activeLivestreamForChannel; const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode) || activeLivestreamForChannel;
const isReadyToPlay = isPlayable && streamingUrl; const isReadyToPlay = isPlayable && streamingUrl;
const isCurrentMediaPlaying = playingUri && playingUri.uri === uri;
const handleResize = React.useCallback(() => { const handleResize = React.useCallback(() => {
const element = document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`); const element = document.querySelector(`.${PRIMARY_PLAYER_WRAPPER_CLASS}`);
@ -80,7 +88,11 @@ export default function FileRenderMobile(props: Props) {
// $FlowFixMe // $FlowFixMe
setFileViewerRect({ ...objectRect }); setFileViewerRect({ ...objectRect });
}, []);
if (doSetMobilePlayerDimensions && (!mobilePlayerDimensions || mobilePlayerDimensions.height !== rect.height)) {
doSetMobilePlayerDimensions(rect.height, rect.width);
}
}, [doSetMobilePlayerDimensions, mobilePlayerDimensions]);
// Initial resize, will place the player correctly above the cover when starts playing // Initial resize, will place the player correctly above the cover when starts playing
// (remember the URI here is from playingUri). The cover then keeps on the page and kind of serves as a placeholder // (remember the URI here is from playingUri). The cover then keeps on the page and kind of serves as a placeholder
@ -128,7 +140,13 @@ export default function FileRenderMobile(props: Props) {
setPlayNextUrl(true); setPlayNextUrl(true);
}, [doNavigate, doPlay, nextListUri, playNextUrl, previousListUri]); }, [doNavigate, doPlay, nextListUri, playNextUrl, previousListUri]);
if (!isPlayable || !uri || countdownCanceled || (collectionId && !canViewFile && !nextListUri)) { if (
!isCurrentMediaPlaying ||
!isPlayable ||
!uri ||
countdownCanceled ||
(collectionId && !canViewFile && !nextListUri)
) {
return null; return null;
} }
@ -146,32 +164,28 @@ export default function FileRenderMobile(props: Props) {
} }
> >
<div className="content__wrapper"> <div className="content__wrapper">
<React.Suspense fallback={<Loading />}> {isCurrentClaimLive && channelClaimId ? (
{isCurrentClaimLive && channelClaimId ? ( <LivestreamIframeRender channelClaimId={channelClaimId} showLivestream mobileVersion />
<LivestreamIframeRender channelClaimId={channelClaimId} showLivestream mobileVersion /> ) : isReadyToPlay ? (
) : isReadyToPlay ? ( <FileRender uri={uri} />
<FileRender uri={uri} /> ) : !canViewFile ? (
) : !canViewFile ? ( <div className="content__loading">
<div className="content__loading"> <AutoplayCountdown
<AutoplayCountdown nextRecommendedUri={nextListUri}
nextRecommendedUri={nextListUri} doNavigate={() => setDoNavigate(true)}
doNavigate={() => setDoNavigate(true)} doReplay={() => doPlayUri(uri)}
doReplay={() => doPlayUri(uri)} doPrevious={() => {
doPrevious={() => { setPlayNextUrl(false);
setPlayNextUrl(false); setDoNavigate(true);
setDoNavigate(true); }}
}} onCanceled={() => setCountdownCanceled(true)}
onCanceled={() => setCountdownCanceled(true)} skipPaid
skipPaid />
/> </div>
</div> ) : (
) : ( <LoadingScreen status={__('Loading')} />
<Loading /> )}
)}
</React.Suspense>
</div> </div>
</div> </div>
); );
} }
const Loading = () => <LoadingScreen status={__('Loading')} />;

View file

@ -41,6 +41,7 @@ type Props = {
pinnedComments: Array<Comment>, pinnedComments: Array<Comment>,
superChats: Array<Comment>, superChats: Array<Comment>,
uri: string, uri: string,
hideHeader?: boolean,
doCommentList: (string, string, number, number) => void, doCommentList: (string, string, number, number) => void,
doResolveUris: (Array<string>, boolean) => void, doResolveUris: (Array<string>, boolean) => void,
doSuperChatList: (string) => void, doSuperChatList: (string) => void,
@ -55,6 +56,7 @@ export default function LivestreamChatLayout(props: Props) {
pinnedComments, pinnedComments,
superChats: superChatsByAmount, superChats: superChatsByAmount,
uri, uri,
hideHeader,
doCommentList, doCommentList,
doResolveUris, doResolveUris,
doSuperChatList, doSuperChatList,
@ -242,60 +244,62 @@ export default function LivestreamChatLayout(props: Props) {
return ( return (
<div className={classnames('card livestream__chat', { 'livestream__chat--popout': isPopoutWindow })}> <div className={classnames('card livestream__chat', { 'livestream__chat--popout': isPopoutWindow })}>
<div className="card__header--between livestreamDiscussion__header"> {!hideHeader && (
<div className="card__title-section--small livestreamDiscussion__title"> <div className="card__header--between livestreamDiscussion__header">
{__('Live Chat')} <div className="card__title-section--small livestreamDiscussion__title">
{__('Live Chat')}
<Menu> <Menu>
<MenuButton className="menu__button"> <MenuButton className="menu__button">
<Icon size={18} icon={ICONS.SETTINGS} /> <Icon size={18} icon={ICONS.SETTINGS} />
</MenuButton> </MenuButton>
<MenuList className="menu__list"> <MenuList className="menu__list">
<MenuItem className="comment__menu-option" onSelect={TOGGLE_TIMESTAMP_OPACITY}> <MenuItem className="comment__menu-option" onSelect={TOGGLE_TIMESTAMP_OPACITY}>
<span className="menu__link"> <span className="menu__link">
<Icon aria-hidden icon={ICONS.TIME} /> <Icon aria-hidden icon={ICONS.TIME} />
{__('Toggle Timestamps')} {__('Toggle Timestamps')}
</span> </span>
</MenuItem> </MenuItem>
<MenuItem className="comment__menu-option" onSelect={() => setChatHidden(true)}> <MenuItem className="comment__menu-option" onSelect={() => setChatHidden(true)}>
<span className="menu__link"> <span className="menu__link">
<Icon aria-hidden icon={ICONS.EYE} /> <Icon aria-hidden icon={ICONS.EYE} />
{__('Hide Chat')} {__('Hide Chat')}
</span> </span>
</MenuItem> </MenuItem>
{!isPopoutWindow && !isMobile && ( {!isPopoutWindow && !isMobile && (
<>
<MenuItem className="comment__menu-option" onSelect={handlePopout}>
<span className="menu__link">
<Icon aria-hidden icon={ICONS.EXTERNAL} />
{__('Popout Chat')}
</span>
</MenuItem>
</>
)}
</MenuList>
</Menu>
</div>
{superChatsByAmount && (
<div className="recommended-content__toggles">
{/* the superchats in chronological order button */}
{chatContentToggle(VIEW_MODES.CHAT, __('Chat'))}
{/* the button to show superchats listed by most to least support amount */}
{chatContentToggle(
VIEW_MODES.SUPERCHAT,
<> <>
<MenuItem className="comment__menu-option" onSelect={handlePopout}> <CreditAmount amount={superChatsLBCAmount || 0} size={8} /> /
<span className="menu__link"> <CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
<Icon aria-hidden icon={ICONS.EXTERNAL} />
{__('Popout Chat')}
</span>
</MenuItem>
</> </>
)} )}
</MenuList> </div>
</Menu> )}
</div> </div>
)}
{superChatsByAmount && (
<div className="recommended-content__toggles">
{/* the superchats in chronological order button */}
{chatContentToggle(VIEW_MODES.CHAT, __('Chat'))}
{/* the button to show superchats listed by most to least support amount */}
{chatContentToggle(
VIEW_MODES.SUPERCHAT,
<>
<CreditAmount amount={superChatsLBCAmount || 0} size={8} /> /
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
</>
)}
</div>
)}
</div>
<div ref={commentsRef} className="livestreamComments__wrapper"> <div ref={commentsRef} className="livestreamComments__wrapper">
{viewMode === VIEW_MODES.CHAT && superChatsByAmount && ( {viewMode === VIEW_MODES.CHAT && superChatsByAmount && (

View file

@ -45,9 +45,12 @@ export default function LivestreamComments(props: Props) {
if (!fetchingComments && commentsToDisplay && commentsToDisplay.length > 0) { if (!fetchingComments && commentsToDisplay && commentsToDisplay.length > 0) {
return ( return (
<div className="livestream__comments"> <div className="livestream__comments">
{commentsToDisplay.map((comment) => ( {commentsToDisplay
<LivestreamComment comment={comment} key={comment.comment_id} uri={uri} forceUpdate={forceUpdate} /> .slice(0)
))} .reverse()
.map((comment) => (
<LivestreamComment comment={comment} key={comment.comment_id} uri={uri} forceUpdate={forceUpdate} />
))}
</div> </div>
); );
} }

View file

@ -7,6 +7,9 @@ import React from 'react';
import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view'; import { PRIMARY_PLAYER_WRAPPER_CLASS } from 'page/file/view';
import FileRenderInitiator from 'component/fileRenderInitiator'; import FileRenderInitiator from 'component/fileRenderInitiator';
import LivestreamIframeRender from './iframe-render'; import LivestreamIframeRender from './iframe-render';
import Button from 'component/button';
import * as ICONS from 'constants/icons';
import SwipeableDrawer from 'component/swipeableDrawer';
const LivestreamChatLayout = lazyImport(() => import('component/livestreamChatLayout' /* webpackChunkName: "chat" */)); const LivestreamChatLayout = lazyImport(() => import('component/livestreamChatLayout' /* webpackChunkName: "chat" */));
@ -35,6 +38,9 @@ export default function LivestreamLayout(props: Props) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [showChat, setShowChat] = React.useState(undefined);
const drawerWasToggled = showChat !== undefined;
if (!claim || !claim.signing_channel) return null; if (!claim || !claim.signing_channel) return null;
const { name: channelName, claim_id: channelClaimId } = claim.signing_channel; const { name: channelName, claim_id: channelClaimId } = claim.signing_channel;
@ -85,10 +91,27 @@ export default function LivestreamLayout(props: Props) {
{isMobile && !hideComments && ( {isMobile && !hideComments && (
<React.Suspense fallback={null}> <React.Suspense fallback={null}>
<LivestreamChatLayout uri={uri} /> <SwipeableDrawer
open={Boolean(showChat)}
toggleDrawer={() => setShowChat(!showChat)}
title={__('Live Chat')}
didInitialDisplay={drawerWasToggled}
>
<LivestreamChatLayout uri={uri} hideHeader />
</SwipeableDrawer>
</React.Suspense> </React.Suspense>
)} )}
{isMobile && (
<Button
className="swipeable-drawer__expand-button"
label="Open Live Chat"
button="primary"
icon={ICONS.CHAT}
onClick={() => setShowChat(!showChat)}
/>
)}
<FileTitleSection uri={uri} livestream isLive={showLivestream} /> <FileTitleSection uri={uri} livestream isLive={showLivestream} />
</div> </div>
</> </>

View file

@ -0,0 +1,11 @@
import { connect } from 'react-redux';
import LivestreamLayout from './view';
import { selectTheme } from 'redux/selectors/settings';
import { selectMobilePlayerDimensions } from 'redux/selectors/app';
const select = (state) => ({
theme: selectTheme(state),
mobilePlayerDimensions: selectMobilePlayerDimensions(state),
});
export default connect(select)(LivestreamLayout);

View file

@ -0,0 +1,109 @@
// @flow
// $FlowFixMe
import { Global } from '@emotion/react';
// $FlowFixMe
import { grey } from '@mui/material/colors';
// $FlowFixMe
import Typography from '@mui/material/Typography';
import { HEADER_HEIGHT_MOBILE } from 'component/fileRenderMobile/view';
import { SwipeableDrawer as MUIDrawer } from '@mui/material';
import * as ICONS from 'constants/icons';
import * as React from 'react';
import Button from 'component/button';
import Icon from 'component/common/icon';
import Portal from '@mui/material/Portal';
const DRAWER_PULLER_HEIGHT = 42;
type Props = {
children: Node,
open: Boolean,
theme: string,
mobilePlayerDimensions?: { height: number },
title: string,
didInitialDisplay?: boolean,
toggleDrawer: () => void,
};
export default function SwipeableDrawer(props: Props) {
const { mobilePlayerDimensions, title, children, open, theme, didInitialDisplay, toggleDrawer } = props;
const [coverHeight, setCoverHeight] = React.useState();
const videoHeight = coverHeight || (mobilePlayerDimensions ? mobilePlayerDimensions.height : 0);
React.useEffect(() => {
if (open && !mobilePlayerDimensions) {
const element = document.querySelector(`.file-page__video-container`);
if (element) {
const rect = element.getBoundingClientRect();
setCoverHeight(rect.height);
}
}
}, [mobilePlayerDimensions, open]);
const drawerGlobalStyles = (
<Global
styles={{
'.main-wrapper__inner--filepage': {
'padding-bottom': didInitialDisplay ? `${DRAWER_PULLER_HEIGHT}px !important` : 'inherit',
overflow: open ? 'hidden' : 'unset',
'max-height': open ? '100vh' : 'unset',
},
'.MuiDrawer-root': {
top: `calc(${HEADER_HEIGHT_MOBILE}px + ${videoHeight}px) !important`,
},
'.MuiDrawer-root > .MuiPaper-root': {
overflow: 'visible',
color: 'var(--color-text)',
position: 'absolute',
height: `calc(100% - ${DRAWER_PULLER_HEIGHT}px)`,
},
}}
/>
);
const Puller = () => (
<span className="swipeable-drawer__puller" style={{ backgroundColor: theme === 'light' ? grey[300] : grey[800] }} />
);
return (
<>
{drawerGlobalStyles}
<MUIDrawer
anchor="bottom"
open={open}
onClose={toggleDrawer}
onOpen={toggleDrawer}
hideBackdrop
disableEnforceFocus
disablePortal
disableSwipeToOpen
ModalProps={{ keepMounted: true }}
>
{didInitialDisplay && (
<div className="swipeable-drawer__header" style={{ top: -DRAWER_PULLER_HEIGHT }}>
{open ? (
<>
<Puller />
<Typography sx={{ p: 1.5, color: 'var(--color-text)', display: 'flex' }}>{title}</Typography>
<Button button="close" icon={ICONS.REMOVE} onClick={toggleDrawer} />
</>
) : (
<Portal>
<div className="swipeable-drawer__expand" onClick={toggleDrawer}>
<Typography sx={{ p: 1.5, color: 'var(--color-text)' }}>{title}</Typography>
<Icon icon={ICONS.UP} />
</div>
</Portal>
)}
</div>
)}
{children}
</MUIDrawer>
</>
);
}

View file

@ -32,6 +32,7 @@ export const TOGGLE_YOUTUBE_SYNC_INTEREST = 'TOGGLE_YOUTUBE_SYNC_INTEREST';
export const TOGGLE_SPLASH_ANIMATION = 'TOGGLE_SPLASH_ANIMATION'; export const TOGGLE_SPLASH_ANIMATION = 'TOGGLE_SPLASH_ANIMATION';
export const SET_ACTIVE_CHANNEL = 'SET_ACTIVE_CHANNEL'; export const SET_ACTIVE_CHANNEL = 'SET_ACTIVE_CHANNEL';
export const SET_INCOGNITO = 'SET_INCOGNITO'; export const SET_INCOGNITO = 'SET_INCOGNITO';
export const SET_MOBILE_PLAYER_DIMENSIONS = 'SET_MOBILE_PLAYER_DIMENSIONS';
export const RELOAD_REQUIRED = 'RELOAD_REQUIRED'; export const RELOAD_REQUIRED = 'RELOAD_REQUIRED';
// Navigation // Navigation

View file

@ -14,6 +14,7 @@ import { selectCostInfoForUri, doFetchCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent, selectClientSetting } from 'redux/selectors/settings'; import { selectShowMatureContent, selectClientSetting } from 'redux/selectors/settings';
import { makeSelectFileRenderModeForUri, makeSelectContentPositionForUri } from 'redux/selectors/content'; import { makeSelectFileRenderModeForUri, makeSelectContentPositionForUri } from 'redux/selectors/content';
import { DISABLE_COMMENTS_TAG } from 'constants/tags'; import { DISABLE_COMMENTS_TAG } from 'constants/tags';
import { doSetMobilePlayerDimensions } from 'redux/actions/app';
import FilePage from './view'; import FilePage from './view';
@ -45,6 +46,7 @@ const perform = {
doSetContentHistoryItem, doSetContentHistoryItem,
doSetPrimaryUri, doSetPrimaryUri,
clearPosition, clearPosition,
doSetMobilePlayerDimensions,
}; };
export default withRouter(connect(select, perform)(FilePage)); export default withRouter(connect(select, perform)(FilePage));

View file

@ -15,6 +15,9 @@ import CollectionContent from 'component/collectionContentSidebar';
import Button from 'component/button'; import Button from 'component/button';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import Empty from 'component/common/empty'; import Empty from 'component/common/empty';
import SwipeableDrawer from 'component/swipeableDrawer';
import * as ICONS from 'constants/icons';
import { useIsMobile } from 'effects/use-screensize';
const CommentsList = lazyImport(() => import('component/commentsList' /* webpackChunkName: "comments" */)); const CommentsList = lazyImport(() => import('component/commentsList' /* webpackChunkName: "comments" */));
const PostViewer = lazyImport(() => import('component/postViewer' /* webpackChunkName: "postViewer" */)); const PostViewer = lazyImport(() => import('component/postViewer' /* webpackChunkName: "postViewer" */));
@ -65,6 +68,12 @@ export default function FilePage(props: Props) {
clearPosition, clearPosition,
} = props; } = props;
const isMobile = useIsMobile();
const [showComments, setShowComments] = React.useState(undefined);
const [commentListTitle, setCommentListTitle] = React.useState();
const drawerWasToggled = showComments !== undefined;
const cost = costInfo ? costInfo.cost : null; const cost = costInfo ? costInfo.cost : null;
const hasFileInfo = fileInfo !== undefined; const hasFileInfo = fileInfo !== undefined;
const isMarkdown = renderMode === RENDER_MODES.MARKDOWN; const isMarkdown = renderMode === RENDER_MODES.MARKDOWN;
@ -177,6 +186,12 @@ export default function FilePage(props: Props) {
); );
} }
const commentsListElement = commentsDisabled ? (
<Empty text={__('The creator of this content has disabled comments.')} />
) : (
<CommentsList uri={uri} linkedCommentId={linkedCommentId} setCommentListTitle={setCommentListTitle} />
);
return ( return (
<Page className="file-page" filePage isMarkdown={isMarkdown}> <Page className="file-page" filePage isMarkdown={isMarkdown}>
<div className={classnames('section card-stack', `file-page__${renderMode}`)}> <div className={classnames('section card-stack', `file-page__${renderMode}`)}>
@ -200,15 +215,30 @@ export default function FilePage(props: Props) {
{RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />} {RENDER_MODES.FLOATING_MODES.includes(renderMode) && <FileTitleSection uri={uri} />}
{commentsDisabled ? ( {isMobile ? (
<Empty text={__('The creator of this content has disabled comments.')} /> <SwipeableDrawer
open={Boolean(showComments)}
toggleDrawer={() => setShowComments(!showComments)}
title={commentListTitle}
didInitialDisplay={drawerWasToggled}
>
{commentsListElement}
</SwipeableDrawer>
) : ( ) : (
<React.Suspense fallback={null}> commentsListElement
<CommentsList uri={uri} linkedCommentId={linkedCommentId} />
</React.Suspense>
)} )}
</> </>
{isMobile && (
<Button
className="swipeable-drawer__expand-button"
label={commentListTitle}
button="primary"
icon={ICONS.CHAT}
onClick={() => setShowComments(!showComments)}
/>
)}
{!isMarkdown && videoTheaterMode && <RightSideContent {...rightSideProps} />} {!isMarkdown && videoTheaterMode && <RightSideContent {...rightSideProps} />}
</div> </div>
)} )}

View file

@ -736,3 +736,8 @@ export function doSetIncognito(incognitoEnabled) {
}, },
}; };
} }
export const doSetMobilePlayerDimensions = (height, width) => ({
type: ACTIONS.SET_MOBILE_PLAYER_DIMENSIONS,
data: { heightWidth: { height, width } },
});

View file

@ -324,6 +324,13 @@ reducers[ACTIONS.SET_INCOGNITO] = (state, action) => {
}; };
}; };
reducers[ACTIONS.SET_MOBILE_PLAYER_DIMENSIONS] = (state, action) => {
return {
...state,
mobilePlayerDimensions: action.data.heightWidth,
};
};
reducers[ACTIONS.USER_STATE_POPULATE] = (state, action) => { reducers[ACTIONS.USER_STATE_POPULATE] = (state, action) => {
const { welcomeVersion, allowAnalytics } = action.data; const { welcomeVersion, allowAnalytics } = action.data;
return { return {

View file

@ -106,3 +106,5 @@ export const selectActiveChannelStakedLevel = (state) => {
}; };
export const selectIncognito = (state) => selectState(state).incognito; export const selectIncognito = (state) => selectState(state).incognito;
export const selectMobilePlayerDimensions = (state) => selectState(state).mobilePlayerDimensions;

View file

@ -68,4 +68,5 @@
@import 'component/stripe-card'; @import 'component/stripe-card';
@import 'component/wallet-tip-send'; @import 'component/wallet-tip-send';
@import 'component/swipe-list'; @import 'component/swipe-list';
@import 'component/swipeable-drawer';
@import 'component/utils'; @import 'component/utils';

View file

@ -13,6 +13,15 @@
.card--enable-overflow { .card--enable-overflow {
overflow: visible; overflow: visible;
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
@media (max-width: $breakpoint-small) {
overflow-y: scroll;
height: 100%;
.card__main-actions {
margin-top: 0px !important;
}
}
} }
.card--disabled { .card--disabled {

View file

@ -27,6 +27,8 @@ $recent-msg-button__height: 2rem;
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
margin: 0 !important; margin: 0 !important;
height: 100%;
margin-bottom: 0px !important;
} }
} }
@ -97,7 +99,6 @@ $recent-msg-button__height: 2rem;
.livestreamComments__wrapper { .livestreamComments__wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100vh - var(--header-height) - #{$discussion-header__height});
.main--empty { .main--empty {
.yrbl__wrap { .yrbl__wrap {
@ -109,11 +110,20 @@ $recent-msg-button__height: 2rem;
} }
} }
} }
@media (min-width: $breakpoint-small) {
height: calc(100% - var(--header-height) - #{$discussion-header__height});
}
@media (max-width: $breakpoint-small) {
height: 100%;
flex-direction: column;
}
} }
.livestream__comments { .livestream__comments {
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column;
font-size: var(--font-small); font-size: var(--font-small);
overflow-y: scroll; overflow-y: scroll;
overflow-x: visible; overflow-x: visible;
@ -138,6 +148,10 @@ $recent-msg-button__height: 2rem;
padding: var(--spacing-s); padding: var(--spacing-s);
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
margin-top: auto; margin-top: auto;
@media (max-width: $breakpoint-small) {
padding: var(--spacing-xxs);
}
} }
.livestreamSuperchats__wrapper { .livestreamSuperchats__wrapper {
@ -153,6 +167,23 @@ $recent-msg-button__height: 2rem;
padding: var(--spacing-xs); padding: var(--spacing-xs);
width: var(--livestream-comments-width); width: var(--livestream-comments-width);
} }
@media (max-width: $breakpoint-small) {
position: absolute;
z-index: 1300;
width: 100%;
background-color: transparent;
padding: 0px;
padding-left: var(--spacing-xxs);
padding-top: var(--spacing-xxs);
border-bottom: none;
scrollbar-width: 0px;
&::-webkit-scrollbar {
width: 0px;
height: 0px;
}
}
} }
.livestreamPinned__wrapper { .livestreamPinned__wrapper {
@ -209,6 +240,17 @@ $recent-msg-button__height: 2rem;
.channel-thumbnail { .channel-thumbnail {
margin-right: var(--spacing-xs); margin-right: var(--spacing-xs);
@include handleChannelGif(2rem); @include handleChannelGif(2rem);
@media (max-width: $breakpoint-small) {
margin-right: var(--spacing-xxs);
@include handleChannelGif(1.5rem);
.channel-staked__wrapper {
padding: 0px;
left: -0.4rem;
bottom: -0.5rem;
}
}
} }
&:first-of-type { &:first-of-type {
@ -236,6 +278,15 @@ $recent-msg-button__height: 2rem;
.channel-name { .channel-name {
max-width: 5rem; max-width: 5rem;
} }
@media (max-width: $breakpoint-small) {
padding: 5px;
padding-bottom: 2px;
span {
font-size: var(--font-xxsmall);
}
}
} }
.livestreamSuperchat__info { .livestreamSuperchat__info {
@ -247,6 +298,10 @@ $recent-msg-button__height: 2rem;
.button { .button {
margin-top: calc(var(--spacing-xxs) / 2); margin-top: calc(var(--spacing-xxs) / 2);
} }
@media (max-width: $breakpoint-small) {
max-height: 40px;
}
} }
.livestreamSuperchat__info--sticker { .livestreamSuperchat__info--sticker {

View file

@ -36,6 +36,11 @@
.main-wrapper__inner--filepage { .main-wrapper__inner--filepage {
padding: 0; padding: 0;
@media (max-width: $breakpoint-small) {
margin-top: 0px;
padding-top: var(--header-height-mobile);
}
} }
.main-wrapper__inner--theater-mode { .main-wrapper__inner--theater-mode {
@ -227,9 +232,13 @@
.card { .card {
border-radius: 0; border-radius: 0;
margin-bottom: var(--spacing-xxs) !important; margin-bottom: 0px !important;
padding: 0; padding: 0;
} }
.file-page__recommended {
margin-top: var(--spacing-xxs) !important;
}
} }
} }
@ -284,13 +293,13 @@
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-m);
@media (min-width: $breakpoint-large + 300px) { @media (min-width: ($breakpoint-large + 300px)) {
max-width: calc(var(--page-max-width--filepage) / 1.25); max-width: calc(var(--page-max-width--filepage) / 1.25);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
@media (min-width: $breakpoint-medium) and (max-width: $breakpoint-large + 300px) { @media (min-width: $breakpoint-medium) and (max-width: ($breakpoint-large + 300px)) {
max-width: calc(100vw - var(--livestream-comments-width) - var(--spacing-m) * 3); max-width: calc(100vw - var(--livestream-comments-width) - var(--spacing-m) * 3);
margin-left: var(--spacing-m); margin-left: var(--spacing-m);
margin-right: var(--spacing-m); margin-right: var(--spacing-m);

View file

@ -68,6 +68,8 @@
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
font-size: var(--font-xsmall); font-size: var(--font-xsmall);
word-wrap: break-word;
overflow: hidden;
a { a {
.button__content { .button__content {

View file

@ -0,0 +1,40 @@
.swipeable-drawer__header {
position: absolute;
visibility: visible;
right: 0;
left: 0;
background-color: var(--color-card-background);
.button--close {
top: 2px !important;
right: 2px !important;
}
}
.swipeable-drawer__puller {
width: 30px;
height: 6px;
border-radius: 3px;
position: absolute;
top: 8px;
left: calc(50% - 15px);
}
.swipeable-drawer__expand-button {
width: 100%;
margin: var(--spacing-xxs) 0;
border-radius: 0;
}
.swipeable-drawer__expand {
border-top: 1px solid var(--color-border);
position: fixed;
background-color: var(--color-card-background);
visibility: visible;
right: 0;
left: 0;
bottom: 0;
display: flex;
flex-direction: row;
align-items: center;
}