From 25119561fbe14261e69cfdc4d31554e8473fb678 Mon Sep 17 00:00:00 2001 From: Michael David Kuckuk <8076094+LBBO@users.noreply.github.com> Date: Thu, 9 Feb 2023 03:50:58 +0100 Subject: [PATCH] Give chat a min-height that other elements yield to on mobile clients (#2676) * Add className prop to some components * Give mobile chatbox height priority over other elements * Optimize for mobile landscape mode * Make thumbnail background black * Fix overflow issues on narrow screens * Adjust layout for offline mode on mobile * Fix main content width on Desktop * Fix offline layout for desktop --- web/components/ui/Content/Content.module.scss | 50 +++-- web/components/ui/Content/Content.tsx | 194 +++++++++--------- .../ui/CrossfadeImage/CrossfadeImage.tsx | 4 +- web/components/ui/Header/Header.module.scss | 4 +- .../ui/OfflineBanner/OfflineBanner.tsx | 5 +- web/components/ui/Statusbar/Statusbar.tsx | 5 +- .../OwncastPlayer/OwncastPlayer.module.scss | 2 +- .../video/OwncastPlayer/OwncastPlayer.tsx | 5 +- web/components/video/VideoJS/VideoJS.scss | 19 +- .../video/VideoPoster/VideoPoster.module.scss | 5 +- .../video/VideoPoster/VideoPoster.tsx | 1 + 11 files changed, 176 insertions(+), 118 deletions(-) diff --git a/web/components/ui/Content/Content.module.scss b/web/components/ui/Content/Content.module.scss index 54b8b7c52..3a4eed937 100644 --- a/web/components/ui/Content/Content.module.scss +++ b/web/components/ui/Content/Content.module.scss @@ -3,19 +3,46 @@ .root { display: grid; grid-template-columns: 1fr auto; + grid-template-rows: 100%; width: 100%; background-color: var(--theme-color-background-main); + height: 100%; + min-height: 0; @include screen(desktop) { height: var(--content-height); } .mainSection { - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: min-content // Skeleton when app is loading + minmax(30px, min-content) // player + min-content // status bar when live + min-content // mid section + minmax(250px, 1fr) // mobile content +; + grid-template-columns: 100%; + + &.offline { + grid-template-rows: min-content // Skeleton when app is loading + min-content // offline banner + min-content // status bar when live + min-content // mid section + minmax(250px, 1fr) // mobile content +; + } + + @include screen(tablet) { + grid-template-columns: 100vw; + } @include screen(desktop) { overflow-y: scroll; + grid-template-rows: unset; + + &.offline { + grid-template-rows: unset; + } } } @@ -27,10 +54,6 @@ display: none; } - .topSection { - padding: 0; - background-color: var(--theme-color-components-video-background); - } .lowerSection { padding: 0em 2%; margin-bottom: 2em; @@ -44,6 +67,14 @@ } } +.topSectionElement { + background-color: var(--theme-color-components-video-background); +} + +.statusBar { + flex-shrink: 0; +} + .leftCol { display: flex; flex-direction: column; @@ -53,13 +84,6 @@ display: grid; } -.main { - display: grid; - flex: 1; - height: 100%; - grid-template-rows: 1fr auto; -} - .replacementBar { display: flex; justify-content: space-between; diff --git a/web/components/ui/Content/Content.tsx b/web/components/ui/Content/Content.tsx index b6e797ac1..bcb142f34 100644 --- a/web/components/ui/Content/Content.tsx +++ b/web/components/ui/Content/Content.tsx @@ -2,6 +2,7 @@ import { useRecoilState, useRecoilValue } from 'recoil'; import { Skeleton } from 'antd'; import { FC, useEffect, useState } from 'react'; import dynamic from 'next/dynamic'; +import classnames from 'classnames'; import { LOCAL_STORAGE_KEYS, getLocalStorage, setLocalStorage } from '../../../utils/localStorage'; import isPushNotificationSupported from '../../../utils/browserPushNotifications'; @@ -331,105 +332,110 @@ export const Content: FC = () => { return ( <> -
-
-
-
- {appState.appLoading && } - {online && ( - - )} - {!online && !appState.appLoading && ( -
- setShowNotifyModal(true)} - onFollowClick={() => setShowFollowModal(true)} - /> -
- )} - {isStreamLive && ( - - )} +
+
+ {appState.appLoading ? ( + + ) : ( +
+ )} + {online && ( + + )} + {!online && !appState.appLoading && ( +
+ setShowNotifyModal(true)} + onFollowClick={() => setShowFollowModal(true)} + className={styles.topSectionElement} + />
-
-
- {!isMobile && ( - - {externalActionButtons} - {supportFediverseFeatures && ( - setShowFollowModal(true)} /> - )} - {supportsBrowserNotifications && ( - setShowNotifyModal(true)} - notificationClosed={() => disableNotifyReminderPopup()} - > - setShowNotifyModal(true)} /> - - )} - - )} + )} + {isStreamLive ? ( + + ) : ( +
+ )} +
+
+ {!isMobile && ( + + {externalActionButtons} + {supportFediverseFeatures && ( + setShowFollowModal(true)} /> + )} + {supportsBrowserNotifications && ( + setShowNotifyModal(true)} + notificationClosed={() => disableNotifyReminderPopup()} + > + setShowNotifyModal(true)} /> + + )} + + )} - disableNotifyReminderPopup()} - handleCancel={() => disableNotifyReminderPopup()} - > - - -
+ disableNotifyReminderPopup()} + handleCancel={() => disableNotifyReminderPopup()} + > + +
- {isMobile ? ( - - ) : ( - - )} - {!isMobile &&
}
- {showChat && !isMobile && } + {isMobile ? ( + + ) : ( + + )} + {!isMobile &&
}
+ {showChat && !isMobile && }
{externalActionToDisplay && ( = ({ height, objectFit = 'fill', duration = '1s', + className, }) => { const spanStyle: React.CSSProperties = useMemo( () => ({ @@ -52,7 +54,7 @@ export const CrossfadeImage: FC = ({ }; return ( - + {[...srcs, nextSrc].map( (singleSrc, index) => singleSrc !== '' && ( diff --git a/web/components/ui/Header/Header.module.scss b/web/components/ui/Header/Header.module.scss index ff96d9b05..52f2df869 100644 --- a/web/components/ui/Header/Header.module.scss +++ b/web/components/ui/Header/Header.module.scss @@ -38,7 +38,9 @@ font-weight: 600; white-space: nowrap; text-overflow: ellipsis; - width: 70vw; + // 6rem is an overapproximation of the width of + // the user menu + max-width: min(70vw, calc(100vw - 6rem)); overflow: hidden; line-height: 1.4; } diff --git a/web/components/ui/OfflineBanner/OfflineBanner.tsx b/web/components/ui/OfflineBanner/OfflineBanner.tsx index 5fd17e602..c27dd5292 100644 --- a/web/components/ui/OfflineBanner/OfflineBanner.tsx +++ b/web/components/ui/OfflineBanner/OfflineBanner.tsx @@ -3,6 +3,7 @@ import { Divider } from 'antd'; import { FC } from 'react'; import formatDistanceToNow from 'date-fns/formatDistanceToNow'; import dynamic from 'next/dynamic'; +import classNames from 'classnames'; import styles from './OfflineBanner.module.scss'; // Lazy loaded components @@ -20,6 +21,7 @@ export type OfflineBannerProps = { showsHeader?: boolean; onNotifyClick?: () => void; onFollowClick?: () => void; + className?: string; }; export const OfflineBanner: FC = ({ @@ -31,6 +33,7 @@ export const OfflineBanner: FC = ({ showsHeader = true, onNotifyClick, onFollowClick, + className, }) => { let text; if (customText) { @@ -74,7 +77,7 @@ export const OfflineBanner: FC = ({ } return ( -
+
{showsHeader && ( <> diff --git a/web/components/ui/Statusbar/Statusbar.tsx b/web/components/ui/Statusbar/Statusbar.tsx index 2ca8b6887..c4f8ae47f 100644 --- a/web/components/ui/Statusbar/Statusbar.tsx +++ b/web/components/ui/Statusbar/Statusbar.tsx @@ -2,6 +2,7 @@ import formatDistanceToNow from 'date-fns/formatDistanceToNow'; import intervalToDuration from 'date-fns/intervalToDuration'; import { FC, useEffect, useState } from 'react'; import dynamic from 'next/dynamic'; +import classNames from 'classnames'; import styles from './Statusbar.module.scss'; import { pluralize } from '../../../utils/helpers'; @@ -16,6 +17,7 @@ export type StatusbarProps = { lastConnectTime?: Date; lastDisconnectTime?: Date; viewerCount: number; + className?: string; }; function makeDurationString(lastConnectTime: Date): string { @@ -43,6 +45,7 @@ export const Statusbar: FC = ({ lastConnectTime, lastDisconnectTime, viewerCount, + className, }) => { const [, setNow] = useState(new Date()); @@ -75,7 +78,7 @@ export const Statusbar: FC = ({ } return ( -
+
{onlineMessage}
{rightSideMessage}
diff --git a/web/components/video/OwncastPlayer/OwncastPlayer.module.scss b/web/components/video/OwncastPlayer/OwncastPlayer.module.scss index 0e3cd3fc5..65e361b18 100644 --- a/web/components/video/OwncastPlayer/OwncastPlayer.module.scss +++ b/web/components/video/OwncastPlayer/OwncastPlayer.module.scss @@ -8,7 +8,7 @@ aspect-ratio: 16 / 9; @media (max-width: 1200px) { - height: unset; + height: 100%; max-height: 75vh; } diff --git a/web/components/video/OwncastPlayer/OwncastPlayer.tsx b/web/components/video/OwncastPlayer/OwncastPlayer.tsx index e806f14b9..bd2b88a1f 100644 --- a/web/components/video/OwncastPlayer/OwncastPlayer.tsx +++ b/web/components/video/OwncastPlayer/OwncastPlayer.tsx @@ -2,6 +2,7 @@ import React, { FC, useEffect } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { useHotkeys } from 'react-hotkeys-hook'; import { VideoJsPlayerOptions } from 'video.js'; +import classNames from 'classnames'; import { VideoJS } from '../VideoJS/VideoJS'; import ViewerPing from '../viewer-ping'; import { VideoPoster } from '../VideoPoster/VideoPoster'; @@ -26,6 +27,7 @@ export type OwncastPlayerProps = { online: boolean; initiallyMuted?: boolean; title: string; + className?: string; }; async function getVideoSettings() { @@ -45,6 +47,7 @@ export const OwncastPlayer: FC = ({ online, initiallyMuted = false, title, + className, }) => { const playerRef = React.useRef(null); const [videoPlaying, setVideoPlaying] = useRecoilState(isVideoPlayingAtom); @@ -308,7 +311,7 @@ export const OwncastPlayer: FC = ({ ); return ( -
+
{online && (
diff --git a/web/components/video/VideoJS/VideoJS.scss b/web/components/video/VideoJS/VideoJS.scss index 58556d528..06fa122cb 100644 --- a/web/components/video/VideoJS/VideoJS.scss +++ b/web/components/video/VideoJS/VideoJS.scss @@ -11,7 +11,18 @@ .vjs-big-play-button { z-index: 10; color: var(--theme-color-action); - font-size: 8rem !important; + + // Setting the font size resizes the video.js + // BigPlayButton due to its style definitions + // (see https://github.com/videojs/video.js/blob/b306ce614e70e6d3305348d1b69e1434031d73ef/src/css/components/_big-play.scss) + // 30vmin determined by trial & error to not cause + // overflow with weird (small) x or y dimensions. + // min and max are also arbitrary; max was the old + // constant value. feel free to change if necessary, + // but check short and narrow screen sizes for overflow + // issues. + font-size: clamp(1rem, 30vmin, 8rem) !important; + border-color: transparent !important; border-radius: var(--theme-rounded-corners) !important; background-color: transparent !important; @@ -58,10 +69,10 @@ font-family: VideoJS, serif; font-weight: 400; font-style: normal; - } - .vjs-icon-placeholder::before { - content: '\f110'; + &::before { + content: '\f110'; + } } } diff --git a/web/components/video/VideoPoster/VideoPoster.module.scss b/web/components/video/VideoPoster/VideoPoster.module.scss index 890bcf064..66b303a5b 100644 --- a/web/components/video/VideoPoster/VideoPoster.module.scss +++ b/web/components/video/VideoPoster/VideoPoster.module.scss @@ -1,7 +1,10 @@ .poster { - background-color: black; display: flex; justify-content: center; width: 100%; height: 100%; } + +.image { + background-color: black; +} diff --git a/web/components/video/VideoPoster/VideoPoster.tsx b/web/components/video/VideoPoster/VideoPoster.tsx index 99f3a23bc..9653472f5 100644 --- a/web/components/video/VideoPoster/VideoPoster.tsx +++ b/web/components/video/VideoPoster/VideoPoster.tsx @@ -36,6 +36,7 @@ export const VideoPoster: FC = ({ online, initialSrc, src: bas objectFit="contain" height="auto" width="100%" + className={styles.image} /> )}