main
Саске Учиха 2 years ago
parent 4d7b3dd8cd
commit 3afcb45816

@ -10,6 +10,10 @@ Ararat International School - платформа по обучению игры
- Express.js
- Mongo DB
#### V 0.06
- [Messenger] Добавленно контектное меню по нажатию ПКМ на сообщения
- [Messenger] Добавлен EmojiPicker
#### V 0.05
- [Messenger] Добавлена заглушка когда чат не выбран
- [Messenger] Чаты теперь открываются по маршруту /messenger/chat/:id

@ -8,6 +8,7 @@
"name": "araratchess",
"version": "0.0.0",
"dependencies": {
"@headlessui/react": "^1.7.15",
"@react-icons/all-files": "https://github.com/react-icons/react-icons/releases/download/v4.10.1/react-icons-all-files-4.10.1.tgz",
"@reduxjs/toolkit": "^1.9.5",
"@types/axios": "^0.14.0",
@ -17,6 +18,7 @@
"axios": "^1.4.0",
"body-scroll-lock": "^4.0.0-beta.0",
"date-fns": "^2.30.0",
"emoji-picker-react": "^4.4.10",
"framer-motion": "^10.13.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -889,6 +891,21 @@
"npm": ">=6.14.13"
}
},
"node_modules/@headlessui/react": {
"version": "1.7.15",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.15.tgz",
"integrity": "sha512-OTO0XtoRQ6JPB1cKNFYBZv2Q0JMqMGNhYP1CjPvcJvjz8YGokz8oAj89HIYZGN0gZzn/4kk9iUpmMF4Q21Gsqw==",
"dependencies": {
"client-only": "^0.0.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@ -1678,6 +1695,19 @@
"node": ">= 6"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -1844,6 +1874,20 @@
"integrity": "sha512-fT3hvdUWLjDbaTGzyOjng/CQhQJSQP8ThO3XZAoaxHvHo2kUXiRQVMj9M235l8uDFiNPsPa6KHT1p3RaR6ugRw==",
"dev": true
},
"node_modules/emoji-picker-react": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.4.10.tgz",
"integrity": "sha512-/5o57w8BKiCHklyqfYCeUlm00R9tdm2ZmJd0p6Q3/Ul7E7eMh+/FduuRFn3UVQCl5F6i4kOdREMz7EJ7YI1vMg==",
"dependencies": {
"clsx": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/esbuild": {
"version": "0.18.14",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.14.tgz",

@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@headlessui/react": "^1.7.15",
"@react-icons/all-files": "https://github.com/react-icons/react-icons/releases/download/v4.10.1/react-icons-all-files-4.10.1.tgz",
"@reduxjs/toolkit": "^1.9.5",
"@types/axios": "^0.14.0",
@ -19,6 +20,7 @@
"axios": "^1.4.0",
"body-scroll-lock": "^4.0.0-beta.0",
"date-fns": "^2.30.0",
"emoji-picker-react": "^4.4.10",
"framer-motion": "^10.13.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

@ -1,11 +1,19 @@
import { FC, useRef, useEffect } from 'react'
import { FC, useRef, useEffect, useState } from 'react'
import TopInfo from './TopInfo';
import Message from './Message';
import EmptyChat from './EmptyChat';
import SendMessage from './SendMessage';
import { useAppSelector } from '../../../hooks/redux';
import MessageMenu from './MessageMenu/MessageMenu';
import { IContext } from '../../../models/IContext';
const Chat: FC = () => {
const [context, setContext] = useState<IContext>({
active: false,
x: 0,
y: 0,
message_id: ''
});
const messagesEndRef = useRef<HTMLDivElement>(null)
const { user } = useAppSelector(state => state.UserSlice)
const { chat } = useAppSelector(state => state.MessengerSlice)
@ -14,19 +22,19 @@ const Chat: FC = () => {
messagesEndRef.current.scrollIntoView()
}
}, [chat.messages])
return (
<div className='w-full bg-gray-300 bg-chat-pattern flex flex-col'>
<TopInfo data={chat.user}/>
<div className="overflow-y-auto flex-grow custom-scroll">
<div className={['flex-grow custom-scroll', context.active ? 'overflow-y-hidden' : 'overflow-y-auto'].join(' ')}>
<div className={['w-full flex flex-col mx-auto max-w-[1200px]', chat.messages.length > 0 ? 'mt-10' : 'h-full justify-center'].join(' ')}>
{chat.messages.length > 0
?
<>
{chat.messages.map(message=>
<Message key={message._id} msg={message} user={message.from === user._id ? user : chat.user} isMe={message.from === user._id ? true : false}/>
<Message key={message._id} msg={message} user={message.from === user._id ? user : chat.user} isMe={message.from === user._id ? true : false} setContext={setContext}/>
)}
<MessageMenu context={context} setContext={setContext}/>
<div ref={messagesEndRef}></div>
</>
:

@ -4,18 +4,25 @@ import { User } from '../../../models/User';
import format from 'date-fns/format';
import { BsCheckAll } from '@react-icons/all-files/bs/BsCheckAll'
import Avatar from '../../UI/Avatar';
import { IContext } from '../../../models/IContext';
interface MessageProps {
msg: IMessage;
user?: User;
isMe: boolean;
setContext: (obj: IContext) => void;
}
const Message: FC<MessageProps> = ({msg, user, isMe}) => {
const Message: FC<MessageProps> = ({msg, user, isMe, setContext}) => {
const handleClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.preventDefault();
setContext({active: true, x: e.clientX, y: e.clientY, message_id: msg._id});
}
return (
<div className={['flex w-1/2 mb-10', isMe ? 'self-end justify-end' : 'self-start justify-start'].join(' ')}>
<Avatar className='-translate-y-[5px] order-2' avatar={user?.avatar}/>
<div className={['shadow-md rounded-md py-2 px-4 relative before:absolute before:border-[10px] before:border-transparent before:border-t-[10px] before:top-0', isMe ? 'bg-apricot order-1 mr-5 before:border-t-apricot before:right-0 before:translate-x-1/2' : 'bg-zinc-400 order-3 ml-5 before:zinc-t-gray-400 before:left-0 before:-translate-x-1/2'].join(' ')}>
<div onContextMenu={e=> handleClick(e)} className={['shadow-md rounded-md py-2 px-4 relative before:absolute before:border-[10px] before:border-transparent before:border-t-[10px] before:top-0', isMe ? 'bg-apricot order-1 mr-5 before:border-t-apricot before:right-0 before:translate-x-1/2' : 'bg-zinc-400 order-3 ml-5 before:zinc-t-gray-400 before:left-0 before:-translate-x-1/2'].join(' ')}>
<h2 className='font-medium mb-1'>{user?.name} {user?.sname}</h2>
<p className='text-sm'>{msg.msg}</p>
<div className="flex items-center justify-end">
@ -24,8 +31,8 @@ const Message: FC<MessageProps> = ({msg, user, isMe}) => {
<span className={['ml-1 text-lg', msg.readed ? 'text-blue-600' : 'text-gray-800'].join(' ')}><BsCheckAll/></span>
}
</div>
</div>
</div>
)
}

@ -0,0 +1,42 @@
import { FC, useRef, useEffect } from 'react'
import { MdOutlineEdit } from '@react-icons/all-files/md/MdOutlineEdit'
import { BsArrow90DegLeft } from '@react-icons/all-files/bs/BsArrow90DegLeft'
import { BsArrow90DegRight } from '@react-icons/all-files/bs/BsArrow90DegRight'
import { MdContentCopy } from '@react-icons/all-files/md/MdContentCopy'
import { AiOutlineDelete } from '@react-icons/all-files/ai/AiOutlineDelete'
import { AiOutlineCheckCircle } from '@react-icons/all-files/ai/AiOutlineCheckCircle'
import MessageMenuItem from './MessageMenuItem';
import { IContext } from '../../../../models/IContext'
import { clickOuter } from '../../../../utils/clickOuter'
interface MessageMenuProps {
context: IContext;
setContext: (obj: IContext) => void;
}
const MessageMenu: FC<MessageMenuProps> = ({ context, setContext }) => {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if(menuRef.current) {
return clickOuter(menuRef.current, ()=>setContext({...context, active: false}));
}
}, [context, setContext]);
return (
<>
{context.active &&
<div ref={menuRef} style={{left: context.x, top: context.y}} className={['bg-gray-800 rounded-sm h-auto absolute', context.y < 280 ? '' : '-translate-y-full'].join(' ')}>
<ul className='flex flex-col'>
<MessageMenuItem icon={<BsArrow90DegLeft/>}>Reply</MessageMenuItem>
<MessageMenuItem icon={<MdOutlineEdit/>}>Edit</MessageMenuItem>
<MessageMenuItem icon={<BsArrow90DegRight/>}>Resend</MessageMenuItem>
<MessageMenuItem icon={<MdContentCopy/>}>Copy text</MessageMenuItem>
<MessageMenuItem icon={<AiOutlineDelete/>}>Delete</MessageMenuItem>
<MessageMenuItem icon={<AiOutlineCheckCircle/>}>Select</MessageMenuItem>
</ul>
</div>
}
</>
)
}
export default MessageMenu;

@ -0,0 +1,14 @@
import { FC, PropsWithChildren } from 'react'
interface MessageMenuItemProps {
icon: React.JSX.Element;
className?: string;
}
const MessageMenuItem: FC<PropsWithChildren<MessageMenuItemProps>> = ({children, icon, className}) => {
return (
<button className={['hover:bg-gray-700 py-3 px-5 rounded-sm transition-all flex items-center text-white text-sm', className].join(' ')}><span className='text-xl mr-3'>{icon}</span>{children}</button>
)
}
export default MessageMenuItem;

@ -1,17 +1,22 @@
import { FC, useState } from 'react'
import { FC, useState, useRef } from 'react'
import Textarea from './Textarea';
import IcoButton from '../../UI/IcoButton';
import { BsMic } from '@react-icons/all-files/bs/BsMic';
import { VscSend } from '@react-icons/all-files/vsc/VscSend';
import {BsEmojiSmile} from '@react-icons/all-files/bs/BsEmojiSmile';
import {ImAttachment} from '@react-icons/all-files/im/ImAttachment';
import EmojiPicker from '../../UI/EmojiPicker';
const SendMessage: FC = () => {
const ref = useRef<HTMLButtonElement>(null);
const [msg, setMsg] = useState<string>('');
const [pickerActive, setPickerActive] = useState<boolean>(false);
return (
<div className='bg-gray-800 border-l-2 flex items-center h-auto justify-between border-gray-700 px-12 p-2'>
<div className='bg-gray-800 border-l-2 flex relative items-center h-auto justify-between border-gray-700 px-12 p-2'>
<div className="flex items-center">
<IcoButton icon={<BsEmojiSmile/>} className='!px-3 !py-2'/>
<IcoButton ref={ref} onClick={()=> {pickerActive ? setPickerActive(false) : setPickerActive(true)}} icon={<BsEmojiSmile/>} className='!px-3 !py-2'/>
<EmojiPicker button={ref.current} msg={msg} setMsg={setMsg} active={pickerActive} setActive={setPickerActive}/>
<IcoButton icon={<ImAttachment/>} className='!px-3 !py-2'/>
</div>
<Textarea msg={msg} setMsg={setMsg}/>

@ -0,0 +1,36 @@
import { FC, useRef, useEffect } from 'react'
import Picker, { EmojiStyle, Theme } from 'emoji-picker-react';
import { EmojiClickData } from 'emoji-picker-react';
import { clickOuter } from '../../utils/clickOuter';
interface EmojiPickerProps {
msg: string;
setMsg: (val: string) => void;
active: boolean;
setActive: (bool: boolean) => void;
button: HTMLButtonElement | null;
}
const EmojiPicker: FC<EmojiPickerProps> = ({msg, setMsg, active, setActive, button}) => {
const onEmojiClick = (emojiObject: EmojiClickData ) => {
setMsg(msg + emojiObject.emoji)
}
const pickerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if(pickerRef.current && button) {
return clickOuter(pickerRef.current, ()=>setActive(false), button);
}
}, [active, setActive, button]);
return (
<>
{active &&
<div ref={pickerRef} className="absolute bottom-16">
<Picker theme={Theme.DARK} emojiStyle={EmojiStyle.NATIVE} onEmojiClick={onEmojiClick} skinTonesDisabled={true}/>
</div>
}
</>
)
}
export default EmojiPicker;

@ -1,14 +1,16 @@
import { FC } from 'react'
import { FC, forwardRef, ForwardedRef } from 'react'
interface IcoButton {
icon: React.JSX.Element;
className?: string;
onClick?: () => void;
ref?: ForwardedRef<HTMLButtonElement>
}
const IcoButton: FC<IcoButton> = ({icon, className}) => {
const IcoButton: FC<IcoButton> = forwardRef(({icon, className, onClick}, ref) => {
return (
<button className={['hover:bg-gray-700 py-3 px-5 rounded-sm transition-all mr-2 text-white text-xl', className].join(' ')}>{icon}</button>
<button ref={ref} className={['hover:bg-gray-700 py-3 px-5 rounded-sm transition-all mr-2 text-white text-xl', className].join(' ')} onClick={onClick}>{icon}</button>
)
}
})
export default IcoButton;

@ -15,4 +15,17 @@
html {
font-family: 'Manrope', sans-serif;
}
.EmojiPickerReact.epr-dark-theme {
--epr-bg-color: #1f2937 !important;
}
*::-webkit-scrollbar-thumb {
background-color: white;
border-radius: 20px;
border: 2px solid #1f2937;
}
*::-webkit-scrollbar {
width: 8px;
}

@ -0,0 +1,6 @@
export interface IContext {
active: boolean;
x: number;
y: number;
message_id: string;
}

@ -0,0 +1,14 @@
export const clickOuter = (ref: HTMLDivElement, callback: () => void, button: HTMLButtonElement | null = null) => {
const onClick = (e: MouseEvent) => {
const target = e.target as HTMLDivElement;
if(button) {
if(!(ref.contains(target) || button.contains(target))) {
callback();
}
} else {
ref.contains(target) || callback();
}
}
document.addEventListener('mousedown', onClick);
return () => document.removeEventListener('mousedown', onClick);
}
Loading…
Cancel
Save