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
This commit is contained in:
Michael David Kuckuk 2023-02-09 03:50:58 +01:00 committed by GitHub
parent c9773091a2
commit 25119561fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 118 deletions

View file

@ -3,19 +3,46 @@
.root { .root {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
grid-template-rows: 100%;
width: 100%; width: 100%;
background-color: var(--theme-color-background-main); background-color: var(--theme-color-background-main);
height: 100%;
min-height: 0;
@include screen(desktop) { @include screen(desktop) {
height: var(--content-height); height: var(--content-height);
} }
.mainSection { .mainSection {
display: flex; display: grid;
flex-direction: column; 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) { @include screen(desktop) {
overflow-y: scroll; overflow-y: scroll;
grid-template-rows: unset;
&.offline {
grid-template-rows: unset;
}
} }
} }
@ -27,10 +54,6 @@
display: none; display: none;
} }
.topSection {
padding: 0;
background-color: var(--theme-color-components-video-background);
}
.lowerSection { .lowerSection {
padding: 0em 2%; padding: 0em 2%;
margin-bottom: 2em; margin-bottom: 2em;
@ -44,6 +67,14 @@
} }
} }
.topSectionElement {
background-color: var(--theme-color-components-video-background);
}
.statusBar {
flex-shrink: 0;
}
.leftCol { .leftCol {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -53,13 +84,6 @@
display: grid; display: grid;
} }
.main {
display: grid;
flex: 1;
height: 100%;
grid-template-rows: 1fr auto;
}
.replacementBar { .replacementBar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View file

@ -2,6 +2,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { Skeleton } from 'antd'; import { Skeleton } from 'antd';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import classnames from 'classnames';
import { LOCAL_STORAGE_KEYS, getLocalStorage, setLocalStorage } from '../../../utils/localStorage'; import { LOCAL_STORAGE_KEYS, getLocalStorage, setLocalStorage } from '../../../utils/localStorage';
import isPushNotificationSupported from '../../../utils/browserPushNotifications'; import isPushNotificationSupported from '../../../utils/browserPushNotifications';
@ -331,105 +332,110 @@ export const Content: FC = () => {
return ( return (
<> <>
<div className={styles.main}> <div className={styles.root}>
<div className={styles.root}> <div className={classnames(styles.mainSection, { [styles.offline]: !online })}>
<div className={styles.mainSection}> {appState.appLoading ? (
<div className={styles.topSection}> <Skeleton loading active paragraph={{ rows: 7 }} className={styles.topSectionElement} />
{appState.appLoading && <Skeleton loading active paragraph={{ rows: 7 }} />} ) : (
{online && ( <div className="skeleton-placeholder" />
<OwncastPlayer )}
source="/hls/stream.m3u8" {online && (
online={online} <OwncastPlayer
title={streamTitle || name} source="/hls/stream.m3u8"
/> online={online}
)} title={streamTitle || name}
{!online && !appState.appLoading && ( className={styles.topSectionElement}
<div id="offline-message"> />
<OfflineBanner )}
showsHeader={false} {!online && !appState.appLoading && (
streamName={name} <div id="offline-message">
customText={offlineMessage} <OfflineBanner
notificationsEnabled={browserNotificationsEnabled} showsHeader={false}
fediverseAccount={fediverseAccount} streamName={name}
lastLive={lastDisconnectTime} customText={offlineMessage}
onNotifyClick={() => setShowNotifyModal(true)} notificationsEnabled={browserNotificationsEnabled}
onFollowClick={() => setShowFollowModal(true)} fediverseAccount={fediverseAccount}
/> lastLive={lastDisconnectTime}
</div> onNotifyClick={() => setShowNotifyModal(true)}
)} onFollowClick={() => setShowFollowModal(true)}
{isStreamLive && ( className={styles.topSectionElement}
<Statusbar />
online={online}
lastConnectTime={lastConnectTime}
lastDisconnectTime={lastDisconnectTime}
viewerCount={viewerCount}
/>
)}
</div> </div>
<div className={styles.midSection}> )}
<div className={styles.buttonsLogoTitleSection}> {isStreamLive ? (
{!isMobile && ( <Statusbar
<ActionButtonRow> online={online}
{externalActionButtons} lastConnectTime={lastConnectTime}
{supportFediverseFeatures && ( lastDisconnectTime={lastDisconnectTime}
<FollowButton size="small" onClick={() => setShowFollowModal(true)} /> viewerCount={viewerCount}
)} className={classnames(styles.topSectionElement, styles.statusBar)}
{supportsBrowserNotifications && ( />
<NotifyReminderPopup ) : (
open={showNotifyReminder} <div className="statusbar-placeholder" />
notificationClicked={() => setShowNotifyModal(true)} )}
notificationClosed={() => disableNotifyReminderPopup()} <div className={styles.midSection}>
> <div className={styles.buttonsLogoTitleSection}>
<NotifyButton onClick={() => setShowNotifyModal(true)} /> {!isMobile && (
</NotifyReminderPopup> <ActionButtonRow>
)} {externalActionButtons}
</ActionButtonRow> {supportFediverseFeatures && (
)} <FollowButton size="small" onClick={() => setShowFollowModal(true)} />
)}
{supportsBrowserNotifications && (
<NotifyReminderPopup
open={showNotifyReminder}
notificationClicked={() => setShowNotifyModal(true)}
notificationClosed={() => disableNotifyReminderPopup()}
>
<NotifyButton onClick={() => setShowNotifyModal(true)} />
</NotifyReminderPopup>
)}
</ActionButtonRow>
)}
<Modal <Modal
title="Browser Notifications" title="Browser Notifications"
open={showNotifyModal} open={showNotifyModal}
afterClose={() => disableNotifyReminderPopup()} afterClose={() => disableNotifyReminderPopup()}
handleCancel={() => disableNotifyReminderPopup()} handleCancel={() => disableNotifyReminderPopup()}
> >
<BrowserNotifyModal /> <BrowserNotifyModal />
</Modal> </Modal>
</div>
</div> </div>
{isMobile ? (
<MobileContent
name={name}
streamTitle={streamTitle}
summary={summary}
tags={tags}
socialHandles={socialHandles}
extraPageContent={extraPageContent}
messages={messages}
currentUser={currentUser}
showChat={showChat}
actions={externalActions}
setExternalActionToDisplay={externalActionSelected}
setShowNotifyPopup={setShowNotifyModal}
setShowFollowModal={setShowFollowModal}
supportFediverseFeatures={supportFediverseFeatures}
supportsBrowserNotifications={supportsBrowserNotifications}
/>
) : (
<DesktopContent
name={name}
streamTitle={streamTitle}
summary={summary}
tags={tags}
socialHandles={socialHandles}
extraPageContent={extraPageContent}
setShowFollowModal={setShowFollowModal}
supportFediverseFeatures={supportFediverseFeatures}
/>
)}
{!isMobile && <Footer version={version} />}
</div> </div>
{showChat && !isMobile && <Sidebar />} {isMobile ? (
<MobileContent
name={name}
streamTitle={streamTitle}
summary={summary}
tags={tags}
socialHandles={socialHandles}
extraPageContent={extraPageContent}
messages={messages}
currentUser={currentUser}
showChat={showChat}
actions={externalActions}
setExternalActionToDisplay={externalActionSelected}
setShowNotifyPopup={setShowNotifyModal}
setShowFollowModal={setShowFollowModal}
supportFediverseFeatures={supportFediverseFeatures}
supportsBrowserNotifications={supportsBrowserNotifications}
/>
) : (
<DesktopContent
name={name}
streamTitle={streamTitle}
summary={summary}
tags={tags}
socialHandles={socialHandles}
extraPageContent={extraPageContent}
setShowFollowModal={setShowFollowModal}
supportFediverseFeatures={supportFediverseFeatures}
/>
)}
{!isMobile && <Footer version={version} />}
</div> </div>
{showChat && !isMobile && <Sidebar />}
</div> </div>
{externalActionToDisplay && ( {externalActionToDisplay && (
<ExternalModal <ExternalModal

View file

@ -8,6 +8,7 @@ export type CrossfadeImageProps = {
height: string; height: string;
objectFit?: ObjectFit; objectFit?: ObjectFit;
duration?: string; duration?: string;
className?: string;
}; };
const imgStyle: React.CSSProperties = { const imgStyle: React.CSSProperties = {
@ -22,6 +23,7 @@ export const CrossfadeImage: FC<CrossfadeImageProps> = ({
height, height,
objectFit = 'fill', objectFit = 'fill',
duration = '1s', duration = '1s',
className,
}) => { }) => {
const spanStyle: React.CSSProperties = useMemo( const spanStyle: React.CSSProperties = useMemo(
() => ({ () => ({
@ -52,7 +54,7 @@ export const CrossfadeImage: FC<CrossfadeImageProps> = ({
}; };
return ( return (
<span style={spanStyle}> <span style={spanStyle} className={className}>
{[...srcs, nextSrc].map( {[...srcs, nextSrc].map(
(singleSrc, index) => (singleSrc, index) =>
singleSrc !== '' && ( singleSrc !== '' && (

View file

@ -38,7 +38,9 @@
font-weight: 600; font-weight: 600;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; 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; overflow: hidden;
line-height: 1.4; line-height: 1.4;
} }

View file

@ -3,6 +3,7 @@ import { Divider } from 'antd';
import { FC } from 'react'; import { FC } from 'react';
import formatDistanceToNow from 'date-fns/formatDistanceToNow'; import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import classNames from 'classnames';
import styles from './OfflineBanner.module.scss'; import styles from './OfflineBanner.module.scss';
// Lazy loaded components // Lazy loaded components
@ -20,6 +21,7 @@ export type OfflineBannerProps = {
showsHeader?: boolean; showsHeader?: boolean;
onNotifyClick?: () => void; onNotifyClick?: () => void;
onFollowClick?: () => void; onFollowClick?: () => void;
className?: string;
}; };
export const OfflineBanner: FC<OfflineBannerProps> = ({ export const OfflineBanner: FC<OfflineBannerProps> = ({
@ -31,6 +33,7 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
showsHeader = true, showsHeader = true,
onNotifyClick, onNotifyClick,
onFollowClick, onFollowClick,
className,
}) => { }) => {
let text; let text;
if (customText) { if (customText) {
@ -74,7 +77,7 @@ export const OfflineBanner: FC<OfflineBannerProps> = ({
} }
return ( return (
<div id="offline-banner" className={styles.outerContainer}> <div id="offline-banner" className={classNames(styles.outerContainer, className)}>
<div className={styles.innerContainer}> <div className={styles.innerContainer}>
{showsHeader && ( {showsHeader && (
<> <>

View file

@ -2,6 +2,7 @@ import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import intervalToDuration from 'date-fns/intervalToDuration'; import intervalToDuration from 'date-fns/intervalToDuration';
import { FC, useEffect, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import classNames from 'classnames';
import styles from './Statusbar.module.scss'; import styles from './Statusbar.module.scss';
import { pluralize } from '../../../utils/helpers'; import { pluralize } from '../../../utils/helpers';
@ -16,6 +17,7 @@ export type StatusbarProps = {
lastConnectTime?: Date; lastConnectTime?: Date;
lastDisconnectTime?: Date; lastDisconnectTime?: Date;
viewerCount: number; viewerCount: number;
className?: string;
}; };
function makeDurationString(lastConnectTime: Date): string { function makeDurationString(lastConnectTime: Date): string {
@ -43,6 +45,7 @@ export const Statusbar: FC<StatusbarProps> = ({
lastConnectTime, lastConnectTime,
lastDisconnectTime, lastDisconnectTime,
viewerCount, viewerCount,
className,
}) => { }) => {
const [, setNow] = useState(new Date()); const [, setNow] = useState(new Date());
@ -75,7 +78,7 @@ export const Statusbar: FC<StatusbarProps> = ({
} }
return ( return (
<div className={styles.statusbar} role="status"> <div className={classNames(styles.statusbar, className)} role="status">
<div>{onlineMessage}</div> <div>{onlineMessage}</div>
<div>{rightSideMessage}</div> <div>{rightSideMessage}</div>
</div> </div>

View file

@ -8,7 +8,7 @@
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
@media (max-width: 1200px) { @media (max-width: 1200px) {
height: unset; height: 100%;
max-height: 75vh; max-height: 75vh;
} }

View file

@ -2,6 +2,7 @@ import React, { FC, useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { VideoJsPlayerOptions } from 'video.js'; import { VideoJsPlayerOptions } from 'video.js';
import classNames from 'classnames';
import { VideoJS } from '../VideoJS/VideoJS'; import { VideoJS } from '../VideoJS/VideoJS';
import ViewerPing from '../viewer-ping'; import ViewerPing from '../viewer-ping';
import { VideoPoster } from '../VideoPoster/VideoPoster'; import { VideoPoster } from '../VideoPoster/VideoPoster';
@ -26,6 +27,7 @@ export type OwncastPlayerProps = {
online: boolean; online: boolean;
initiallyMuted?: boolean; initiallyMuted?: boolean;
title: string; title: string;
className?: string;
}; };
async function getVideoSettings() { async function getVideoSettings() {
@ -45,6 +47,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
online, online,
initiallyMuted = false, initiallyMuted = false,
title, title,
className,
}) => { }) => {
const playerRef = React.useRef(null); const playerRef = React.useRef(null);
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom); const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
@ -308,7 +311,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({
); );
return ( return (
<div className={styles.container} id="player"> <div className={classNames(styles.container, className)} id="player">
{online && ( {online && (
<div className={styles.player}> <div className={styles.player}>
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} aria-label={title} /> <VideoJS options={videoJsOptions} onReady={handlePlayerReady} aria-label={title} />

View file

@ -11,7 +11,18 @@
.vjs-big-play-button { .vjs-big-play-button {
z-index: 10; z-index: 10;
color: var(--theme-color-action); 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-color: transparent !important;
border-radius: var(--theme-rounded-corners) !important; border-radius: var(--theme-rounded-corners) !important;
background-color: transparent !important; background-color: transparent !important;
@ -58,10 +69,10 @@
font-family: VideoJS, serif; font-family: VideoJS, serif;
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
}
.vjs-icon-placeholder::before { &::before {
content: '\f110'; content: '\f110';
}
} }
} }

View file

@ -1,7 +1,10 @@
.poster { .poster {
background-color: black;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.image {
background-color: black;
}

View file

@ -36,6 +36,7 @@ export const VideoPoster: FC<VideoPosterProps> = ({ online, initialSrc, src: bas
objectFit="contain" objectFit="contain"
height="auto" height="auto"
width="100%" width="100%"
className={styles.image}
/> />
)} )}
</div> </div>