feat: render messages

This commit is contained in:
Sergey Yarkov 2022-02-10 22:23:28 +03:00
parent 464f58363b
commit b60b330fe9
8 changed files with 191 additions and 62 deletions

View File

@ -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<StackProps> = ({ children }) => {
return (
<Stack mt='12' maxH='690px' overflowY={'scroll'} pr={{ base: '0', lg: '3' }}>
{children}
</Stack>
);
};
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 (
<Wrapper>
<Skeleton h='90px' borderRadius={'lg'} />
<Skeleton h='90px' borderRadius={'lg'} />
</Wrapper>
);
const filtered = !search ? users : users.filter(u => u.userName.toLocaleLowerCase().includes(search));
const isEmpty = filtered.length === 0;
return (
<Stack mt='12' maxH='690px' overflowY={'scroll'} pr={{ base: '0', lg: '3' }}>
<Wrapper>
{!isEmpty ? (
filtered.map(u => (
<User key={u.id} id={u.id}>
<User.Avatar isOnline={false} fullname={u.name} />
<User.Info fullname={u.name} lastMessage={u.lastMessage} />
<User.Time time={u.time} />
<User key={u.userId} id={u.userId}>
<User.Avatar isOnline={onlineStore.isOnline(u.userId) || false} fullname={u.userName} />
<User.Info fullname={u.userName} lastMessage={''} />
<User.Time time={'2h'} />
</User>
))
) : (
<Box textAlign={'center'}>
<Text userSelect={'none'} color='gray.500'>
Cannot find any chat
Cannot find any users
</Text>
</Box>
)}
</Stack>
</Wrapper>
);
};
});
export { Users };

View File

@ -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<IAvatarProps>;
@ -13,6 +14,22 @@ interface IMyMessageProps {
isMyMessage: boolean;
}
const MessageList: React.FC<{ messages: MessageType[] }> = ({ messages, children }) => {
const listRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (listRef.current) {
listRef.current.scrollTo(0, listRef.current.scrollHeight);
}
}, [messages]);
return (
<Stack ref={listRef} h='65vh' overflowY={'scroll'} mb='12' mt='6' spacing={'3'} pr='8'>
{children}
</Stack>
);
};
const Message: React.FC<IMyMessageProps> & IMessageComposition = ({ children, isMyMessage }) => {
return (
<MessageContext.Provider value={{ isMyMessage }}>
@ -26,4 +43,4 @@ const Message: React.FC<IMyMessageProps> & IMessageComposition = ({ children, is
Message.Avatar = Avatar;
Message.Text = Text;
export { Message };
export { MessageList, Message };

View File

@ -1 +1 @@
export { Message } from './Message';
export * from './Message';

View File

@ -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<GridItemProps> = props => {
const {
userStore: { me },
onlineStore,
} = useRootStore();
const chatId = new URLSearchParams(location.search).get('chat_id');
const [inputValue, setInputValue] = useState<string>('');
const [messages, setMessages] = useState<MessageType[]>([]);
const { socket } = useContext(SocketContext);
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
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<HTMLInputElement>) => 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 (
<GridItem pt={'7rem'} pb='5' px='12' {...props}>
{chatId ? (
<Flex flexDir={'column'}>
<Box my='3'>
<Heading as='h2' fontWeight={'semibold'} fontSize='3xl'>
John Doe
{chatId}
</Heading>
</Box>
<Stack h='65vh' overflowY={'scroll'} mb='12' mt='6' spacing={'3'} pr='8'>
<Message isMyMessage={false}>
<Message.Avatar fullname='John Doe' />
<Message.Text content={'Lorem ipsum dolor sit amet consectetur adipisicing elit.'} time='18:00' />
</Message>
<Message isMyMessage={true}>
<Message.Avatar fullname='Sergey Yarkov' />
<Message.Text content={'Lorem ipsum dolor sit amet consectetur.'} time='18:00' />
</Message>
</Stack>
<Flex mt='auto' alignItems={'flex-end'}>
<Input
resize={'block'}
type='text'
placeholder='Write a message...'
maxW='900px'
variant={'outline'}
rounded='xl'
mr='2'
/>
<IconButton
icon={<MdSend />}
aria-label='Send message'
variant={'ghost'}
_hover={{ bg: 'inherit' }}
_active={{ bg: 'inherit' }}
_focus={{ outline: 'none' }}
/>
</Flex>
<Divider my='2' />
<MessageList messages={messages}>
{messages.length !== 0 ? (
messages.map((message, i) => (
<Message key={i} isMyMessage={message.isMyMessage}>
<Message.Avatar fullname={message.userName} />
<Message.Text content={message.content} time={message.time} />
</Message>
))
) : (
<Box>
<Text mt='5' textAlign={'center'} color='gray.500' userSelect={'none'}>
Your message history will be displayed here.
</Text>
</Box>
)}
</MessageList>
<form onSubmit={e => onSubmit(e)}>
<Flex mt='auto' alignItems={'flex-end'}>
<Input
onChange={onChangeInputValue}
value={inputValue}
resize={'block'}
type='text'
placeholder='Write a message...'
maxW='900px'
variant={'outline'}
rounded='xl'
mr='2'
/>
<IconButton
type='submit'
icon={<MdSend />}
aria-label='Send message'
variant={'ghost'}
_hover={{ bg: 'inherit' }}
_active={{ bg: 'inherit' }}
_focus={{ outline: 'none' }}
/>
</Flex>
</form>
</Flex>
) : (
<Box textAlign={'center'} mt={{ base: '0', lg: '15rem' }}>

View File

@ -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(() => {

View File

@ -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<IPageProps> = () => {
);
};
export default MessagesPage;
export default observer(MessagesPage);

View File

@ -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<string>;
public online: Map<string, UserType> | 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]);
}
}

View File

@ -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
*/