mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: render messages
This commit is contained in:
parent
464f58363b
commit
b60b330fe9
@ -1,39 +1,53 @@
|
|||||||
import React from 'react';
|
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 { User } from './User';
|
||||||
import { useChatContext } from '../../context';
|
import { useChatContext } from '../../context';
|
||||||
|
import { useRootStore } from '@educt/hooks/useRootStore';
|
||||||
|
import { Skeleton } from '@chakra-ui/react';
|
||||||
|
|
||||||
const tempDb = [
|
const Wrapper: React.FC<StackProps> = ({ children }) => {
|
||||||
{ id: 'user-1', name: 'Trace Kerluke', lastMessage: 'Hello.', time: '2g' },
|
return (
|
||||||
{ id: 'user-2', name: 'Dalton Conn ', lastMessage: 'Hello, how you doing ?', time: '30m' },
|
<Stack mt='12' maxH='690px' overflowY={'scroll'} pr={{ base: '0', lg: '3' }}>
|
||||||
{ id: 'user-3', name: 'Cleo Hane', lastMessage: 'Hi.', time: '1h' },
|
{children}
|
||||||
];
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Users: React.FC = () => {
|
const Users: React.FC = observer(() => {
|
||||||
|
const { onlineStore } = useRootStore();
|
||||||
const { search } = useChatContext();
|
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;
|
const isEmpty = filtered.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack mt='12' maxH='690px' overflowY={'scroll'} pr={{ base: '0', lg: '3' }}>
|
<Wrapper>
|
||||||
{!isEmpty ? (
|
{!isEmpty ? (
|
||||||
filtered.map(u => (
|
filtered.map(u => (
|
||||||
<User key={u.id} id={u.id}>
|
<User key={u.userId} id={u.userId}>
|
||||||
<User.Avatar isOnline={false} fullname={u.name} />
|
<User.Avatar isOnline={onlineStore.isOnline(u.userId) || false} fullname={u.userName} />
|
||||||
<User.Info fullname={u.name} lastMessage={u.lastMessage} />
|
<User.Info fullname={u.userName} lastMessage={''} />
|
||||||
<User.Time time={u.time} />
|
<User.Time time={'2h'} />
|
||||||
</User>
|
</User>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<Box textAlign={'center'}>
|
<Box textAlign={'center'}>
|
||||||
<Text userSelect={'none'} color='gray.500'>
|
<Text userSelect={'none'} color='gray.500'>
|
||||||
Cannot find any chat
|
Cannot find any users
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export { Users };
|
export { Users };
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex, Stack } from '@chakra-ui/react';
|
||||||
import { Avatar, IAvatarProps } from './Avatar';
|
import { Avatar, IAvatarProps } from './Avatar';
|
||||||
import { Text, ITextProps } from './Text';
|
import { Text, ITextProps } from './Text';
|
||||||
import { MessageContext } from './context';
|
import { MessageContext } from './context';
|
||||||
|
import { MessageType } from '@educt/types';
|
||||||
|
|
||||||
interface IMessageComposition {
|
interface IMessageComposition {
|
||||||
Avatar: React.FC<IAvatarProps>;
|
Avatar: React.FC<IAvatarProps>;
|
||||||
@ -13,6 +14,22 @@ interface IMyMessageProps {
|
|||||||
isMyMessage: boolean;
|
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 }) => {
|
const Message: React.FC<IMyMessageProps> & IMessageComposition = ({ children, isMyMessage }) => {
|
||||||
return (
|
return (
|
||||||
<MessageContext.Provider value={{ isMyMessage }}>
|
<MessageContext.Provider value={{ isMyMessage }}>
|
||||||
@ -26,4 +43,4 @@ const Message: React.FC<IMyMessageProps> & IMessageComposition = ({ children, is
|
|||||||
Message.Avatar = Avatar;
|
Message.Avatar = Avatar;
|
||||||
Message.Text = Text;
|
Message.Text = Text;
|
||||||
|
|
||||||
export { Message };
|
export { MessageList, Message };
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { Message } from './Message';
|
export * from './Message';
|
||||||
|
|||||||
@ -1,53 +1,131 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { ChangeEvent, useContext, useEffect, useState } from 'react';
|
||||||
import { GridItem, GridItemProps, Flex, Box, Icon, Text, Input, Heading, IconButton, Stack } from '@chakra-ui/react';
|
import { GridItem, GridItemProps, Flex, Box, Icon, Text, Input, Heading, IconButton, Divider } from '@chakra-ui/react';
|
||||||
import { MdOutlineChat, MdSend } from 'react-icons/md';
|
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 Window: React.FC<GridItemProps> = props => {
|
||||||
|
const {
|
||||||
|
userStore: { me },
|
||||||
|
onlineStore,
|
||||||
|
} = useRootStore();
|
||||||
const chatId = new URLSearchParams(location.search).get('chat_id');
|
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(() => {
|
useEffect(() => {
|
||||||
|
// TODO load message history
|
||||||
console.log('chat id changed!');
|
console.log('chat id changed!');
|
||||||
|
setMessages([]);
|
||||||
}, [chatId]);
|
}, [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 (
|
return (
|
||||||
<GridItem pt={'7rem'} pb='5' px='12' {...props}>
|
<GridItem pt={'7rem'} pb='5' px='12' {...props}>
|
||||||
{chatId ? (
|
{chatId ? (
|
||||||
<Flex flexDir={'column'}>
|
<Flex flexDir={'column'}>
|
||||||
<Box my='3'>
|
<Box my='3'>
|
||||||
<Heading as='h2' fontWeight={'semibold'} fontSize='3xl'>
|
<Heading as='h2' fontWeight={'semibold'} fontSize='3xl'>
|
||||||
John Doe
|
{chatId}
|
||||||
</Heading>
|
</Heading>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack h='65vh' overflowY={'scroll'} mb='12' mt='6' spacing={'3'} pr='8'>
|
|
||||||
<Message isMyMessage={false}>
|
<Divider my='2' />
|
||||||
<Message.Avatar fullname='John Doe' />
|
|
||||||
<Message.Text content={'Lorem ipsum dolor sit amet consectetur adipisicing elit.'} time='18:00' />
|
<MessageList messages={messages}>
|
||||||
</Message>
|
{messages.length !== 0 ? (
|
||||||
<Message isMyMessage={true}>
|
messages.map((message, i) => (
|
||||||
<Message.Avatar fullname='Sergey Yarkov' />
|
<Message key={i} isMyMessage={message.isMyMessage}>
|
||||||
<Message.Text content={'Lorem ipsum dolor sit amet consectetur.'} time='18:00' />
|
<Message.Avatar fullname={message.userName} />
|
||||||
</Message>
|
<Message.Text content={message.content} time={message.time} />
|
||||||
</Stack>
|
</Message>
|
||||||
<Flex mt='auto' alignItems={'flex-end'}>
|
))
|
||||||
<Input
|
) : (
|
||||||
resize={'block'}
|
<Box>
|
||||||
type='text'
|
<Text mt='5' textAlign={'center'} color='gray.500' userSelect={'none'}>
|
||||||
placeholder='Write a message...'
|
Your message history will be displayed here.
|
||||||
maxW='900px'
|
</Text>
|
||||||
variant={'outline'}
|
</Box>
|
||||||
rounded='xl'
|
)}
|
||||||
mr='2'
|
</MessageList>
|
||||||
/>
|
<form onSubmit={e => onSubmit(e)}>
|
||||||
<IconButton
|
<Flex mt='auto' alignItems={'flex-end'}>
|
||||||
icon={<MdSend />}
|
<Input
|
||||||
aria-label='Send message'
|
onChange={onChangeInputValue}
|
||||||
variant={'ghost'}
|
value={inputValue}
|
||||||
_hover={{ bg: 'inherit' }}
|
resize={'block'}
|
||||||
_active={{ bg: 'inherit' }}
|
type='text'
|
||||||
_focus={{ outline: 'none' }}
|
placeholder='Write a message...'
|
||||||
/>
|
maxW='900px'
|
||||||
</Flex>
|
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>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<Box textAlign={'center'} mt={{ base: '0', lg: '15rem' }}>
|
<Box textAlign={'center'} mt={{ base: '0', lg: '15rem' }}>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import { SocketContext } from '@educt/contexts';
|
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);
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import { Page } from '@educt/components/PageElements';
|
import { Page } from '@educt/components/PageElements';
|
||||||
import { IPageProps } from '@educt/interfaces';
|
import { IPageProps } from '@educt/interfaces';
|
||||||
import { Chat } from '@educt/components/Chat';
|
import { Chat } from '@educt/components/Chat';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Messages page
|
* 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 { makeAutoObservable } from 'mobx';
|
||||||
import RootStore from './RootStore';
|
import RootStore from './RootStore';
|
||||||
|
|
||||||
|
type UserType = {
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default class OnlineStore {
|
export default class OnlineStore {
|
||||||
public root: RootStore;
|
public root: RootStore;
|
||||||
|
|
||||||
public online: Set<string>;
|
public online: Map<string, UserType> | null = null;
|
||||||
|
|
||||||
constructor(root: RootStore) {
|
constructor(root: RootStore) {
|
||||||
this.root = root;
|
this.root = root;
|
||||||
this.online = new Set();
|
|
||||||
|
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public loadOnline(online: string[]) {
|
public loadOnline(online: [string, UserType][]) {
|
||||||
this.online = new Set(online);
|
this.online = new Map(online);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isOnline(id: string) {
|
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() {
|
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;
|
expired_seconds: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MessageType = {
|
||||||
|
userName: string;
|
||||||
|
content: string;
|
||||||
|
time: string;
|
||||||
|
isMyMessage: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change email page context
|
* Change email page context
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user