mirror of
https://github.com/owncast/owncast.git
synced 2024-10-10 19:16:02 +00:00
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:
parent
b9b569f3fe
commit
69f217f758
@ -0,0 +1,16 @@
|
||||
import { setup } from '../../support/setup.js';
|
||||
import filterTests from '../../support/filterTests';
|
||||
|
||||
setup();
|
||||
|
||||
filterTests(['mobile'], () => {
|
||||
describe(`Live mobile tests`, () => {
|
||||
it('Can visit the page', () => {
|
||||
cy.visit('http://localhost:8080');
|
||||
});
|
||||
|
||||
it('Mobile chat button should not be visible', () => {
|
||||
cy.get('#mobile-chat-button').should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
import { setup } from '../../support/setup.js';
|
||||
import fetchData from '../../support/fetchData.js';
|
||||
import filterTests from '../../support/filterTests';
|
||||
|
||||
setup();
|
||||
|
||||
@ -12,73 +13,60 @@ describe(`Live tests`, () => {
|
||||
cy.get('.vjs-big-play-button').should('be.visible');
|
||||
});
|
||||
|
||||
// it('Chat should be visible', () => {
|
||||
// cy.get('#chat-container').should('be.visible');
|
||||
// });
|
||||
|
||||
it('User menu should be visible', () => {
|
||||
cy.get('#user-menu').should('be.visible');
|
||||
});
|
||||
|
||||
// it('Chat join message should exist', () => {
|
||||
// cy.contains('joined the chat').should('be.visible');
|
||||
// });
|
||||
|
||||
it('User menu should be visible', () => {
|
||||
cy.get('#user-menu').should('be.visible');
|
||||
});
|
||||
|
||||
it('Click on user menu', () => {
|
||||
cy.get('#user-menu').click();
|
||||
});
|
||||
|
||||
it('Can toggle chat off', () => {
|
||||
cy.contains('Hide Chat').click();
|
||||
});
|
||||
|
||||
it('Chat should not be visible', () => {
|
||||
cy.get('#chat-container').should('not.exist');
|
||||
});
|
||||
|
||||
it('Click on user menu', () => {
|
||||
cy.get('#user-menu').click();
|
||||
});
|
||||
|
||||
it('Can toggle chat on', () => {
|
||||
cy.contains('Show Chat').click();
|
||||
});
|
||||
|
||||
// it('Chat should be re-visible', () => {
|
||||
// cy.get('#chat-container').should('be.visible');
|
||||
// });
|
||||
|
||||
it('Click on user menu', () => {
|
||||
cy.get('#user-menu').click();
|
||||
});
|
||||
|
||||
it('Show change name modal', () => {
|
||||
cy.contains('Change name').click();
|
||||
});
|
||||
|
||||
it('Should change name', () => {
|
||||
cy.get('#name-change-field').focus();
|
||||
cy.get('#name-change-field').type('{ctrl+a}');
|
||||
cy.get('#name-change-field').type('my-new-name');
|
||||
cy.get('#name-change-submit').click();
|
||||
cy.get('.ant-modal-close-x').click();
|
||||
cy.wait(1500);
|
||||
// cy.contains('is now known as').should('be.visible');
|
||||
});
|
||||
|
||||
it('Should change to custom websocket host', () => {
|
||||
fetchData('http://localhost:8080/api/admin/config/sockethostoverride', {
|
||||
method: 'POST',
|
||||
data: { value: 'ws://localhost:8080' },
|
||||
});
|
||||
cy.wait(1500);
|
||||
});
|
||||
|
||||
it('Refresh page with new socket host', () => {
|
||||
cy.visit('http://localhost:8080');
|
||||
});
|
||||
});
|
||||
|
||||
filterTests(['desktop'], () => {
|
||||
describe(`Live desktop tests`, () => {
|
||||
it('Click on user menu', () => {
|
||||
cy.get('#user-menu').click();
|
||||
});
|
||||
it('Can toggle chat off', () => {
|
||||
cy.contains('Hide Chat').click();
|
||||
});
|
||||
|
||||
it('Chat should not be visible', () => {
|
||||
cy.get('#chat-container').should('not.exist');
|
||||
});
|
||||
|
||||
it('Click on user menu', () => {
|
||||
cy.get('#user-menu').click();
|
||||
});
|
||||
|
||||
it('Can toggle chat on', () => {
|
||||
cy.contains('Show Chat').click();
|
||||
});
|
||||
|
||||
it('Click on user menu', () => {
|
||||
cy.get('#user-menu').click();
|
||||
});
|
||||
|
||||
it('Show change name modal', () => {
|
||||
cy.contains('Change name').click();
|
||||
});
|
||||
|
||||
it('Should change name', () => {
|
||||
cy.get('#name-change-field').focus();
|
||||
cy.get('#name-change-field').type('{ctrl+a}');
|
||||
cy.get('#name-change-field').type('my-new-name');
|
||||
cy.get('#name-change-submit').click();
|
||||
cy.get('.ant-modal-close-x').click();
|
||||
cy.wait(1500);
|
||||
// cy.contains('is now known as').should('be.visible');
|
||||
});
|
||||
|
||||
it('Should change to custom websocket host', () => {
|
||||
fetchData('http://localhost:8080/api/admin/config/sockethostoverride', {
|
||||
method: 'POST',
|
||||
data: { value: 'ws://localhost:8080' },
|
||||
});
|
||||
cy.wait(1500);
|
||||
});
|
||||
|
||||
it('Refresh page with new socket host', () => {
|
||||
cy.visit('http://localhost:8080');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { setup } from '../../support/setup.js';
|
||||
import filterTests from '../../support/filterTests';
|
||||
|
||||
setup();
|
||||
|
||||
filterTests(['mobile'], () => {
|
||||
describe(`Live mobile tests`, () => {
|
||||
it('Can visit the page', () => {
|
||||
cy.visit('http://localhost:8080');
|
||||
});
|
||||
|
||||
it('Mobile chat button should be visible', () => {
|
||||
cy.get('#mobile-chat-button').should('be.visible');
|
||||
});
|
||||
|
||||
it('Click mobile chat button', () => {
|
||||
cy.get('#mobile-chat-button').click();
|
||||
});
|
||||
|
||||
it('Mobile chat modal should be visible', () => {
|
||||
cy.get('.ant-modal').should('be.visible');
|
||||
});
|
||||
|
||||
it('Chat container should be visible', () => {
|
||||
cy.get('#chat-container').should('be.visible');
|
||||
});
|
||||
|
||||
it('Chat input should be visible', () => {
|
||||
cy.get('#chat-input').should('be.visible');
|
||||
});
|
||||
|
||||
it('Click on user menu', () => {
|
||||
cy.get('#chat-modal-user-menu').click();
|
||||
});
|
||||
|
||||
it('Show change name modal', () => {
|
||||
cy.contains('Change name').click();
|
||||
});
|
||||
});
|
||||
});
|
@ -15,7 +15,6 @@ else
|
||||
echo "Google Chrome not found. Using Electron."
|
||||
fi
|
||||
|
||||
|
||||
# Bundle the updated web code into the server codebase.
|
||||
if [ -z "$SKIP_BUILD" ]; then
|
||||
echo "Bundling web code into server..."
|
||||
@ -30,7 +29,6 @@ else
|
||||
echo "Skipping web build..."
|
||||
fi
|
||||
|
||||
|
||||
# Install the web test framework
|
||||
if [ -z "$SKIP_BUILD" ]; then
|
||||
echo "Installing test dependencies..."
|
||||
@ -47,13 +45,13 @@ install_ffmpeg
|
||||
start_owncast
|
||||
|
||||
# Run cypress browser tests for desktop
|
||||
npx cypress run --browser "$BROWSER" --group "desktop-offline" --env tags=desktop --ci-build-id $BUILD_ID --tag "desktop,offline" --record --key e9c8b547-7a8f-452d-8c53-fd7531491e3b --spec "cypress/e2e/offline/*.cy.js"
|
||||
npx cypress run --parallel --browser "$BROWSER" --group "desktop-offline" --env tags=desktop --ci-build-id $BUILD_ID --tag "desktop,offline" --record --key e9c8b547-7a8f-452d-8c53-fd7531491e3b --spec "cypress/e2e/offline/*.cy.js"
|
||||
# Run cypress browser tests for mobile
|
||||
npx cypress run --browser "$BROWSER" --group "mobile-offline" --ci-build-id $BUILD_ID --tag "mobile,offline" --record --key e9c8b547-7a8f-452d-8c53-fd7531491e3b --spec "cypress/e2e/offline/*.cy.js" --config viewportWidth=375,viewportHeight=667
|
||||
npx cypress run --parallel --browser "$BROWSER" --group "mobile-offline" --env tags=mobile --ci-build-id $BUILD_ID --tag "mobile,offline" --record --key e9c8b547-7a8f-452d-8c53-fd7531491e3b --spec "cypress/e2e/offline/*.cy.js" --config viewportWidth=375,viewportHeight=667
|
||||
|
||||
start_stream
|
||||
|
||||
# Run cypress browser tests for desktop
|
||||
npx cypress run --browser "$BROWSER" --group "desktop-online" --env tags=desktop --ci-build-id $BUILD_ID --tag "desktop,online" --record --key e9c8b547-7a8f-452d-8c53-fd7531491e3b --spec "cypress/e2e/online/*.cy.js"
|
||||
npx cypress run --parallel --browser "$BROWSER" --group "desktop-online" --env tags=desktop --ci-build-id $BUILD_ID --tag "desktop,online" --record --key e9c8b547-7a8f-452d-8c53-fd7531491e3b --spec "cypress/e2e/online/*.cy.js"
|
||||
# Run cypress browser tests for mobile
|
||||
npx cypress run --browser "$BROWSER" --group "mobile-online" --ci-build-id $BUILD_ID --tag "mobile,online" --record --key e9c8b547-7a8f-452d-8c53-fd7531491e3b --spec "cypress/e2e/online/*.cy.js" --config viewportWidth=375,viewportHeight=667
|
||||
npx cypress run --parallel --browser "$BROWSER" --group "mobile-online" --env tags=mobile --ci-build-id $BUILD_ID --tag "mobile,online" --record --key e9c8b547-7a8f-452d-8c53-fd7531491e3b --spec "cypress/e2e/online/*.cy.js" --config viewportWidth=375,viewportHeight=667
|
||||
|
@ -5,7 +5,9 @@ set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
finish() {
|
||||
# shellcheck disable=SC2317
|
||||
kill_with_kids "$BROWSERSTACK_PID"
|
||||
# shellcheck disable=SC2317
|
||||
kill_with_kids "$STREAM_PID"
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-bottom: 16px; // to match antd nav margin-bottom
|
||||
|
||||
button {
|
||||
height: 100%;
|
||||
|
@ -29,6 +29,7 @@ export type ChatContainerProps = {
|
||||
showInput?: boolean;
|
||||
height?: string;
|
||||
chatAvailable: boolean;
|
||||
focusInput?: boolean;
|
||||
};
|
||||
|
||||
function shouldCollapseMessages(
|
||||
@ -93,6 +94,7 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
showInput,
|
||||
height,
|
||||
chatAvailable: chatEnabled,
|
||||
focusInput = true,
|
||||
}) => {
|
||||
const [showScrollToBottomButton, setShowScrollToBottomButton] = useState(false);
|
||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||
@ -282,7 +284,7 @@ export const ChatContainer: FC<ChatContainerProps> = ({
|
||||
{MessagesTable}
|
||||
{showInput && (
|
||||
<div className={styles.chatTextField}>
|
||||
<ChatTextField enabled={chatEnabled} />
|
||||
<ChatTextField enabled={chatEnabled} focusInput={focusInput} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -131,11 +131,12 @@ const getCharacterCount = node => {
|
||||
export type ChatTextFieldProps = {
|
||||
defaultText?: string;
|
||||
enabled: boolean;
|
||||
focusInput: boolean;
|
||||
};
|
||||
|
||||
const characterLimit = 300;
|
||||
|
||||
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled }) => {
|
||||
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
|
||||
const [showEmojis, setShowEmojis] = useState(false);
|
||||
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||
@ -240,7 +241,7 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled })
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div id="chat-input" className={styles.root}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.inputWrap,
|
||||
@ -269,7 +270,7 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled })
|
||||
style={{ width: '100%' }}
|
||||
role="textbox"
|
||||
aria-label="Chat text input"
|
||||
autoFocus
|
||||
autoFocus={focusInput}
|
||||
/>
|
||||
<Popover
|
||||
content={
|
||||
|
@ -3,20 +3,31 @@
|
||||
.root {
|
||||
button {
|
||||
border: none;
|
||||
.ant-space {
|
||||
.ant-space-item {
|
||||
color: var(--theme-unknown-2);
|
||||
}
|
||||
}
|
||||
.ant-space {
|
||||
.ant-space-item {
|
||||
color: var(--theme-unknown-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
.username {
|
||||
display: inline;
|
||||
|
||||
@include screen(desktop) {
|
||||
display: inline;
|
||||
font-weight: 600;
|
||||
font-size: .8rem;
|
||||
}
|
||||
@include screen(desktop) {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hideTitleOnMobile {
|
||||
@include screen(mobile) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chatToggle {
|
||||
@include screen(mobile) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ const Example = args => {
|
||||
[],
|
||||
);
|
||||
|
||||
return <UserDropdown {...args} />;
|
||||
return <UserDropdown id="user-menu" {...args} />;
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof UserDropdown> = args => (
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Menu, Dropdown, Button } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { FC, useState } from 'react';
|
||||
@ -55,16 +56,29 @@ const AuthModal = dynamic(
|
||||
);
|
||||
|
||||
export type UserDropdownProps = {
|
||||
id: string;
|
||||
username?: string;
|
||||
hideTitleOnMobile?: boolean;
|
||||
showToggleChatOption?: boolean;
|
||||
};
|
||||
|
||||
export const UserDropdown: FC<UserDropdownProps> = ({ username: defaultUsername = undefined }) => {
|
||||
export const UserDropdown: FC<UserDropdownProps> = ({
|
||||
id,
|
||||
username: defaultUsername = undefined,
|
||||
hideTitleOnMobile = false,
|
||||
showToggleChatOption: showHideChatOption = true,
|
||||
}) => {
|
||||
const [showNameChangeModal, setShowNameChangeModal] = useState<boolean>(false);
|
||||
const [showAuthModal, setShowAuthModal] = useState<boolean>(false);
|
||||
const [chatToggleVisible, setChatToggleVisible] = useRecoilState(chatVisibleToggleAtom);
|
||||
const appState = useRecoilValue<AppStateOptions>(appStateAtom);
|
||||
|
||||
const toggleChatVisibility = () => {
|
||||
// If we don't support the hide chat option then don't do anything.
|
||||
if (!showHideChatOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
setChatToggleVisible(!chatToggleVisible);
|
||||
};
|
||||
|
||||
@ -97,12 +111,13 @@ export const UserDropdown: FC<UserDropdownProps> = ({ username: defaultUsername
|
||||
<Menu.Item key="1" icon={<LockOutlined />} onClick={() => setShowAuthModal(true)}>
|
||||
Authenticate
|
||||
</Menu.Item>
|
||||
{appState.chatAvailable && (
|
||||
{showHideChatOption && appState.chatAvailable && (
|
||||
<Menu.Item
|
||||
key="3"
|
||||
icon={<MessageOutlined />}
|
||||
onClick={() => toggleChatVisibility()}
|
||||
aria-expanded={chatToggleVisible}
|
||||
className={styles.chatToggle}
|
||||
>
|
||||
{chatToggleVisible ? 'Hide Chat' : 'Show Chat'}
|
||||
</Menu.Item>
|
||||
@ -121,10 +136,17 @@ export const UserDropdown: FC<UserDropdownProps> = ({ username: defaultUsername
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<div id="user-menu" className={styles.root}>
|
||||
<div id={id} className={styles.root}>
|
||||
<Dropdown overlay={menu} trigger={['click']}>
|
||||
<Button type="primary" icon={<UserOutlined className={styles.userIcon} />}>
|
||||
<span className={styles.username}>{username}</span>
|
||||
<span
|
||||
className={classnames([
|
||||
styles.username,
|
||||
hideTitleOnMobile && styles.hideTitleOnMobile,
|
||||
])}
|
||||
>
|
||||
{username}
|
||||
</span>
|
||||
<CaretDownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
@ -4,7 +4,7 @@
|
||||
// this margin is for fixed header
|
||||
padding-top: var(--header-height);
|
||||
background-color: var(--theme-color-main-background);
|
||||
min-height: 100dvh;
|
||||
min-height: 100vh;
|
||||
|
||||
position: relative;
|
||||
|
||||
|
@ -171,11 +171,9 @@ export const Main: FC = () => {
|
||||
<FatalErrorStateModal title={fatalError.title} message={fatalError.message} />
|
||||
)}
|
||||
|
||||
{(!isMobile || !online) && (
|
||||
<div className={styles.footerContainer}>
|
||||
<Footer dynamicPaddingValue={dynamicFooterPadding} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.footerContainer}>
|
||||
<Footer dynamicPaddingValue={dynamicFooterPadding} />
|
||||
</div>
|
||||
</Layout>
|
||||
<Noscript />
|
||||
</>
|
||||
|
12
web/components/modals/ChatModal/ChatModal.module.scss
Normal file
12
web/components/modals/ChatModal/ChatModal.module.scss
Normal 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;
|
||||
}
|
598
web/components/modals/ChatModal/ChatModal.stories.tsx
Normal file
598
web/components/modals/ChatModal/ChatModal.stories.tsx
Normal 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;
|
66
web/components/modals/ChatModal/ChatModal.tsx
Normal file
66
web/components/modals/ChatModal/ChatModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user