diff --git a/src/components/Chat/Sidebar/Users/Users.tsx b/src/components/Chat/Sidebar/Users/Users.tsx index 8b79d7c..a908aeb 100644 --- a/src/components/Chat/Sidebar/Users/Users.tsx +++ b/src/components/Chat/Sidebar/Users/Users.tsx @@ -1,39 +1,53 @@ import React from 'react'; -import { Box, Stack, Text } from '@chakra-ui/react'; +import { observer } from 'mobx-react'; +import { Box, Stack, StackProps, Text } from '@chakra-ui/react'; import { User } from './User'; import { useChatContext } from '../../context'; +import { useRootStore } from '@educt/hooks/useRootStore'; +import { Skeleton } from '@chakra-ui/react'; -const tempDb = [ - { id: 'user-1', name: 'Trace Kerluke', lastMessage: 'Hello.', time: '2g' }, - { id: 'user-2', name: 'Dalton Conn ', lastMessage: 'Hello, how you doing ?', time: '30m' }, - { id: 'user-3', name: 'Cleo Hane', lastMessage: 'Hi.', time: '1h' }, -]; +const Wrapper: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; -const Users: React.FC = () => { +const Users: React.FC = observer(() => { + const { onlineStore } = useRootStore(); const { search } = useChatContext(); + const { users } = onlineStore; - const filtered = !search ? tempDb : tempDb.filter(u => u.name.toLocaleLowerCase().includes(search)); + if (users === null) + return ( + + + + + ); + + const filtered = !search ? users : users.filter(u => u.userName.toLocaleLowerCase().includes(search)); const isEmpty = filtered.length === 0; return ( - + {!isEmpty ? ( filtered.map(u => ( - - - - + + + + )) ) : ( - Cannot find any chat + Cannot find any users )} - + ); -}; - +}); export { Users }; diff --git a/src/components/Chat/Window/Message/Message.tsx b/src/components/Chat/Window/Message/Message.tsx index 75b66e5..cd4d5de 100644 --- a/src/components/Chat/Window/Message/Message.tsx +++ b/src/components/Chat/Window/Message/Message.tsx @@ -1,8 +1,9 @@ -import React from 'react'; -import { Flex } from '@chakra-ui/react'; +import React, { useEffect, useRef } from 'react'; +import { Flex, Stack } from '@chakra-ui/react'; import { Avatar, IAvatarProps } from './Avatar'; import { Text, ITextProps } from './Text'; import { MessageContext } from './context'; +import { MessageType } from '@educt/types'; interface IMessageComposition { Avatar: React.FC; @@ -13,6 +14,22 @@ interface IMyMessageProps { isMyMessage: boolean; } +const MessageList: React.FC<{ messages: MessageType[] }> = ({ messages, children }) => { + const listRef = useRef(null); + + useEffect(() => { + if (listRef.current) { + listRef.current.scrollTo(0, listRef.current.scrollHeight); + } + }, [messages]); + + return ( + + {children} + + ); +}; + const Message: React.FC & IMessageComposition = ({ children, isMyMessage }) => { return ( @@ -26,4 +43,4 @@ const Message: React.FC & IMessageComposition = ({ children, is Message.Avatar = Avatar; Message.Text = Text; -export { Message }; +export { MessageList, Message }; diff --git a/src/components/Chat/Window/Message/index.ts b/src/components/Chat/Window/Message/index.ts index f2d89d2..cd58e02 100644 --- a/src/components/Chat/Window/Message/index.ts +++ b/src/components/Chat/Window/Message/index.ts @@ -1 +1 @@ -export { Message } from './Message'; +export * from './Message'; diff --git a/src/components/Chat/Window/Window.tsx b/src/components/Chat/Window/Window.tsx index 2f267fc..c1c237e 100644 --- a/src/components/Chat/Window/Window.tsx +++ b/src/components/Chat/Window/Window.tsx @@ -1,53 +1,131 @@ -import React, { useEffect } from 'react'; -import { GridItem, GridItemProps, Flex, Box, Icon, Text, Input, Heading, IconButton, Stack } from '@chakra-ui/react'; +import React, { ChangeEvent, useContext, useEffect, useState } from 'react'; +import { GridItem, GridItemProps, Flex, Box, Icon, Text, Input, Heading, IconButton, Divider } from '@chakra-ui/react'; import { MdOutlineChat, MdSend } from 'react-icons/md'; -import { Message } from './Message'; +import { Message, MessageList } from './Message'; +import { useRootStore } from '@educt/hooks/useRootStore'; +import { SocketContext } from '@educt/contexts'; +import { useSocketEvent } from '@educt/hooks/useSocketEvent'; +import { MessageType } from '@educt/types'; const Window: React.FC = props => { + const { + userStore: { me }, + onlineStore, + } = useRootStore(); const chatId = new URLSearchParams(location.search).get('chat_id'); + const [inputValue, setInputValue] = useState(''); + const [messages, setMessages] = useState([]); + const { socket } = useContext(SocketContext); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (inputValue === '' || me === null) return; + + const message: MessageType = { + userName: me.fullname, + content: inputValue, + time: new Date().toLocaleTimeString(), + isMyMessage: true, + }; + + /** + * Emit event to server and update messages state + */ + if (chatId) { + socket?.emit('message:private', { + content: message.content, + to: chatId, + }); + setMessages(prev => [...prev, message]); + setInputValue(''); + } + }; + + const onChangeInputValue = (e: ChangeEvent) => setInputValue(e.target.value); useEffect(() => { + // TODO load message history console.log('chat id changed!'); + setMessages([]); }, [chatId]); + // TODO add types for this event + useSocketEvent('message:private', data => { + const user = onlineStore.getUser(data.from); + console.log('message!'); + + if (user) { + if (chatId === data.from) { + /** + * Do not add duplicate of message + */ + if (data.to === data.from) return; + + const message: MessageType = { + content: data.content, + userName: user.userName, + isMyMessage: false, + time: new Date().toLocaleTimeString(), + }; + + setMessages(prev => [...prev, message]); + } + } + }); + return ( {chatId ? ( - John Doe + {chatId} - - - - - - - - - - - - - } - aria-label='Send message' - variant={'ghost'} - _hover={{ bg: 'inherit' }} - _active={{ bg: 'inherit' }} - _focus={{ outline: 'none' }} - /> - + + + + + {messages.length !== 0 ? ( + messages.map((message, i) => ( + + + + + )) + ) : ( + + + Your message history will be displayed here. + + + )} + +
onSubmit(e)}> + + + } + aria-label='Send message' + variant={'ghost'} + _hover={{ bg: 'inherit' }} + _active={{ bg: 'inherit' }} + _focus={{ outline: 'none' }} + /> + +
) : ( diff --git a/src/hooks/useSocketEvent.ts b/src/hooks/useSocketEvent.ts index 272597b..0a81121 100644 --- a/src/hooks/useSocketEvent.ts +++ b/src/hooks/useSocketEvent.ts @@ -1,7 +1,7 @@ import { useContext, useEffect } from 'react'; import { SocketContext } from '@educt/contexts'; -export const useSocketEvent = (event: string, callback: (...args: any) => void) => { +export const useSocketEvent = (event: string, callback: (...args: unknown[]) => void) => { const { socket } = useContext(SocketContext); useEffect(() => { diff --git a/src/pages/messages/messages.tsx b/src/pages/messages/messages.tsx index fc1d8d7..b2ca915 100644 --- a/src/pages/messages/messages.tsx +++ b/src/pages/messages/messages.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Page } from '@educt/components/PageElements'; import { IPageProps } from '@educt/interfaces'; import { Chat } from '@educt/components/Chat'; +import { observer } from 'mobx-react'; /** * Messages page @@ -22,4 +23,4 @@ const MessagesPage: React.FC = () => { ); }; -export default MessagesPage; +export default observer(MessagesPage); diff --git a/src/stores/OnlineStore.ts b/src/stores/OnlineStore.ts index 1aab713..8ba68d0 100644 --- a/src/stores/OnlineStore.ts +++ b/src/stores/OnlineStore.ts @@ -1,27 +1,39 @@ import { makeAutoObservable } from 'mobx'; import RootStore from './RootStore'; +type UserType = { + userId: string; + userName: string; +}; + export default class OnlineStore { public root: RootStore; - public online: Set; + public online: Map | null = null; constructor(root: RootStore) { this.root = root; - this.online = new Set(); makeAutoObservable(this); } - public loadOnline(online: string[]) { - this.online = new Set(online); + public loadOnline(online: [string, UserType][]) { + this.online = new Map(online); } public isOnline(id: string) { - return this.online.has(id); + return this.online && this.online.has(id); + } + + public getUser(id: string) { + return this.online && this.online.get(id); } public get count() { - return this.online.size; + return this.online && this.online.size; + } + + public get users() { + return this.online && Array.from(this.online).map(entry => entry[1]); } } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index e58f1cf..e673964 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -62,6 +62,13 @@ export type ConfirmEmailContainerDataType = { expired_seconds: number; }; +export type MessageType = { + userName: string; + content: string; + time: string; + isMyMessage: boolean; +}; + /** * Change email page context */