mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 02:09:08 +03:00
feat: render messages
This commit is contained in:
parent
464f58363b
commit
b60b330fe9
@ -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 };
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -1 +1 @@
|
||||
export { Message } from './Message';
|
||||
export * from './Message';
|
||||
|
||||
@ -1,36 +1,112 @@
|
||||
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' />
|
||||
|
||||
<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>
|
||||
<Message isMyMessage={true}>
|
||||
<Message.Avatar fullname='Sergey Yarkov' />
|
||||
<Message.Text content={'Lorem ipsum dolor sit amet consectetur.'} time='18:00' />
|
||||
</Message>
|
||||
</Stack>
|
||||
))
|
||||
) : (
|
||||
<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...'
|
||||
@ -40,6 +116,7 @@ const Window: React.FC<GridItemProps> = props => {
|
||||
mr='2'
|
||||
/>
|
||||
<IconButton
|
||||
type='submit'
|
||||
icon={<MdSend />}
|
||||
aria-label='Send message'
|
||||
variant={'ghost'}
|
||||
@ -48,6 +125,7 @@ const Window: React.FC<GridItemProps> = props => {
|
||||
_focus={{ outline: 'none' }}
|
||||
/>
|
||||
</Flex>
|
||||
</form>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box textAlign={'center'} mt={{ base: '0', lg: '15rem' }}>
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
7
src/types/index.d.ts
vendored
7
src/types/index.d.ts
vendored
@ -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
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user