mirror of
https://github.com/owncast/owncast.git
synced 2025-11-01 10:55:57 +08:00
Fill out some more components + add application state enums
This commit is contained in:
@ -1,5 +1,46 @@
|
|||||||
interface Props {}
|
import { Menu, Dropdown } from 'antd';
|
||||||
|
import { DownOutlined } from '@ant-design/icons';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { ChatVisibilityState, ChatState } from '../interfaces/application-state';
|
||||||
|
import { chatVisibility as chatVisibilityAtom } from './stores/ClientConfigStore';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
username: string;
|
||||||
|
chatState: ChatState;
|
||||||
|
}
|
||||||
|
|
||||||
export default function UserDropdown(props: Props) {
|
export default function UserDropdown(props: Props) {
|
||||||
return <div>User settings dropdown component goes here</div>;
|
const { username, chatState } = props;
|
||||||
|
|
||||||
|
const chatEnabled = chatState !== ChatState.NotAvailable;
|
||||||
|
const [chatVisibility, setChatVisibility] =
|
||||||
|
useRecoilState<ChatVisibilityState>(chatVisibilityAtom);
|
||||||
|
|
||||||
|
const toggleChatVisibility = () => {
|
||||||
|
if (chatVisibility === ChatVisibilityState.Hidden) {
|
||||||
|
setChatVisibility(ChatVisibilityState.Visible);
|
||||||
|
} else {
|
||||||
|
setChatVisibility(ChatVisibilityState.Hidden);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item key="0">Change name</Menu.Item>
|
||||||
|
<Menu.Item key="1">Authenticate</Menu.Item>
|
||||||
|
{chatEnabled && (
|
||||||
|
<Menu.Item key="3" onClick={() => toggleChatVisibility()}>
|
||||||
|
Toggle chat
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} trigger={['click']}>
|
||||||
|
<button type="button" className="ant-dropdown-link" onClick={e => e.preventDefault()}>
|
||||||
|
{username} <DownOutlined />
|
||||||
|
</button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,21 @@
|
|||||||
|
import { Spin } from 'antd';
|
||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||||
|
import { ChatState } from '../../interfaces/application-state';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
|
state: ChatState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatContainer(props: Props) {
|
export default function ChatContainer(props: Props) {
|
||||||
return <div>Chat container goes here</div>;
|
const { messages, state } = props;
|
||||||
|
const loading = state === ChatState.Loading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Spin tip="Loading..." spinning={loading}>
|
||||||
|
Chat container with scrolling chat messages go here
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,35 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
export default function ChatTextField(props: Props) {
|
export default function ChatTextField(props: Props) {
|
||||||
return <div>Component goes here</div>;
|
const [value, setValue] = useState('');
|
||||||
|
const [showEmojis, setShowEmojis] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
placeholder="Type a message here then hit ENTER"
|
||||||
|
/>
|
||||||
|
<button type="button" onClick={() => setShowEmojis(!showEmojis)}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="icon"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,21 @@ import { ChatMessage } from '../../interfaces/chat-message.model';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
|
showModeratorMenu: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatUserMessage(props: Props) {
|
export default function ChatUserMessage(props: Props) {
|
||||||
return <div>Component goes here</div>;
|
const { message, showModeratorMenu } = props;
|
||||||
|
const { body, user, timestamp } = message;
|
||||||
|
const { displayName, displayColor } = user;
|
||||||
|
|
||||||
|
// TODO: Convert displayColor (a hue) to a usable color.
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>{displayName}</div>
|
||||||
|
<div>{body}</div>
|
||||||
|
{showModeratorMenu && <div>Moderator menu</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ReactElement } from 'react-markdown/lib/react-markdown';
|
|
||||||
import { atom, useRecoilState } from 'recoil';
|
import { atom, useRecoilState } from 'recoil';
|
||||||
import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model';
|
import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model';
|
||||||
import ClientConfigService from '../../services/client-config-service';
|
import ClientConfigService from '../../services/client-config-service';
|
||||||
|
import ChatService from '../../services/chat-service';
|
||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
import { ChatMessage } from '../../interfaces/chat-message.model';
|
||||||
|
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
|
||||||
|
import { ChatVisibilityState } from '../../interfaces/application-state';
|
||||||
|
|
||||||
// The config that comes from the API.
|
// The config that comes from the API.
|
||||||
export const clientConfigState = atom({
|
export const clientConfigState = atom({
|
||||||
@ -11,14 +13,19 @@ export const clientConfigState = atom({
|
|||||||
default: makeEmptyClientConfig(),
|
default: makeEmptyClientConfig(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const chatCurrentlyVisible = atom({
|
export const chatVisibility = atom<ChatVisibilityState>({
|
||||||
key: 'chatvisible',
|
key: 'chatVisibility',
|
||||||
default: false,
|
default: ChatVisibilityState.Hidden,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const chatDisplayName = atom({
|
export const chatDisplayName = atom({
|
||||||
key: 'chatDisplayName',
|
key: 'chatDisplayName',
|
||||||
default: '',
|
default: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const accessTokenAtom = atom({
|
||||||
|
key: 'accessToken',
|
||||||
|
default: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const chatMessages = atom({
|
export const chatMessages = atom({
|
||||||
@ -26,8 +33,11 @@ export const chatMessages = atom({
|
|||||||
default: [] as ChatMessage[],
|
default: [] as ChatMessage[],
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ClientConfigStore(): ReactElement {
|
export function ClientConfigStore() {
|
||||||
const [, setClientConfig] = useRecoilState<ClientConfig>(clientConfigState);
|
const [, setClientConfig] = useRecoilState<ClientConfig>(clientConfigState);
|
||||||
|
const [, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessages);
|
||||||
|
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
||||||
|
const [, setChatDisplayName] = useRecoilState<string>(chatDisplayName);
|
||||||
|
|
||||||
const updateClientConfig = async () => {
|
const updateClientConfig = async () => {
|
||||||
try {
|
try {
|
||||||
@ -39,9 +49,41 @@ export function ClientConfigStore(): ReactElement {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUserRegistration = async (optionalDisplayName: string) => {
|
||||||
|
try {
|
||||||
|
const response = await ChatService.registerUser(optionalDisplayName);
|
||||||
|
console.log(`ChatService -> registerUser() response: \n${JSON.stringify(response)}`);
|
||||||
|
const { accessToken: newAccessToken, displayName } = response;
|
||||||
|
if (!newAccessToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAccessToken(accessToken);
|
||||||
|
setLocalStorage('accessToken', newAccessToken);
|
||||||
|
setChatDisplayName(displayName);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`ChatService -> registerUser() ERROR: \n${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Requires access token.
|
||||||
|
const getChatHistory = async () => {
|
||||||
|
try {
|
||||||
|
const messages = await ChatService.getChatHistory(accessToken);
|
||||||
|
console.log(`ChatService -> getChatHistory() messages: \n${JSON.stringify(messages)}`);
|
||||||
|
setChatMessages(messages);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`ChatService -> getChatHistory() ERROR: \n${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateClientConfig();
|
updateClientConfig();
|
||||||
|
handleUserRegistration();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getChatHistory();
|
||||||
|
}, [accessToken]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ReactElement } from 'react-markdown/lib/react-markdown';
|
|
||||||
import { atom, useRecoilState } from 'recoil';
|
import { atom, useRecoilState } from 'recoil';
|
||||||
import { ServerStatus, makeEmptyServerStatus } from '../../interfaces/server-status.model';
|
import { ServerStatus, makeEmptyServerStatus } from '../../interfaces/server-status.model';
|
||||||
import ServerStatusService from '../../services/status-service';
|
import ServerStatusService from '../../services/status-service';
|
||||||
@ -9,7 +8,7 @@ export const serverStatusState = atom({
|
|||||||
default: makeEmptyServerStatus(),
|
default: makeEmptyServerStatus(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ServerStatusStore(): ReactElement {
|
export function ServerStatusStore() {
|
||||||
const [, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState);
|
const [, setServerStatus] = useRecoilState<ServerStatus>(serverStatusState);
|
||||||
|
|
||||||
const updateServerStatus = async () => {
|
const updateServerStatus = async () => {
|
||||||
|
|||||||
@ -2,14 +2,16 @@ import Sider from 'antd/lib/layout/Sider';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||||
import ChatContainer from '../../chat/ChatContainer';
|
import ChatContainer from '../../chat/ChatContainer';
|
||||||
import { chatMessages } from '../../stores/ClientConfigStore';
|
import { chatMessages, chatVisibility as chatVisibilityAtom } from '../../stores/ClientConfigStore';
|
||||||
|
import { ChatVisibilityState } from '../../../interfaces/application-state';
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const messages = useRecoilValue<ChatMessage[]>(chatMessages);
|
const messages = useRecoilValue<ChatMessage[]>(chatMessages);
|
||||||
|
const chatVisibility = useRecoilValue<ChatVisibilityState>(chatVisibilityAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sider
|
<Sider
|
||||||
collapsed={false}
|
collapsed={chatVisibility === ChatVisibilityState.Hidden}
|
||||||
width={300}
|
width={300}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
|||||||
17
web/interfaces/application-state.ts
Normal file
17
web/interfaces/application-state.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export enum AppState {
|
||||||
|
AppLoading,
|
||||||
|
ChatLoading,
|
||||||
|
Loading,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChatVisibilityState {
|
||||||
|
Hidden, // The chat is available but the user has hidden it
|
||||||
|
Visible, // The chat is available and visible
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChatState {
|
||||||
|
Available, // Normal state
|
||||||
|
NotAvailable, // Chat features are not available
|
||||||
|
Loading, // Chat is connecting and loading history
|
||||||
|
Offline, // Chat is offline/disconnected for some reason
|
||||||
|
}
|
||||||
@ -1 +1,9 @@
|
|||||||
export interface ChatMessage {}
|
import { User } from './user';
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
timestamp: Date;
|
||||||
|
user: User;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|||||||
9
web/interfaces/user.ts
Normal file
9
web/interfaces/user.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
displayColor: number;
|
||||||
|
createdAt: Date;
|
||||||
|
previousNames: string[];
|
||||||
|
nameChangedAt: Date;
|
||||||
|
scopes: string[];
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { ChatMessage } from '../interfaces/chat-message.model';
|
||||||
|
const ENDPOINT = `http://localhost:8080/api/chat`;
|
||||||
const URL_CHAT_REGISTRATION = `http://localhost:8080/api/chat/register`;
|
const URL_CHAT_REGISTRATION = `http://localhost:8080/api/chat/register`;
|
||||||
|
|
||||||
interface UserRegistrationResponse {
|
interface UserRegistrationResponse {
|
||||||
@ -6,8 +8,14 @@ interface UserRegistrationResponse {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserService {
|
class ChatService {
|
||||||
registerUser(username: string): Promise<UserRegistrationResponse> {
|
public static async getChatHistory(accessToken: string): Promise<ChatMessage[]> {
|
||||||
|
const response = await fetch(`${ENDPOINT}?accessToken=${accessToken}`);
|
||||||
|
const status = await response.json();
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async registerUser(username: string): Promise<UserRegistrationResponse> {
|
||||||
const options = {
|
const options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -27,3 +35,5 @@ class UserService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default ChatService;
|
||||||
File diff suppressed because one or more lines are too long
@ -2,12 +2,6 @@ import React from 'react';
|
|||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
import ChatTextField from '../components/chat/ChatTextField';
|
import ChatTextField from '../components/chat/ChatTextField';
|
||||||
|
|
||||||
const Example = () => (
|
|
||||||
<div>
|
|
||||||
<ChatTextField />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'owncast/Chat/Input text field',
|
title: 'owncast/Chat/Input text field',
|
||||||
component: ChatTextField,
|
component: ChatTextField,
|
||||||
@ -15,7 +9,7 @@ export default {
|
|||||||
} as ComponentMeta<typeof ChatTextField>;
|
} as ComponentMeta<typeof ChatTextField>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const Template: ComponentStory<typeof ChatTextField> = args => <Example />;
|
const Template: ComponentStory<typeof ChatTextField> = args => <ChatTextField />;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const Basic = Template.bind({});
|
export const Example = Template.bind({});
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
import UserChatMessage from '../components/chat/ChatUserMessage';
|
import UserChatMessage from '../components/chat/ChatUserMessage';
|
||||||
|
import { ChatMessage } from '../interfaces/chat-message.model';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'owncast/Chat/Messages/Standard user',
|
title: 'owncast/Chat/Messages/Standard user',
|
||||||
@ -8,8 +9,74 @@ export default {
|
|||||||
parameters: {},
|
parameters: {},
|
||||||
} as ComponentMeta<typeof UserChatMessage>;
|
} as ComponentMeta<typeof UserChatMessage>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const Template: ComponentStory<typeof UserChatMessage> = args => <UserChatMessage {...args} />;
|
||||||
const Template: ComponentStory<typeof UserChatMessage> = args => <UserChatMessage />;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const standardMessage: ChatMessage = JSON.parse(`{
|
||||||
export const Basic = Template.bind({});
|
"type": "CHAT",
|
||||||
|
"id": "wY-MEXwnR",
|
||||||
|
"timestamp": "2022-04-28T20:30:27.001762726Z",
|
||||||
|
"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": "Test message from a regular user."}`);
|
||||||
|
|
||||||
|
const moderatorMessage: ChatMessage = JSON.parse(`{
|
||||||
|
"type": "CHAT",
|
||||||
|
"id": "wY-MEXwnR",
|
||||||
|
"timestamp": "2022-04-28T20:30:27.001762726Z",
|
||||||
|
"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": ["moderator"]
|
||||||
|
},
|
||||||
|
"body": "I am a moderator user."}`);
|
||||||
|
|
||||||
|
const authenticatedUserMessage: ChatMessage = JSON.parse(`{
|
||||||
|
"type": "CHAT",
|
||||||
|
"id": "wY-MEXwnR",
|
||||||
|
"timestamp": "2022-04-28T20:30:27.001762726Z",
|
||||||
|
"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",
|
||||||
|
"authenticated": true,
|
||||||
|
"scopes": []
|
||||||
|
},
|
||||||
|
"body": "I am an authenticated user."}`);
|
||||||
|
|
||||||
|
export const WithoutModeratorMenu = Template.bind({});
|
||||||
|
WithoutModeratorMenu.args = {
|
||||||
|
message: standardMessage,
|
||||||
|
showModeratorMenu: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithModeratorMenu = Template.bind({});
|
||||||
|
WithModeratorMenu.args = {
|
||||||
|
message: standardMessage,
|
||||||
|
showModeratorMenu: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FromModeratorUser = Template.bind({});
|
||||||
|
FromModeratorUser.args = {
|
||||||
|
message: moderatorMessage,
|
||||||
|
showModeratorMenu: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FromAuthenticatedUser = Template.bind({});
|
||||||
|
FromAuthenticatedUser.args = {
|
||||||
|
message: authenticatedUserMessage,
|
||||||
|
showModeratorMenu: false,
|
||||||
|
};
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Menu, Dropdown } from 'antd';
|
|
||||||
import { DownOutlined } from '@ant-design/icons';
|
|
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
|
||||||
|
|
||||||
const menu = (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item key="0">
|
|
||||||
<a href="https://owncast.online">1st menu item</a>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item key="1">
|
|
||||||
<a href="https://directory.owncast.online">2nd menu item</a>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Divider />
|
|
||||||
<Menu.Item key="3">3rd menu item</Menu.Item>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
const DropdownExample = () => (
|
|
||||||
<Dropdown overlay={menu} trigger={['click']}>
|
|
||||||
<button type="button" className="ant-dropdown-link" onClick={e => e.preventDefault()}>
|
|
||||||
Click me <DownOutlined />
|
|
||||||
</button>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'example/Dropdown',
|
|
||||||
component: Dropdown,
|
|
||||||
parameters: {},
|
|
||||||
} as ComponentMeta<typeof Dropdown>;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const Template: ComponentStory<typeof Dropdown> = args => <DropdownExample />;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
export const Basic = Template.bind({});
|
|
||||||
@ -3,7 +3,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
|
|||||||
import FollowerCollection from '../components/FollowersCollection';
|
import FollowerCollection from '../components/FollowersCollection';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'owncast/Follower collection',
|
title: 'owncast/Followers collection',
|
||||||
component: FollowerCollection,
|
component: FollowerCollection,
|
||||||
parameters: {},
|
parameters: {},
|
||||||
} as ComponentMeta<typeof FollowerCollection>;
|
} as ComponentMeta<typeof FollowerCollection>;
|
||||||
@ -16,46 +16,215 @@ export const Example = Template.bind({});
|
|||||||
Example.args = {
|
Example.args = {
|
||||||
followers: [
|
followers: [
|
||||||
{
|
{
|
||||||
name: 'John Doe',
|
link: 'https://sun.minuscule.space/users/mardijker',
|
||||||
description: 'User',
|
name: 'mardijker',
|
||||||
username: '@account@domain.tld',
|
username: 'mardijker@sun.minuscule.space',
|
||||||
image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
|
image:
|
||||||
link: 'https://yahoo.com',
|
'https://sun.minuscule.space/media/336af7ae5a2bcb508308eddb30b661ee2b2e15004a50795ee3ba0653ab190a93.jpg',
|
||||||
|
timestamp: '2022-04-27T12:12:50Z',
|
||||||
|
disabledAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'John Doe',
|
link: 'https://mastodon.online/users/Kallegro',
|
||||||
description: 'User',
|
name: '',
|
||||||
username: '@account@domain.tld',
|
username: 'Kallegro@mastodon.online',
|
||||||
image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
|
image: '',
|
||||||
link: 'https://yahoo.com',
|
timestamp: '2022-04-26T15:24:09Z',
|
||||||
|
disabledAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'John Doe',
|
link: 'https://mastodon.online/users/kerfuffle',
|
||||||
description: 'User',
|
name: 'Kerfuffle',
|
||||||
username: '@account@domain.tld',
|
username: 'kerfuffle@mastodon.online',
|
||||||
image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
|
image:
|
||||||
link: 'https://yahoo.com',
|
'https://files.mastodon.online/accounts/avatars/000/133/698/original/6aa73caa898b2d36.gif',
|
||||||
|
timestamp: '2022-04-25T21:32:41Z',
|
||||||
|
disabledAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'John Doe',
|
link: 'https://mastodon.uno/users/informapirata',
|
||||||
description: 'User',
|
name: 'informapirata :privacypride:',
|
||||||
username: '@account@domain.tld',
|
username: 'informapirata@mastodon.uno',
|
||||||
image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
|
image:
|
||||||
link: 'https://yahoo.com',
|
'https://cdn.masto.host/mastodonuno/accounts/avatars/000/060/227/original/da4c44c716a339b8.png',
|
||||||
|
timestamp: '2022-04-25T11:38:23Z',
|
||||||
|
disabledAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'John Doe',
|
link: 'https://gamethattune.club/users/Raeanus',
|
||||||
description: 'User',
|
name: 'Raeanus',
|
||||||
username: '@account@domain.tld',
|
username: 'Raeanus@gamethattune.club',
|
||||||
image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
|
image:
|
||||||
link: 'https://yahoo.com',
|
'https://gamethattune.club/media/a6e6ccea-34f8-4c2e-b9dc-ad8cca7fafd3/DD14E3BF-1358-4961-A900-42F3495F6BE2.jpeg',
|
||||||
|
timestamp: '2022-04-23T00:46:56Z',
|
||||||
|
disabledAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'John Doe',
|
link: 'https://mastodon.ml/users/latte',
|
||||||
description: 'User',
|
name: 'Даниил',
|
||||||
username: '@account@domain.tld',
|
username: 'latte@mastodon.ml',
|
||||||
image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
|
image:
|
||||||
link: 'https://yahoo.com',
|
'https://mastodon.ml/system/accounts/avatars/107/837/409/059/601/386/original/c45ec2676489e363.png',
|
||||||
|
timestamp: '2022-04-19T13:06:09Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://wienermobile.rentals/users/jprjr',
|
||||||
|
name: 'Johnny',
|
||||||
|
username: 'jprjr@wienermobile.rentals',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-04-14T14:48:11Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://gamethattune.club/users/johnny',
|
||||||
|
name: 'John Regan',
|
||||||
|
username: 'johnny@gamethattune.club',
|
||||||
|
image:
|
||||||
|
'https://gamethattune.club/media/3c10cd89-866b-4604-ae40-39387fe17061/profile_large.jpg',
|
||||||
|
timestamp: '2022-04-14T14:42:48Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://mastodon.social/users/MightyOwlbear',
|
||||||
|
name: 'Haunted Owlbear',
|
||||||
|
username: 'MightyOwlbear@mastodon.social',
|
||||||
|
image:
|
||||||
|
'https://files.mastodon.social/accounts/avatars/107/246/961/007/605/352/original/a86fc3db97a6de04.jpg',
|
||||||
|
timestamp: '2022-04-14T13:33:03Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://gamethattune.club/users/thelinkfloyd',
|
||||||
|
name: 'thelinkfloyd',
|
||||||
|
username: 'thelinkfloyd@gamethattune.club',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-04-05T12:23:32Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://gamethattune.club/users/TheBaffler',
|
||||||
|
name: 'TheBaffler',
|
||||||
|
username: 'TheBaffler@gamethattune.club',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-04-04T19:50:08Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://gamethattune.club/users/Gttjessie',
|
||||||
|
name: 'Gttjessie',
|
||||||
|
username: 'Gttjessie@gamethattune.club',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-03-30T20:18:47Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://cybre.space/users/fractal',
|
||||||
|
name: 'Le fractal',
|
||||||
|
username: 'fractal@cybre.space',
|
||||||
|
image:
|
||||||
|
'https://cybre.ams3.digitaloceanspaces.com/accounts/avatars/000/405/126/original/f1f2832a7bf1a967.png',
|
||||||
|
timestamp: '2022-03-30T19:46:17Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://fosstodon.org/users/jumboshrimp',
|
||||||
|
name: 'alex 👑🦐',
|
||||||
|
username: 'jumboshrimp@fosstodon.org',
|
||||||
|
image: 'https://cdn.fosstodon.org/accounts/avatars/000/320/316/original/de43cda8653ade7f.jpg',
|
||||||
|
timestamp: '2022-03-30T18:09:54Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://gamethattune.club/users/nvrslep303',
|
||||||
|
name: 'Tay',
|
||||||
|
username: 'nvrslep303@gamethattune.club',
|
||||||
|
image: 'https://gamethattune.club/media/5cf9bc27-8821-445a-86ce-8aa3704acf2d/pfp.jpg',
|
||||||
|
timestamp: '2022-03-30T15:27:49Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://gamethattune.club/users/anKerrigan',
|
||||||
|
name: 'anKerrigan',
|
||||||
|
username: 'anKerrigan@gamethattune.club',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-03-30T14:47:04Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://gamethattune.club/users/jgangsta187',
|
||||||
|
name: 'jgangsta187',
|
||||||
|
username: 'jgangsta187@gamethattune.club',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-03-30T14:42:52Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://gamethattune.club/users/aekre',
|
||||||
|
name: 'aekre',
|
||||||
|
username: 'aekre@gamethattune.club',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-03-30T14:41:32Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://gamethattune.club/users/mork',
|
||||||
|
name: 'mork',
|
||||||
|
username: 'mork@gamethattune.club',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-03-30T14:37:10Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://fosstodon.org/users/owncast',
|
||||||
|
name: 'Owncast',
|
||||||
|
username: 'owncast@fosstodon.org',
|
||||||
|
image:
|
||||||
|
'https://cdn.fosstodon.org/accounts/avatars/107/017/218/425/829/465/original/f98ba4cd61f483ab.png',
|
||||||
|
timestamp: '2022-03-29T21:38:02Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://cybre.space/users/wklew',
|
||||||
|
name: 'wally',
|
||||||
|
username: 'wklew@cybre.space',
|
||||||
|
image:
|
||||||
|
'https://cybre.ams3.digitaloceanspaces.com/accounts/avatars/000/308/727/original/7453e74f3e09b27b.jpg',
|
||||||
|
timestamp: '2022-03-29T18:24:29Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://mastodon.social/users/nvrslep303',
|
||||||
|
name: 'Tay',
|
||||||
|
username: 'nvrslep303@mastodon.social',
|
||||||
|
image:
|
||||||
|
'https://files.mastodon.social/accounts/avatars/108/041/196/166/285/851/original/fc444dd6096381af.jpg',
|
||||||
|
timestamp: '2022-03-29T18:19:31Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://mastodon.social/users/morky',
|
||||||
|
name: '',
|
||||||
|
username: 'morky@mastodon.social',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-03-29T18:17:59Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://mastodon.social/users/jgangsta187',
|
||||||
|
name: 'John H.',
|
||||||
|
username: 'jgangsta187@mastodon.social',
|
||||||
|
image: '',
|
||||||
|
timestamp: '2022-03-29T18:15:48Z',
|
||||||
|
disabledAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
link: 'https://fosstodon.org/users/meisam',
|
||||||
|
name: 'Meisam 🇪🇺:archlinux:',
|
||||||
|
username: 'meisam@fosstodon.org',
|
||||||
|
image: 'https://cdn.fosstodon.org/accounts/avatars/000/264/096/original/54b4e6db97206bda.jpg',
|
||||||
|
timestamp: '2022-03-29T18:12:21Z',
|
||||||
|
disabledAt: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +1,32 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
import UserDropdownMenu from '../components/UserDropdownMenu';
|
import UserDropdownMenu from '../components/UserDropdownMenu';
|
||||||
|
import { ChatState } from '../interfaces/application-state';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'owncast/User settings dropdown menu',
|
title: 'owncast/User settings menu',
|
||||||
component: UserDropdownMenu,
|
component: UserDropdownMenu,
|
||||||
parameters: {},
|
parameters: {},
|
||||||
} as ComponentMeta<typeof UserDropdownMenu>;
|
} as ComponentMeta<typeof UserDropdownMenu>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// This component uses Recoil internally so wrap it in a RecoilRoot.
|
||||||
const Template: ComponentStory<typeof UserDropdownMenu> = args => <UserDropdownMenu />;
|
const Example = args => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<UserDropdownMenu {...args} />
|
||||||
|
</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
const Template: ComponentStory<typeof UserDropdownMenu> = args => <Example {...args} />;
|
||||||
export const Example = Template.bind({});
|
|
||||||
|
export const ChatEnabled = Template.bind({});
|
||||||
|
ChatEnabled.args = {
|
||||||
|
username: 'test-user',
|
||||||
|
chatState: ChatState.Available,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChatDisabled = Template.bind({});
|
||||||
|
ChatDisabled.args = {
|
||||||
|
username: 'test-user',
|
||||||
|
chatState: ChatState.NotAvailable,
|
||||||
|
};
|
||||||
|
|||||||
@ -12,9 +12,9 @@ export const URL_BAN_USER = `/api/chat/users/setenabled`;
|
|||||||
|
|
||||||
// TODO: This directory is customizable in the config. So we should expose this via the config API.
|
// TODO: This directory is customizable in the config. So we should expose this via the config API.
|
||||||
export const URL_STREAM = `/hls/stream.m3u8`;
|
export const URL_STREAM = `/hls/stream.m3u8`;
|
||||||
export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${
|
// export const URL_WEBSOCKET = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${
|
||||||
location.host
|
// location.host
|
||||||
}/ws`;
|
// }/ws`;
|
||||||
export const URL_CHAT_REGISTRATION = `/api/chat/register`;
|
export const URL_CHAT_REGISTRATION = `/api/chat/register`;
|
||||||
export const URL_FOLLOWERS = `/api/followers`;
|
export const URL_FOLLOWERS = `/api/followers`;
|
||||||
export const URL_PLAYBACK_METRICS = `/api/metrics/playback`;
|
export const URL_PLAYBACK_METRICS = `/api/metrics/playback`;
|
||||||
|
|||||||
Reference in New Issue
Block a user