Refactor mobile chat into modal (#3038)
* feat(mobile): refactor mobile chat into modal - Make page always scrollable - Move mobile chat into a standalone modal * fix(test): split out mobile browser test specs * fix(mobile): force chat button to render on top of footer * fix: some small updates from review * fix: hide/show hide chat menu option based on width * fix: chat button icon getting cut off * chore(tests): add browser tests for mobile chat modal * chore(tests): add story for ChatModal component * fix(test): quiet shellcheck * fix: remove unused import * fix(tests): silence storybook linting warning * fix(ui): reposition chat modal button icon with transform
This commit is contained in:
@@ -67,7 +67,7 @@ const ActionButtons: FC<ActionButtonProps> = ({
|
||||
)}
|
||||
</ActionButtonRow>
|
||||
</div>
|
||||
<div className={styles.mobileActionButtonMenu}>
|
||||
<div className={styles.mobileActionButtons}>
|
||||
{(supportsBrowserNotifications ||
|
||||
supportsBrowserNotifications ||
|
||||
externalActionButtons.length > 0) && (
|
||||
|
||||
@@ -13,20 +13,9 @@
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
@include screen(tablet) {
|
||||
top: 0;
|
||||
@include screen(tablet) {
|
||||
top: 0;
|
||||
position: relative;
|
||||
|
||||
&.online {
|
||||
position: absolute;
|
||||
top: calc(var(--player-container-height) + var(--status-bar-height) + var(--header-height));
|
||||
|
||||
// As we want content in the tabs to scroll within itself, force the tabs to display max height at all times.
|
||||
// (We don't have to do this when not-online because we're having the entire layout scroll.
|
||||
:global(.ant-tabs-content) {
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.ant-tabs-nav) {
|
||||
@@ -53,16 +42,22 @@
|
||||
color: var(--theme-color-background-main);
|
||||
}
|
||||
|
||||
.mobileActionButtonMenu {
|
||||
.mobileActionButtons {
|
||||
display: none;
|
||||
|
||||
@include screen(tablet) {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 10px;
|
||||
z-index: 199;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.desktopActionButtons {
|
||||
@@ -109,3 +104,12 @@
|
||||
.offlineBanner {
|
||||
color: var(--theme-color-background-main);
|
||||
}
|
||||
|
||||
.floatingMobileChatModalButton {
|
||||
position: fixed;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
bottom: 40px;
|
||||
right: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { Skeleton, Col, Row } from 'antd';
|
||||
import { Skeleton, Col, Row, Button } from 'antd';
|
||||
import MessageFilled from '@ant-design/icons/MessageFilled';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import classnames from 'classnames';
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
clientConfigStateAtom,
|
||||
chatMessagesAtom,
|
||||
currentUserAtom,
|
||||
isChatAvailableSelector,
|
||||
isChatVisibleSelector,
|
||||
appStateAtom,
|
||||
isOnlineSelector,
|
||||
@@ -32,6 +32,7 @@ import { ExternalAction } from '../../../interfaces/external-action';
|
||||
import { Modal } from '../Modal/Modal';
|
||||
import { DesktopContent } from './DesktopContent';
|
||||
import { MobileContent } from './MobileContent';
|
||||
import { ChatModal } from '../../modals/ChatModal/ChatModal';
|
||||
|
||||
// Lazy loaded components
|
||||
|
||||
@@ -91,7 +92,6 @@ export const Content: FC = () => {
|
||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const isChatVisible = useRecoilValue<boolean>(isChatVisibleSelector);
|
||||
const isChatAvailable = useRecoilValue<boolean>(isChatAvailableSelector);
|
||||
const currentUser = useRecoilValue(currentUserAtom);
|
||||
const serverStatus = useRecoilValue<ServerStatus>(serverStatusState);
|
||||
const [isMobile, setIsMobile] = useRecoilState<boolean | undefined>(isMobileAtom);
|
||||
@@ -124,6 +124,8 @@ export const Content: FC = () => {
|
||||
const [supportsBrowserNotifications, setSupportsBrowserNotifications] = useState(false);
|
||||
const supportFediverseFeatures = fediverseEnabled;
|
||||
|
||||
const [showChatModal, setShowChatModal] = useState(false);
|
||||
|
||||
const externalActionSelected = (action: ExternalAction) => {
|
||||
const { openExternally, url } = action;
|
||||
// apply openExternally only if we don't have an HTML embed
|
||||
@@ -262,12 +264,8 @@ export const Content: FC = () => {
|
||||
tags={tags}
|
||||
socialHandles={socialHandles}
|
||||
extraPageContent={extraPageContent}
|
||||
messages={messages}
|
||||
currentUser={currentUser}
|
||||
showChat={showChat}
|
||||
setShowFollowModal={setShowFollowModal}
|
||||
supportFediverseFeatures={supportFediverseFeatures}
|
||||
chatEnabled={isChatAvailable}
|
||||
online={online}
|
||||
/>
|
||||
) : (
|
||||
@@ -305,6 +303,24 @@ export const Content: FC = () => {
|
||||
handleClose={() => setShowFollowModal(false)}
|
||||
/>
|
||||
</Modal>
|
||||
{showChatModal && isChatVisible && (
|
||||
<ChatModal
|
||||
messages={messages}
|
||||
currentUser={currentUser}
|
||||
handleClose={() => setShowChatModal(false)}
|
||||
/>
|
||||
)}
|
||||
{isChatVisible && (
|
||||
<Button
|
||||
id="mobile-chat-button"
|
||||
type="primary"
|
||||
onClick={() => setShowChatModal(true)}
|
||||
className={styles.floatingMobileChatModalButton}
|
||||
style={{ zIndex: 99 }}
|
||||
>
|
||||
Chat <MessageFilled style={{ transform: 'translateX(-1px)' }} />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Skeleton, TabsProps } from 'antd';
|
||||
import { TabsProps } from 'antd';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import classNames from 'classnames';
|
||||
import { SocialLink } from '../../../interfaces/social-link.model';
|
||||
import styles from './Content.module.scss';
|
||||
import { CustomPageContent } from '../CustomPageContent/CustomPageContent';
|
||||
import { ContentHeader } from '../../common/ContentHeader/ContentHeader';
|
||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||
import { CurrentUser } from '../../../interfaces/current-user';
|
||||
import { ComponentError } from '../ComponentError/ComponentError';
|
||||
|
||||
export type MobileContentProps = {
|
||||
@@ -19,10 +17,6 @@ export type MobileContentProps = {
|
||||
extraPageContent: string;
|
||||
setShowFollowModal: (show: boolean) => void;
|
||||
supportFediverseFeatures: boolean;
|
||||
messages: ChatMessage[];
|
||||
currentUser: CurrentUser;
|
||||
showChat: boolean;
|
||||
chatEnabled: boolean;
|
||||
online: boolean;
|
||||
};
|
||||
|
||||
@@ -42,20 +36,6 @@ const FollowerCollection = dynamic(
|
||||
},
|
||||
);
|
||||
|
||||
const ChatContainer = dynamic(
|
||||
() => import('../../chat/ChatContainer/ChatContainer').then(mod => mod.ChatContainer),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
type ChatContentProps = {
|
||||
showChat: boolean;
|
||||
chatEnabled: boolean;
|
||||
messages: ChatMessage[];
|
||||
currentUser: CurrentUser;
|
||||
};
|
||||
|
||||
const ComponentErrorFallback = ({ error, resetErrorBoundary }) => (
|
||||
<ComponentError
|
||||
message={error}
|
||||
@@ -64,32 +44,12 @@ const ComponentErrorFallback = ({ error, resetErrorBoundary }) => (
|
||||
/>
|
||||
);
|
||||
|
||||
const ChatContent: FC<ChatContentProps> = ({ showChat, chatEnabled, messages, currentUser }) => {
|
||||
const { id, displayName } = currentUser;
|
||||
|
||||
return showChat && !!currentUser ? (
|
||||
<ChatContainer
|
||||
messages={messages}
|
||||
usernameToHighlight={displayName}
|
||||
chatUserId={id}
|
||||
isModerator={false}
|
||||
chatAvailable={chatEnabled}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton loading active paragraph={{ rows: 7 }} />
|
||||
);
|
||||
};
|
||||
|
||||
export const MobileContent: FC<MobileContentProps> = ({
|
||||
name,
|
||||
summary,
|
||||
tags,
|
||||
socialHandles,
|
||||
extraPageContent,
|
||||
messages,
|
||||
currentUser,
|
||||
showChat,
|
||||
chatEnabled,
|
||||
setShowFollowModal,
|
||||
supportFediverseFeatures,
|
||||
online,
|
||||
@@ -111,23 +71,10 @@ export const MobileContent: FC<MobileContentProps> = ({
|
||||
);
|
||||
|
||||
const items = [];
|
||||
if (showChat && currentUser) {
|
||||
items.push({
|
||||
label: 'Chat',
|
||||
key: '0',
|
||||
children: (
|
||||
<ChatContent
|
||||
showChat={showChat}
|
||||
chatEnabled={chatEnabled}
|
||||
messages={messages}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
items.push({ label: 'About', key: '2', children: aboutTabContent });
|
||||
|
||||
items.push({ label: 'About', key: '0', children: aboutTabContent });
|
||||
if (supportFediverseFeatures) {
|
||||
items.push({ label: 'Followers', key: '3', children: followersTabContent });
|
||||
items.push({ label: 'Followers', key: '1', children: followersTabContent });
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tooltip, Avatar } from 'antd';
|
||||
import { FC } from 'react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import cn from 'classnames';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Link from 'next/link';
|
||||
@@ -21,41 +21,51 @@ export type HeaderComponentProps = {
|
||||
online: boolean;
|
||||
};
|
||||
|
||||
export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisabled, online }) => (
|
||||
<header className={cn([`${styles.header}`], 'global-header')}>
|
||||
{online ? (
|
||||
<Link href="#player" className={styles.skipLink}>
|
||||
Skip to player
|
||||
export const Header: FC<HeaderComponentProps> = ({ name, chatAvailable, chatDisabled, online }) => {
|
||||
const [canHideChat, setCanHideChat] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCanHideChat(window.innerWidth >= 768);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className={cn([`${styles.header}`], 'global-header')}>
|
||||
{online ? (
|
||||
<Link href="#player" className={styles.skipLink}>
|
||||
Skip to player
|
||||
</Link>
|
||||
) : (
|
||||
<Link href="#offline-message" className={styles.skipLink}>
|
||||
Skip to offline message
|
||||
</Link>
|
||||
)}
|
||||
<Link href="#skip-to-content" className={styles.skipLink}>
|
||||
Skip to page content
|
||||
</Link>
|
||||
) : (
|
||||
<Link href="#offline-message" className={styles.skipLink}>
|
||||
Skip to offline message
|
||||
<Link href="#footer" className={styles.skipLink}>
|
||||
Skip to footer
|
||||
</Link>
|
||||
)}
|
||||
<Link href="#skip-to-content" className={styles.skipLink}>
|
||||
Skip to page content
|
||||
</Link>
|
||||
<Link href="#footer" className={styles.skipLink}>
|
||||
Skip to footer
|
||||
</Link>
|
||||
<div className={styles.logo}>
|
||||
<div id="header-logo" className={styles.logoImage}>
|
||||
<Avatar src="/logo" size="large" shape="circle" className={styles.avatar} />
|
||||
<div className={styles.logo}>
|
||||
<div id="header-logo" className={styles.logoImage}>
|
||||
<Avatar src="/logo" size="large" shape="circle" className={styles.avatar} />
|
||||
</div>
|
||||
<h1 className={styles.title} id="global-header-text">
|
||||
{name}
|
||||
</h1>
|
||||
</div>
|
||||
<h1 className={styles.title} id="global-header-text">
|
||||
{name}
|
||||
</h1>
|
||||
</div>
|
||||
{chatAvailable && !chatDisabled && <UserDropdown />}
|
||||
{!chatAvailable && !chatDisabled && (
|
||||
<Tooltip
|
||||
overlayClassName={styles.toolTip}
|
||||
title="Chat will be available when the stream is live."
|
||||
placement="left"
|
||||
>
|
||||
<span className={styles.chatOfflineText}>Chat is offline</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
{chatAvailable && !chatDisabled && (
|
||||
<UserDropdown id="user-menu" hideTitleOnMobile showToggleChatOption={canHideChat} />
|
||||
)}
|
||||
{!chatAvailable && !chatDisabled && (
|
||||
<Tooltip
|
||||
overlayClassName={styles.toolTip}
|
||||
title="Chat will be available when the stream is live."
|
||||
placement="left"
|
||||
>
|
||||
<span className={styles.chatOfflineText}>Chat is offline</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
export default Header;
|
||||
|
||||
Reference in New Issue
Block a user