feat: load chat history

This commit is contained in:
Sergey Yarkov 2022-02-12 22:25:02 +03:00
parent 5602c29b15
commit ce5411bd50
13 changed files with 84 additions and 74 deletions

View File

@ -1 +1,2 @@
export const BACKEND_URL = 'http://localhost:3333'; export const BACKEND_URL = 'http://localhost:3333';
export const WS_URL = 'ws://localhost:3333';

View File

@ -1,7 +1,8 @@
import { AxiosRequestConfig } from 'axios'; import { AxiosRequestConfig } from 'axios';
import * as constants from '../constants';
const apiConfig: AxiosRequestConfig = { const apiConfig: AxiosRequestConfig = {
baseURL: '/api/', baseURL: `${constants.BACKEND_URL}/api/`,
responseType: 'json', responseType: 'json',
withCredentials: true, withCredentials: true,
}; };

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { Avatar as ChakraAvatar, AvatarBadge, WrapItem } from '@chakra-ui/react'; import { Avatar as ChakraAvatar, AvatarBadge, WrapItem } from '@chakra-ui/react';
import { useRootStore } from '@educt/hooks/useRootStore';
import { MdOutlineBookmark } from 'react-icons/md';
export interface IAvatarProps { export interface IAvatarProps {
fullname: string; fullname: string;
@ -7,12 +9,21 @@ export interface IAvatarProps {
} }
const Avatar: React.FC<IAvatarProps> = ({ fullname: name, isOnline }) => { const Avatar: React.FC<IAvatarProps> = ({ fullname: name, isOnline }) => {
const {
userStore: { me },
} = useRootStore();
const isMe = me?.fullname === name;
const onlineBg = isOnline ? 'green.500' : 'red.500'; const onlineBg = isOnline ? 'green.500' : 'red.500';
return ( return (
<WrapItem> <WrapItem>
<ChakraAvatar name={name} size='md'> <ChakraAvatar
<AvatarBadge boxSize='0.90em' bg={onlineBg} /> name={isMe ? undefined : name}
bg={isMe ? 'blue.400' : undefined}
icon={<MdOutlineBookmark color='white' />}
size='md'
>
{!isMe && <AvatarBadge boxSize='0.90em' bg={onlineBg} />}
</ChakraAvatar> </ChakraAvatar>
</WrapItem> </WrapItem>
); );

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { Box, Text } from '@chakra-ui/react'; import { Box, Text } from '@chakra-ui/react';
import { useRootStore } from '@educt/hooks/useRootStore';
export interface IInfoProps { export interface IInfoProps {
fullname: string; fullname: string;
@ -7,10 +8,15 @@ export interface IInfoProps {
} }
const Info: React.FC<IInfoProps> = ({ fullname, lastMessage }) => { const Info: React.FC<IInfoProps> = ({ fullname, lastMessage }) => {
const {
userStore: { me },
} = useRootStore();
const isMe = me?.fullname === fullname;
return ( return (
<Box ml='4'> <Box ml='4'>
<Text fontWeight={'semibold'}> <Text fontWeight={'semibold'}>
{fullname} <br /> {isMe ? 'Favorites' : fullname} <br />
<Text as={'span'} fontWeight='normal' fontSize={'sm'} color='gray.500'> <Text as={'span'} fontWeight='normal' fontSize={'sm'} color='gray.500'>
{lastMessage} {lastMessage}
</Text> </Text>

View File

@ -38,6 +38,8 @@ const Window: React.FC = props => {
const history = useHistory(); const history = useHistory();
const toast = useToast(); const toast = useToast();
const isMe = me?.id === selectedUser?.id;
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
@ -54,7 +56,7 @@ const Window: React.FC = props => {
* Emit event to server and update messages state * Emit event to server and update messages state
*/ */
if (chatId) { if (chatId) {
socket?.emit('message:private', { socket?.emit('chat:message', {
content: message.content, content: message.content,
to: chatId, to: chatId,
}); });
@ -74,7 +76,22 @@ const Window: React.FC = props => {
try { try {
setIsLoading(true); setIsLoading(true);
const { data: user } = await UserServiceInstance.fetchUserById(chatId); const { data: user } = await UserServiceInstance.fetchUserById(chatId);
const {
data: { history },
} = await UserServiceInstance.fetchChatHistory(chatId);
// TODO add type for message
const messages = history.map(message => {
return {
isMyMessage: message.from === me.id,
content: message.content,
time: new Date(message.time).toLocaleTimeString(),
userName: message.from === me.id ? me.fullname : user.fullname,
};
});
setSelectedUser(user); setSelectedUser(user);
setMessages(messages);
} catch (error) { } catch (error) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
if (error.response?.status === 404) { if (error.response?.status === 404) {
@ -91,20 +108,11 @@ const Window: React.FC = props => {
} }
}, [chatId]); }, [chatId]);
// TODO load message history
/**
* Loading messages history
*/
useEffect(() => {
console.log('chat id changed!');
setMessages([]);
}, [chatId]);
// TODO add types for this event // TODO add types for this event
/** /**
* Event on accept new messages * Event on accept new messages
*/ */
useSocketEvent('message:private', data => { useSocketEvent('chat:message', data => {
const user = onlineStore.getUser(data.from); const user = onlineStore.getUser(data.from);
if (user) { if (user) {
@ -118,7 +126,7 @@ const Window: React.FC = props => {
content: data.content, content: data.content,
userName: user.userName, userName: user.userName,
isMyMessage: false, isMyMessage: false,
time: new Date().toLocaleTimeString(), time: new Date(data.time).toLocaleTimeString(),
}; };
setMessages(prev => [...prev, message]); setMessages(prev => [...prev, message]);
@ -155,7 +163,7 @@ const Window: React.FC = props => {
<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'>
{selectedUser.fullname} {isMe && me ? 'Favorites' : selectedUser.fullname}
</Heading> </Heading>
</Box> </Box>
<Divider my='2' /> <Divider my='2' />

View File

@ -47,11 +47,12 @@ const SettingsContextProvider: React.FC = ({ children }) => {
runInAction(() => { runInAction(() => {
if (me !== null) { if (me !== null) {
me.about = about === undefined ? me.about : about; me.about = about === undefined ? me.about : about;
if (me.contacts) { me.contacts = {
me.contacts.phone_number = phone_number === undefined ? me.contacts.phone_number : phone_number; phone_number: phone_number === undefined ? me.contacts?.phone_number || null : phone_number,
me.contacts.twitter_id = twitter_id === undefined ? me.contacts.twitter_id : twitter_id; twitter_id: twitter_id === undefined ? me.contacts?.twitter_id || null : twitter_id,
me.contacts.telegram_id = telegram_id === undefined ? me.contacts.telegram_id : telegram_id; telegram_id: telegram_id === undefined ? me.contacts?.telegram_id || null : telegram_id,
} vk_id: null,
};
} }
}); });

View File

@ -48,7 +48,7 @@ const MainPage: React.FC<IPageProps> = () => {
<Stat> <Stat>
<StatLabel>Online</StatLabel> <StatLabel>Online</StatLabel>
<StatContent> <StatContent>
<StatNumber>{onlineStore.count}</StatNumber> <StatNumber>{onlineStore.count || 0}</StatNumber>
<StatIcon icon={MdOutlineGroup} /> <StatIcon icon={MdOutlineGroup} />
</StatContent> </StatContent>
</Stat> </Stat>

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Box, Flex, Heading, Text } from '@chakra-ui/layout';
/** /**
* Types * Types
@ -10,6 +9,7 @@ import { IPageProps } from '@educt/interfaces';
/** /**
* Components * Components
*/ */
import { Page } from '@educt/components/PageElements';
import LoadingPage from '@educt/components/LoadingPage'; import LoadingPage from '@educt/components/LoadingPage';
import UpdateEmailForm from './components/UpdateEmailForm'; import UpdateEmailForm from './components/UpdateEmailForm';
@ -32,7 +32,6 @@ import { ChangeEmailPageContext } from '@educt/contexts';
* Providers * Providers
*/ */
import { ChangeEmailPageContextProvider } from '@educt/providers'; import { ChangeEmailPageContextProvider } from '@educt/providers';
import PrevPageButton from '@educt/components/PrevPageButton';
/** /**
* Change email page * Change email page
@ -48,22 +47,18 @@ const ChangeEmailPage: React.FC<IPageProps> = () => {
<ChangeEmailPageContextProvider> <ChangeEmailPageContextProvider>
<ChangeEmailPageContext.Consumer> <ChangeEmailPageContext.Consumer>
{({ isCodeSent, confirmEmailData }) => ( {({ isCodeSent, confirmEmailData }) => (
<> <Page>
{!isCodeSent ? ( {!isCodeSent ? (
<Box maxW='700px'> <>
<Flex alignItems='center'> <Page.Heading heading='Edit email address' description='Type your new email address.' />
<PrevPageButton prevPage='/profile' /> <Page.Content maxW='650px'>
<Heading as='h1'>Edit email address</Heading>
</Flex>
<Box mt='3'>
<Text>Type your new email address.</Text>
</Box>
<UpdateEmailForm currentEmail={me.email} /> <UpdateEmailForm currentEmail={me.email} />
</Box> </Page.Content>
</>
) : ( ) : (
confirmEmailData && <ConfirmEmailContainer data={confirmEmailData} /> confirmEmailData && <ConfirmEmailContainer data={confirmEmailData} />
)} )}
</> </Page>
)} )}
</ChangeEmailPageContext.Consumer> </ChangeEmailPageContext.Consumer>
</ChangeEmailPageContextProvider> </ChangeEmailPageContextProvider>

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { Button } from '@chakra-ui/button';
import { FormControl, FormHelperText, FormLabel } from '@chakra-ui/form-control'; import { FormControl, FormHelperText, FormLabel } from '@chakra-ui/form-control';
import { Input } from '@chakra-ui/input'; import { Input } from '@chakra-ui/input';
import { Box, Stack } from '@chakra-ui/layout'; import { Box, Stack } from '@chakra-ui/layout';
@ -26,6 +25,7 @@ import { ChangeEmailPageContext } from '@educt/contexts';
* Schema * Schema
*/ */
import UpdateEmailSchema from './UpdateEmailForm.validator'; import UpdateEmailSchema from './UpdateEmailForm.validator';
import { SaveButton } from '@educt/components/Buttons';
type UpdateEmailInputType = { type UpdateEmailInputType = {
email: string; email: string;
@ -87,17 +87,7 @@ const UpdateEmailForm: React.FC<UpdateEmailFormPropsType> = ({ currentEmail }) =
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
</Stack> </Stack>
<Button <SaveButton mt='4' type='submit' isLoading={isLoading} />
colorScheme='blue'
mt='4'
type='submit'
size='md'
variant='outline'
isLoading={isLoading}
loadingText='Sending...'
>
Send code
</Button>
</form> </form>
</Box> </Box>
); );

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Box, Heading, Flex, Text } from '@chakra-ui/react';
/** /**
* Types * Types
@ -11,13 +10,13 @@ import { IPageProps } from '@educt/interfaces';
* Components * Components
*/ */
import UpdatePasswordForm from './components/UpdatePasswordForm'; import UpdatePasswordForm from './components/UpdatePasswordForm';
import PrevPageButton from '@educt/components/PrevPageButton';
import LoadingPage from '@educt/components/LoadingPage'; import LoadingPage from '@educt/components/LoadingPage';
/** /**
* Hooks * Hooks
*/ */
import { useRootStore } from '@educt/hooks/useRootStore'; import { useRootStore } from '@educt/hooks/useRootStore';
import { Page } from '@educt/components/PageElements';
/** /**
* Change password page * Change password page
@ -30,16 +29,12 @@ const ChangePasswordPage: React.FC<IPageProps> = () => {
if (me === null) return <LoadingPage />; if (me === null) return <LoadingPage />;
return ( return (
<Box maxW='700px'> <Page>
<Flex alignItems='center'> <Page.Heading heading='Update password' description='Your new password must be different from the current one.' />
<PrevPageButton prevPage='/profile' /> <Page.Content>
<Heading as='h1'>Update password</Heading>
</Flex>
<Box mt='3'>
<Text>Your new password must be different from the current one.</Text>
</Box>
<UpdatePasswordForm /> <UpdatePasswordForm />
</Box> </Page.Content>
</Page>
); );
}; };

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Box, Stack } from '@chakra-ui/layout'; import { Box, Stack } from '@chakra-ui/layout';
import { FormControl, FormLabel, FormHelperText } from '@chakra-ui/form-control'; import { FormControl, FormLabel, FormHelperText } from '@chakra-ui/form-control';
import { Button } from '@chakra-ui/button';
import { Input } from '@chakra-ui/input'; import { Input } from '@chakra-ui/input';
import { SaveButton } from '@educt/components/Buttons';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
/** /**
@ -129,17 +129,7 @@ const UpdatePasswordForm: React.FC = () => {
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
</Stack> </Stack>
<Button <SaveButton mt='4' type='submit' isLoading={isLoading} />
colorScheme='blue'
mt='4'
type='submit'
size='md'
variant='outline'
isLoading={isLoading}
loadingText='Saving...'
>
Change
</Button>
</form> </form>
</Box> </Box>
); );

View File

@ -1,4 +1,5 @@
import React, { useState, ReactNode } from 'react'; import React, { useState, ReactNode } from 'react';
import * as constants from '../../constants';
import { import {
ChangeEmailPageContext, ChangeEmailPageContext,
CoursesPageContext, CoursesPageContext,
@ -25,7 +26,7 @@ export const RootStoreProvider = ({ children }: { children: ReactNode }) => {
* Socket context provider * Socket context provider
*/ */
export const SocketContextProvider: React.FC = ({ children }) => { export const SocketContextProvider: React.FC = ({ children }) => {
const socket = useSocket('ws://localhost:3333'); const socket = useSocket(constants.WS_URL);
return <SocketContext.Provider value={{ socket }}>{children}</SocketContext.Provider>; return <SocketContext.Provider value={{ socket }}>{children}</SocketContext.Provider>;
}; };

View File

@ -163,6 +163,17 @@ class UserService {
}); });
return result.data; return result.data;
} }
/**
* Get history of chat
*
* @param chatId Chat id
* @returns Array of messages
*/
public async fetchChatHistory(chatId: string): Promise<IApiRespose<{ history: unknown[] }>> {
const result = await this.api.get(`v1/me/messages/${chatId}`);
return result.data;
}
} }
export const UserServiceInstance = new UserService(ApiServiceInstance.api); export const UserServiceInstance = new UserService(ApiServiceInstance.api);