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}
/>
)}