From 4997c7c5ac09ba03ee51752eb46ebcf12ee8a5cc Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 29 Apr 2022 15:09:53 -0700 Subject: [PATCH] Fill out some more components + add application state enums --- web/components/UserDropdownMenu.tsx | 45 +++- web/components/chat/ChatContainer.tsx | 14 +- web/components/chat/ChatTextField.tsx | 32 ++- web/components/chat/ChatUserMessage.tsx | 15 +- web/components/stores/ClientConfigStore.tsx | 54 +++- web/components/stores/ServerStatusStore.tsx | 3 +- web/components/ui/Sidebar/Sidebar.tsx | 6 +- web/interfaces/application-state.ts | 17 ++ web/interfaces/chat-message.model.ts | 10 +- web/interfaces/user.ts | 9 + .../{user-service.ts => chat-service.ts} | 14 +- web/stories/ChatContainer.stories.tsx | 33 ++- web/stories/ChatTextField.stories.tsx | 10 +- web/stories/ChatUserMessage.stories.tsx | 75 +++++- web/stories/Dropdown.stories.tsx | 37 --- web/stories/Followercollection.stories.tsx | 231 +++++++++++++++--- web/stories/UserDropdownMenu.stories.tsx | 27 +- web/utils/constants.js | 6 +- 18 files changed, 522 insertions(+), 116 deletions(-) create mode 100644 web/interfaces/application-state.ts create mode 100644 web/interfaces/user.ts rename web/services/{user-service.ts => chat-service.ts} (54%) delete mode 100644 web/stories/Dropdown.stories.tsx diff --git a/web/components/UserDropdownMenu.tsx b/web/components/UserDropdownMenu.tsx index 36e5bc548..c4b8d8c1c 100644 --- a/web/components/UserDropdownMenu.tsx +++ b/web/components/UserDropdownMenu.tsx @@ -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) { - return
User settings dropdown component goes here
; + const { username, chatState } = props; + + const chatEnabled = chatState !== ChatState.NotAvailable; + const [chatVisibility, setChatVisibility] = + useRecoilState(chatVisibilityAtom); + + const toggleChatVisibility = () => { + if (chatVisibility === ChatVisibilityState.Hidden) { + setChatVisibility(ChatVisibilityState.Visible); + } else { + setChatVisibility(ChatVisibilityState.Hidden); + } + }; + + const menu = ( + + Change name + Authenticate + {chatEnabled && ( + toggleChatVisibility()}> + Toggle chat + + )} + + ); + + return ( + + + + ); } diff --git a/web/components/chat/ChatContainer.tsx b/web/components/chat/ChatContainer.tsx index e6a484d70..77ff5e3fe 100644 --- a/web/components/chat/ChatContainer.tsx +++ b/web/components/chat/ChatContainer.tsx @@ -1,9 +1,21 @@ +import { Spin } from 'antd'; import { ChatMessage } from '../../interfaces/chat-message.model'; +import { ChatState } from '../../interfaces/application-state'; interface Props { messages: ChatMessage[]; + state: ChatState; } export default function ChatContainer(props: Props) { - return
Chat container goes here
; + const { messages, state } = props; + const loading = state === ChatState.Loading; + + return ( +
+ + Chat container with scrolling chat messages go here + +
+ ); } diff --git a/web/components/chat/ChatTextField.tsx b/web/components/chat/ChatTextField.tsx index 00d908bc6..b4cbf31dd 100644 --- a/web/components/chat/ChatTextField.tsx +++ b/web/components/chat/ChatTextField.tsx @@ -1,5 +1,35 @@ +import { useState } from 'react'; + interface Props {} export default function ChatTextField(props: Props) { - return
Component goes here
; + const [value, setValue] = useState(''); + const [showEmojis, setShowEmojis] = useState(false); + + return ( +
+ setValue(e.target.value)} + placeholder="Type a message here then hit ENTER" + /> + +
+ ); } diff --git a/web/components/chat/ChatUserMessage.tsx b/web/components/chat/ChatUserMessage.tsx index e51053293..95fa77445 100644 --- a/web/components/chat/ChatUserMessage.tsx +++ b/web/components/chat/ChatUserMessage.tsx @@ -2,8 +2,21 @@ import { ChatMessage } from '../../interfaces/chat-message.model'; interface Props { message: ChatMessage; + showModeratorMenu: boolean; } export default function ChatUserMessage(props: Props) { - return
Component goes here
; + const { message, showModeratorMenu } = props; + const { body, user, timestamp } = message; + const { displayName, displayColor } = user; + + // TODO: Convert displayColor (a hue) to a usable color. + + return ( +
+
{displayName}
+
{body}
+ {showModeratorMenu &&
Moderator menu
} +
+ ); } diff --git a/web/components/stores/ClientConfigStore.tsx b/web/components/stores/ClientConfigStore.tsx index 18fff4fb2..e0ff46b8e 100644 --- a/web/components/stores/ClientConfigStore.tsx +++ b/web/components/stores/ClientConfigStore.tsx @@ -1,9 +1,11 @@ import { useEffect } from 'react'; -import { ReactElement } from 'react-markdown/lib/react-markdown'; import { atom, useRecoilState } from 'recoil'; import { makeEmptyClientConfig, ClientConfig } from '../../interfaces/client-config.model'; import ClientConfigService from '../../services/client-config-service'; +import ChatService from '../../services/chat-service'; 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. export const clientConfigState = atom({ @@ -11,14 +13,19 @@ export const clientConfigState = atom({ default: makeEmptyClientConfig(), }); -export const chatCurrentlyVisible = atom({ - key: 'chatvisible', - default: false, +export const chatVisibility = atom({ + key: 'chatVisibility', + default: ChatVisibilityState.Hidden, }); export const chatDisplayName = atom({ key: 'chatDisplayName', - default: '', + default: null, +}); + +export const accessTokenAtom = atom({ + key: 'accessToken', + default: null, }); export const chatMessages = atom({ @@ -26,8 +33,11 @@ export const chatMessages = atom({ default: [] as ChatMessage[], }); -export function ClientConfigStore(): ReactElement { +export function ClientConfigStore() { const [, setClientConfig] = useRecoilState(clientConfigState); + const [, setChatMessages] = useRecoilState(chatMessages); + const [accessToken, setAccessToken] = useRecoilState(accessTokenAtom); + const [, setChatDisplayName] = useRecoilState(chatDisplayName); const updateClientConfig = async () => { 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(() => { updateClientConfig(); + handleUserRegistration(); }, []); + useEffect(() => { + getChatHistory(); + }, [accessToken]); + return null; } diff --git a/web/components/stores/ServerStatusStore.tsx b/web/components/stores/ServerStatusStore.tsx index 9db567423..93cf493f8 100644 --- a/web/components/stores/ServerStatusStore.tsx +++ b/web/components/stores/ServerStatusStore.tsx @@ -1,5 +1,4 @@ import { useEffect } from 'react'; -import { ReactElement } from 'react-markdown/lib/react-markdown'; import { atom, useRecoilState } from 'recoil'; import { ServerStatus, makeEmptyServerStatus } from '../../interfaces/server-status.model'; import ServerStatusService from '../../services/status-service'; @@ -9,7 +8,7 @@ export const serverStatusState = atom({ default: makeEmptyServerStatus(), }); -export function ServerStatusStore(): ReactElement { +export function ServerStatusStore() { const [, setServerStatus] = useRecoilState(serverStatusState); const updateServerStatus = async () => { diff --git a/web/components/ui/Sidebar/Sidebar.tsx b/web/components/ui/Sidebar/Sidebar.tsx index 2071b6913..1f5b5256d 100644 --- a/web/components/ui/Sidebar/Sidebar.tsx +++ b/web/components/ui/Sidebar/Sidebar.tsx @@ -2,14 +2,16 @@ import Sider from 'antd/lib/layout/Sider'; import { useRecoilValue } from 'recoil'; import { ChatMessage } from '../../../interfaces/chat-message.model'; 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() { const messages = useRecoilValue(chatMessages); + const chatVisibility = useRecoilValue(chatVisibilityAtom); return (