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:
Gabe Kangas
2023-05-22 18:56:44 -07:00
committed by GitHub
parent b9b569f3fe
commit 69f217f758
21 changed files with 945 additions and 215 deletions

View File

@@ -0,0 +1,12 @@
.modalWrapper {
// This is so modals and popovers spawned from this modal will
// be on top of it. Modals usually have a z-index of 1000 and popovers 800.
// We're getting into really hacky territory here. Don't judge, there
// is history behind this. Ask an elder to tell you tales.
z-index: 799;
top: unset;
}
.root {
// margin-top: 18vh;
}

View File

@@ -0,0 +1,598 @@
/* eslint-disable object-shorthand */
import React, { useEffect } from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { ChatModal, ChatModalProps } from './ChatModal';
import { ChatMessage } from '../../../interfaces/chat-message.model';
import { CurrentUser } from '../../../interfaces/current-user';
import { currentUserAtom } from '../../stores/ClientConfigStore';
export default {
title: 'owncast/Chat/Chat modal',
component: ChatModal,
parameters: {
chromatic: { diffThreshold: 0.8 },
docs: {},
},
} as ComponentMeta<typeof ChatModal>;
const testMessages = `[
{
"type": "CHAT",
"id": "wY-MEXwnR",
"timestamp": "2022-04-28T20:30:27.001762726Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "UserDisplayName42",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "this is a test message"
},
{
"type": "CHAT",
"id": "VhLGEXwnR",
"timestamp": "2022-04-28T20:30:28.806999545Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "Hit 3"
},
{
"type": "CHAT",
"id": "GguMEuw7R",
"timestamp": "2022-04-28T20:30:34.500150601Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "Jkjk"
},
{
"type": "CHAT",
"id": "y_-VEXwnR",
"timestamp": "2022-04-28T20:31:32.695583044Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "I\\u0026#39;m doing alright. How about you Hatnix?"
},
{
"type": "CHAT",
"id": "qAaKEuwng",
"timestamp": "2022-04-28T20:34:16.22275314Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "Oh shiet I didn\\u0026#39;t think you would kill him"
},
{
"type": "CHAT",
"id": "8wUFEuwnR",
"timestamp": "2022-04-28T20:34:21.624898714Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "Hahaha, ruthless"
},
{
"type": "CHAT",
"id": "onYcPuQnR",
"timestamp": "2022-04-28T20:34:50.671024312Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "I\\u0026#39;ve never played it before"
},
{
"type": "CHAT",
"id": "kORyEXQ7R",
"timestamp": "2022-04-28T20:40:29.761977233Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "brb real quick"
},
{
"type": "CHAT",
"id": "F3DvsuQ7g",
"timestamp": "2022-04-28T20:50:29.451341783Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "I\\u0026#39;m back"
},
{
"type": "CHAT",
"id": "AH2vsXwnR",
"timestamp": "2022-04-28T20:50:33.872156152Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "Whoa what happened here?"
},
{
"type": "CHAT",
"id": "xGkOsuw7R",
"timestamp": "2022-04-28T20:50:53.202147658Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "Your dwarf was half naked."
},
{
"type": "CHAT",
"id": "opIdsuw7g",
"timestamp": "2022-04-28T20:50:59.631595947Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "lol"
},
{
"type": "CHAT",
"id": "JpwdsuQnR",
"timestamp": "2022-04-28T20:51:18.065535459Z",
"user": {
"id": "vbh9gtPng",
"displayName": "𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"displayColor": 276,
"createdAt": "2022-03-16T21:02:32.009965702Z",
"previousNames": [
"goth-volhard",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"
],
"nameChangedAt": "2022-04-14T21:51:50.97992512Z",
"scopes": [
""
]
},
"body": "evening did i just see you running around in... nothing"
},
{
"type": "CHAT",
"id": "R4WKsXw7R",
"timestamp": "2022-04-28T20:51:28.064914803Z",
"user": {
"id": "vbh9gtPng",
"displayName": "𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"displayColor": 276,
"createdAt": "2022-03-16T21:02:32.009965702Z",
"previousNames": [
"goth-volhard",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"
],
"nameChangedAt": "2022-04-14T21:51:50.97992512Z",
"scopes": [
""
]
},
"body": "^^"
},
{
"type": "CHAT",
"id": "g-PKyXw7g",
"timestamp": "2022-04-28T20:51:47.936500772Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "Lol Starfarer, so my eyes didnt deceive me."
},
{
"type": "CHAT",
"id": "fV8Ksuw7R",
"timestamp": "2022-04-28T20:51:49.588744112Z",
"user": {
"id": "h_5GQ6E7R",
"displayName": "EliteMooseTaskForce",
"displayColor": 329,
"createdAt": "2022-03-24T03:52:37.966584694Z",
"previousNames": [
"gifted-nobel",
"EliteMooseTaskForce"
],
"nameChangedAt": "2022-04-26T23:56:05.531287897Z",
"scopes": [
""
]
},
"body": "hahahaha"
},
{
"type": "CHAT",
"id": "TaStyuwnR",
"timestamp": "2022-04-28T20:52:38.127528579Z",
"user": {
"id": "vbh9gtPng",
"displayName": "𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"displayColor": 276,
"createdAt": "2022-03-16T21:02:32.009965702Z",
"previousNames": [
"goth-volhard",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"
],
"nameChangedAt": "2022-04-14T21:51:50.97992512Z",
"scopes": [
""
]
},
"body": "lol sounds nice"
},
{
"type": "CHAT",
"id": "JGposuwng",
"timestamp": "2022-04-28T20:53:49.329567087Z",
"user": {
"id": "GCa3J9P7R",
"displayName": "(ghost of)^10 * toudy49",
"displayColor": 147,
"createdAt": "2022-03-22T21:49:25.284237821Z",
"previousNames": [
"lucid-pike",
"toudy49",
"ghost of toudy49",
"ghost of ghost of toudy49",
"ghost of ghost of ghost of toudy49",
"ghost of ghost of ghost of ghost of toudy49",
"ghost of ghost of ghost of ghost of ghost of toudy49",
"ghost ofghost of ghost of ghost of ghost of ghost of toudy49",
"ghostof ghost of ghost of ghost of ghost of ghost of toudy49",
"(ghost of)^6 * toudy49",
"(ghost of)^7 * toudy49",
"(ghost of)^8 * toudy49",
"(ghost of)^9 * toudy49",
"(ghost of)^10 * toudy49"
],
"nameChangedAt": "2022-04-11T21:01:19.938445828Z",
"scopes": [
""
]
},
"body": "!hydrate"
},
{
"type": "CHAT",
"id": "T4tTsuwng",
"timestamp": "2022-04-28T20:53:49.391636551Z",
"user": {
"id": "fKINHKpnR",
"displayName": "hatnixbot",
"displayColor": 325,
"createdAt": "2021-11-24T08:11:32Z",
"previousNames": [
"hatnixbot"
],
"scopes": [
"CAN_SEND_SYSTEM_MESSAGES",
"CAN_SEND_MESSAGES",
"HAS_ADMIN_ACCESS"
]
},
"body": "test 123"
},
{
"type": "CHAT",
"id": "wUJTsuw7R",
"timestamp": "2022-04-28T20:53:54.073218761Z",
"user": {
"id": "GCa3J9P7R",
"displayName": "(ghost of)^10 * toudy49",
"displayColor": 147,
"createdAt": "2022-03-22T21:49:25.284237821Z",
"previousNames": [
"lucid-pike",
"toudy49",
"ghost of toudy49",
"ghost of ghost of toudy49",
"ghost of ghost of ghost of toudy49",
"ghost of ghost of ghost of ghost of toudy49",
"ghost of ghost of ghost of ghost of ghost of toudy49",
"ghost ofghost of ghost of ghost of ghost of ghost of toudy49",
"ghostof ghost of ghost of ghost of ghost of ghost of toudy49",
"(ghost of)^6 * toudy49",
"(ghost of)^7 * toudy49",
"(ghost of)^8 * toudy49",
"(ghost of)^9 * toudy49",
"(ghost of)^10 * toudy49"
],
"nameChangedAt": "2022-04-11T21:01:19.938445828Z",
"scopes": [
""
]
},
"body": "!stretch"
},
{
"id": "xDHBYL4Vgz",
"timestamp": "2022-10-05T01:50:08.178863235Z",
"type": "USER_JOINED",
"user": {
"id": "fg9tcCnVg",
"displayName": "brave-khorana",
"displayColor": 293,
"createdAt": "2022-09-25T15:27:35.444193966Z",
"previousNames": [
"brave-khorana"
],
"nameChangedAt": "0001-01-01T00:00:00Z",
"isBot": false,
"authenticated": false
}
},
{
"type": "CHAT",
"id": "S_Joyuw7R",
"timestamp": "2022-04-28T20:53:54.119778013Z",
"user": {
"id": "fKINHKpnR",
"displayName": "hatnixbot",
"displayColor": 325,
"createdAt": "2021-11-24T08:11:32Z",
"previousNames": [
"hatnixbot"
],
"scopes": [
"CAN_SEND_SYSTEM_MESSAGES",
"CAN_SEND_MESSAGES",
"HAS_ADMIN_ACCESS"
]
},
"body": "blah blah"
},
{
"body": "Bonjour gabe. What a pleasure to meet you.",
"id": "YZqhLYV4g",
"timestamp": "2022-10-05T01:47:13.909247665Z",
"type": "SYSTEM",
"user": {
"displayName": "Owncast TV"
}
},
{
"type": "CHAT",
"id": "MtYTyXwnR",
"timestamp": "2022-04-28T20:53:57.796985761Z",
"user": {
"id": "vbh9gtPng",
"displayName": "𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"displayColor": 276,
"createdAt": "2022-03-16T21:02:32.009965702Z",
"previousNames": [
"goth-volhard",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"
],
"nameChangedAt": "2022-04-14T21:51:50.97992512Z",
"scopes": [
""
]
},
"body": "heyy toudy"
},
{
"type": "CHAT",
"id": "MtYTyXwnR",
"timestamp": "2022-04-28T20:53:57.796985761Z",
"user": {
"id": "vbh9gtPng",
"displayName": "𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"displayColor": 276,
"createdAt": "2022-03-16T21:02:32.009965702Z",
"previousNames": [
"goth-volhard",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™",
"𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™",
"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"
],
"nameChangedAt": "2022-04-14T21:51:50.97992512Z",
"scopes": [
""
]
},
"body": "how is everyone?"
},
{
"body": "Gabe Test liked that this stream went live.",
"id": "FTprqf0VR",
"image": "https://media.mastodon.cloud/accounts/avatars/000/463/008/original/d0bc0971a54ffc75.jpg",
"link": "https://mastodon.cloud/users/gabektest",
"timestamp": "2023-02-05T17:49:36.619470844-08:00",
"title": "gabektest@mastodon.cloud",
"type": "FEDIVERSE_ENGAGEMENT_LIKE",
"user": {
"displayName": "New Owncast Server"
}
}
]`;
const messages: ChatMessage[] = JSON.parse(testMessages);
const currentUser = {
id: 'fKINHKpnR',
displayName: 'testuser',
displayColor: 2,
isModerator: true,
};
// This component uses Recoil internally so wrap it in a RecoilRoot.
const Component = args => {
const setCurrentUser = useSetRecoilState<CurrentUser>(currentUserAtom);
useEffect(() => setCurrentUser(currentUser), []);
return <ChatModal {...args} />;
};
const Template: ComponentStory<typeof ChatModal> = args => (
<RecoilRoot>
<Component {...args} />
</RecoilRoot>
);
export const Example = Template.bind({});
Example.args = {
loading: false,
messages: messages,
usernameToHighlight: 'testuser',
chatUserId: 'testuser',
isModerator: true,
showInput: true,
chatAvailable: true,
handleClose: () => {},
currentUser: currentUser,
} as ChatModalProps;

View File

@@ -0,0 +1,66 @@
import { Modal } from 'antd';
import { FC } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import styles from './ChatModal.module.scss';
import { ComponentError } from '../../ui/ComponentError/ComponentError';
import { ChatContainer } from '../../chat/ChatContainer/ChatContainer';
import { ChatMessage } from '../../../interfaces/chat-message.model';
import { CurrentUser } from '../../../interfaces/current-user';
import { UserDropdown } from '../../common/UserDropdown/UserDropdown';
export type ChatModalProps = {
messages: ChatMessage[];
currentUser: CurrentUser;
handleClose: () => void;
};
export const ChatModal: FC<ChatModalProps> = ({ messages, currentUser, handleClose }) => {
if (!currentUser) {
return null;
}
const { id, displayName, isModerator } = currentUser;
const modalBodyStyle = {
padding: '0px',
height: '55vh',
};
return (
<ErrorBoundary
// eslint-disable-next-line react/no-unstable-nested-components
fallbackRender={({ error, resetErrorBoundary }) => (
<ComponentError
componentName="ChatModal"
message={error.message}
retryFunction={resetErrorBoundary}
/>
)}
>
<Modal
open
centered
maskClosable={false}
footer={null}
title={<UserDropdown id="chat-modal-user-menu" showToggleChatOption={false} />}
maskStyle={{
zIndex: 700,
}}
className={styles.root}
bodyStyle={modalBodyStyle}
wrapClassName={styles.modalWrapper}
onCancel={handleClose}
>
<ChatContainer
messages={messages}
usernameToHighlight={displayName}
chatUserId={id}
isModerator={isModerator}
chatAvailable
focusInput={false}
/>
</Modal>
</ErrorBoundary>
);
};