Parcourir la source

fixed reducer, creation and updating chat

Ivar il y a 2 ans
Parent
commit
28b90eaf33

+ 8 - 2
src/App.scss

@@ -1,5 +1,12 @@
 
 
+.avatarInModal {
+   // пиздец...
+   .MuiAvatar-root {
+      width: 80px;
+      height: 80px;
+   }
+}
 
 .chatItem {
    height: 70px;
@@ -11,7 +18,6 @@
 
    .chatAvatar {
       flex-shrink: 0;
-      // пиздец
       .MuiAvatar-root {
          width: 50px;
          height: 50px;
@@ -25,7 +31,7 @@
       align-items: flex-start;
       max-width: 100%;
       flex-grow: 1;
-      margin-left: 10px;      
+      margin-left: 15px;      
 
       .chatTitle {
          width: 100%;

+ 112 - 31
src/actions/chatsActions.js

@@ -6,23 +6,96 @@ import {
    actionChatLeft,
 } from '../reducers'
 import { actionGetAllLastMsg } from './msgActions'
+import { actionUploadFile } from './mediaActions'
+
 
 
 // в массив newMemders передавать объекты только с полем _id
-export const actionUpdateChat = (title, members, chatId) => (
+const actionUpdateChat = (title, members, chatId) => (
    actionPromise('updateChat', gql(`mutation updateChat($chat:ChatInput) {
       ChatUpsert(chat:$chat) {
-         _id     
-         owner {_id login}       
+         _id
          title
+         avatar {
+            _id
+            url
+         }
+         owner {
+            _id
+            login
+            avatar {
+               _id
+               url
+            }
+         }
          members {
             _id
             login
-         }      
+            nick
+            avatar {
+               _id
+               url
+            }
+         }
+         lastModified    
       }
    }`, { chat: {_id: chatId, title, members} }))
 )
 
+// const actionUpdateChatAvatar = (avatarId, chatId) => (
+//    actionPromise('updateChatAvatar', gql(`mutation updateChatAvatar($chat:ChatInput) {
+//       ChatUpsert(chat:$chat) {
+//          _id
+//          title
+//          avatar {
+//             _id
+//             url
+//          }
+//          owner {
+//             _id
+//             login
+//             avatar {
+//                _id
+//                url
+//             }
+//          }
+//          members {
+//             _id
+//             login
+//             nick
+//             avatar {
+//                _id
+//                url
+//             }
+//          }
+//          lastModified    
+//       }
+//    }`, { chat: {_id: chatId,  avatar: {_id: avatarId} } }))
+// )
+
+// MediaUpsert нужен только для добавления данных для загруженного файла
+// и дальнейшего отображения его через эти данные (через аватары, сообщения)
+// const actionUpdateChatAvatar = (mediaId) => (
+//     actionPromise('uploadFile', gql(`mutation uploadFile($media: MediaInput) {  
+//         MediaUpsert(media: $media) {
+//         }
+//     }`, { media: {} }
+//     ))
+//  )
+
+export const actionSetChatInfo = (name, file, title, members, chatId) => (
+   async (dispatch) => {
+
+      await dispatch(actionUpdateChat(title, members, chatId))         
+
+      // let fileObj
+      // if (file) {
+      //    fileObj = await dispatch(actionUploadFile(name, file))
+      // } 
+   }
+)
+
+
 
 
 
@@ -39,18 +112,20 @@ export const actionGetChatsByUser = (userId, skipCount=0, limitCount=50) => (
          owner {
             _id
             login
+            avatar {
+               _id
+               url
+            }
          }
          members {
             _id
             login
-         }
-            messages {
+            nick
+            avatar {
                _id
-               owner {
-                  login
-               }
-               text
+               url
             }
+         }
          lastModified
       }     
    }`, { 
@@ -96,18 +171,20 @@ export const actionGetChatById = (chatId) => (
          owner {
             _id
             login
+            avatar {
+               _id
+               url
+            }
          }
          members {
             _id
             login
-         }
-            messages {
+            nick
+            avatar {
                _id
-               owner {
-                  login
-               }
-               text
+               url
             }
+         }
          lastModified
       }     
    }`, { 
@@ -146,25 +223,29 @@ export const actionChatsCount = (userId) => (
 // export const actionGetAllChats = (userId) => (
 //    actionPromise('getAll', gql(`query getAll($q: String){
 //       ChatFind (query: $q){
-//          _id
-//          title
-//          owner {
-//             login
-//          }
-//          avatar {
-//             url
-//          }
-//          members {
 //             _id
-//             login
-//          }
-//          messages {
+//             title
+//             avatar {
+//                _id
+//                url
+//             }
 //             owner {
+//                _id
 //                login
+//                avatar {
+//                   _id
+//                   url
+//                }
 //             }
-//             text
-//          }
-//          lastModified
+//             members {
+//                _id
+//                login
+//                avatar {
+//                   _id
+//                   url
+//                }
+//             }
+//             lastModified
 //       }         
 //    }`, { 
 //          q: JSON.stringify([ { 'members._id': userId }, { skip: [0], limit: [100] } ])

+ 53 - 3
src/actions/findActions.js

@@ -1,10 +1,10 @@
 import {gql} from '../helpers'
 import {   
-   actionPromise,
+   actionPromise
 } from '../reducers'
 
 
-export const actionFindUsers = (word, skipCount=0, limitCount=50) => (
+export const actionFindUsers = (word, skipCount=0, limitCount=20) => (
    actionPromise('findUsers', gql(`query findUsers($q: String) {
       UserFind (query: $q){
          _id
@@ -12,6 +12,7 @@ export const actionFindUsers = (word, skipCount=0, limitCount=50) => (
          login
          nick
          avatar {
+            _id
             url
          }
       }     
@@ -30,4 +31,53 @@ export const actionFindUsers = (word, skipCount=0, limitCount=50) => (
                            ])
       }
    ))
-)
+)
+
+// поиск чатов конкретного юзера по названию 
+export const actionFindChatsByUser = (userId, word, skipCount=0, limitCount=20) => (
+   actionPromise('findChatsByUser', gql(`query findChatsByUser($q: String) {
+      ChatFind (query: $q){
+         _id
+         _id
+         title
+         avatar {
+            _id
+            url
+         }
+         owner {
+            _id
+            login
+            avatar {
+               _id
+               url
+            }
+         }
+         members {
+            _id
+            login
+            nick
+            avatar {
+               _id
+               url
+            }
+         }
+         lastModified  
+      }     
+   }`, { 
+         q: JSON.stringify([  {  title: `/${word}/`, 
+                                 $or: [   
+                                    { ___owner: userId }, 
+                                    { 'members._id': userId }                                    
+                                 ] 
+                              },
+                              { 
+                                 sort: [{title: 1}],  
+                                 skip: [skipCount], 
+                                 limit: [limitCount] 
+                              } 
+                           ])
+      }
+   ))
+)
+
+// можно добавить еще поиск чатов по мемберам

+ 3 - 3
src/actions/index.js

@@ -7,10 +7,10 @@ import {
    actionSetUserPass,
 } from './authActions'
 import {
-   actionUpdateChat,
    actionGetChatsByUser,
    actionGetChatById,
-   
+
+   actionSetChatInfo,
    actionFullChatList,
    actionChatsCount
 } from './chatsActions'
@@ -40,10 +40,10 @@ export {
    actionSetUserPass,
 } 
 export {
-   actionUpdateChat,
    actionGetChatsByUser,
    actionGetChatById,
 
+   actionSetChatInfo,
    actionFullChatList,
    actionChatsCount
 }

+ 0 - 12
src/actions/mediaActions.js

@@ -17,18 +17,6 @@ export const actionUploadFile = (name, file) => {
 }
 
     
-// MediaUpsert нужен только для добавления данных для загруженного файла
-// и дальнейшего отображения его через эти данные (через аватары, сообщения)
-// const actionChangeFile = (mediaId) => (
-//     actionPromise('uploadFile', gql(`mutation uploadFile($media: MediaInput) {  
-//         MediaUpsert(media: $media) {
-//         }
-//     }`, { media: {} }
-//     ))
-//  )
-
-
-
 
 
 

BIN
src/assets/chatSound.ogg


Fichier diff supprimé car celui-ci est trop grand
+ 3 - 0
src/assets/kick.svg


Fichier diff supprimé car celui-ci est trop grand
+ 5 - 0
src/assets/kick2.svg


BIN
src/assets/tg.png


+ 5 - 3
src/components/Avatar.jsx

@@ -50,7 +50,9 @@ export const CMyAvatar = connect( state => ({ profile: state.promise.myProfile?.
 export const ChatAvatar = ({ chat, bigSize=false }) => {
  
   function getUrl() {
-      if (chat.avatar?.url) {
+    if (chat.localUrl) {
+      return chat.localUrl
+    } else if (chat.avatar?.url) {
       return backURL + chat.avatar?.url
     } else {
       return false
@@ -61,9 +63,9 @@ export const ChatAvatar = ({ chat, bigSize=false }) => {
     <>
     {
         getUrl() ?
-        <Avatar  sx={ bigSize ? big : middle } 
+        <Avatar  sx={ bigSize ? big : small } 
                 alt={chat.title } src={getUrl()} /> :
-        <Avatar  sx={ bigSize ? big : middle }
+        <Avatar  sx={ bigSize ? big : small }
                 {...stringColor.stringAvatar(chat.title)} /> 
     }        
     </>

+ 16 - 6
src/components/ChatList.jsx

@@ -4,7 +4,7 @@ import Box from '@mui/material/Box';
 
 import {Link} from 'react-router-dom'
 
-import { FloatBtn, ChatAvatar } from "../components"
+import { FloatBtn, ChatAvatar, CChatModal } from "../components"
 import { connect }  from 'react-redux'
 
 
@@ -63,6 +63,16 @@ const Chat = ({ chat, currChat }) => {
   )
 }
 
+
+const FloatBtnModal = ({ chat={}, OPEN }) => {
+  return (
+    <Box onClick={OPEN}
+      sx={{  position: 'fixed', top: '90%', left: '25%', zIndex: 10}} >
+      <FloatBtn />
+    </Box>
+  )
+}
+
 const ChatList = ({ chats=[], currChatId }) => {
 
   // const [chatsInput, setChats] = useState(chats)
@@ -71,18 +81,18 @@ const ChatList = ({ chats=[], currChatId }) => {
   // }, [chats])
 
   return (
-      <div        
+      <List        
         sx={{ maxWidth: '100%', bgcolor: 'background.paper', position: 'relative', zIndex: 2, }}
         >
           <div>
-            <Box sx={{  position: 'fixed', top: '90%', left: '25%', zIndex: 10}} >
-              <FloatBtn />
-            </Box>
+
+              <CChatModal key={'creation'} create={true} render={FloatBtnModal} />
+
               {chats.map(chat =>          
                     <Chat key={chat._id} chat={chat} currChat={currChatId === chat._id} /> 
               )}  
           </div>  
-        </div>
+      </List>
 
     
       

+ 118 - 0
src/components/ChatMngHeader.jsx

@@ -0,0 +1,118 @@
+import React, {useState, useEffect, useRef} from 'react'
+import IconButton from '@mui/material/IconButton';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import MoreVertIcon from '@mui/icons-material/MoreVert';
+
+import { ChatAvatar, CChatModal } from "../components"
+
+import { printEnding } from "../helpers"
+import { connect } from 'react-redux'
+
+
+const chatMngBody = {
+   width: "100%",
+   display: "flex",
+   justifyContent: "flex-start",
+   aligneItems: "center"
+}
+   const blockMobile = {
+      flexShrink: 0,
+      flexGrow: 0,
+      flexBasis: "60px",
+      display: "flex",
+      justifyContent: "flex-start",
+      aligneItems: "center",
+      display: "none"
+   }
+   const blockLeft = {
+      flexShrink: 1,
+      flexGrow: 1,
+      display: "flex",
+      justifyContent: "flex-start",
+      aligneItems: "center",
+      cursor: "pointer"
+   }
+      const blockAv = {
+         margin: "auto 0"
+      }
+      const blockInfo = {
+         display: "flex",
+         flexDirection: "column",
+         justifyContent: "space-between",
+         whiteSpace: "nowrap",
+         textOverFlow: "ellipsis",
+         marginLeft: "15px",
+         userSelect: "none"
+      }
+         const blockName = {
+            fontSize: "16px",
+            fontWeight: "500",
+         }
+         const blockMemb = {
+            fontSize: "14px",
+            fontWeight: "300",
+         }
+   const blockRight = {
+      flexShrink: 0,
+      flexGrow: 0,
+      flexBasis: "10%",
+      display: "flex",
+      justifyContent: "flex-end",
+      aligneItems: "center"
+   }
+
+
+
+const chatMinInfo = ({ chat, OPEN }) => {
+   if (chat) {
+      return (
+         <div style={blockLeft} onClick={OPEN}>
+            <div style={blockAv}>
+               <ChatAvatar chat={chat} />
+            </div>
+            <div style={blockInfo}>
+               <div style={blockName}>
+                  {chat.title}
+               </div>
+               <div style={blockMemb}>
+                  {chat.members && `${chat.members.length} участник${printEnding(String(chat.members.length))}`} 
+               </div>
+            </div>
+         </div>
+      )
+   } else {
+      return (
+         <></>
+      )
+   }
+}
+
+
+const ChatMngHeader = ({ chats, chatId }) => {
+
+
+   const chat = chats[chatId]
+
+   // console.log(chat)
+   return (
+      <div style={chatMngBody}>
+
+         <div style={blockMobile}>
+            <IconButton style={{ color: '#fff' }} >
+               <ArrowBackIcon />
+            </IconButton>
+         </div>
+
+
+         <CChatModal key={chatId} create={false} chat={chat} render={chatMinInfo} /> 
+         
+
+         <div style={blockRight}>
+            <IconButton style={{ color: '#fff' }} >
+               <MoreVertIcon />
+            </IconButton>
+         </div>
+      </div>
+   )
+}
+export const CChatMngHeader = connect(state => ( { chats: state.chats || []} ))(ChatMngHeader)

+ 262 - 0
src/components/ChatModal.jsx

@@ -0,0 +1,262 @@
+import React, {useEffect, useState} from 'react';
+import Box from '@mui/material/Box';
+import Modal from '@mui/material/Modal';
+import Button from '@mui/material/Button';
+import IconButton from '@mui/material/IconButton';
+import Typography from '@mui/material/Typography';
+import TextField from '@mui/material/TextField';
+import CloseIcon from '@mui/icons-material/Close';
+import List from '@mui/material/List';
+import Divider from '@mui/material/Divider';
+
+import {ReactComponent as KickLogo} from '../assets/kick.svg'
+
+
+import {useDropzone} from 'react-dropzone';
+
+import { ChatAvatar, CUserSearch, UserCard } from '.'
+
+import { printStrReq } from "../helpers"
+import {connect}  from 'react-redux'
+import { actionSetChatInfo } from '../actions'
+
+const styleModalParrent = {
+   position: 'absolute',
+   top: '50%',
+   left: '50%',
+   transform: 'translate(-50%, -50%)',
+   width: '70%',
+   maxWidth: '1000px',
+   height: '95%',
+   bgcolor: 'background.paper',
+   border: '1px solid #999',
+   boxShadow: 24,
+   p: 3,
+   display: 'flex',
+   flexDirection: 'column',
+   justifyContent: 'space-between',
+}
+
+const DelBtn = ({ onEvent }) => (
+   <IconButton edge="end" onClick={onEvent}>
+      <KickLogo />
+   </IconButton>
+)
+
+const ChatModal = ({ minTitle="2", chat, onСonfirm, titleError, create, render }) => {
+
+   const [open, setOpen] = useState(false)
+   const handleOpen = () =>  setOpen(true) 
+   const handleClose = () => setOpen(false)
+ 
+   const [title, setTitle] = useState(chat?.title || '')
+   const [titleBlur, setTitleBlur] = useState(false)
+
+   const [img, setImg] = useState(null)
+   const {
+      getRootProps,
+      getInputProps
+   } = useDropzone({   
+      accept: 'image/*', 
+      maxFiles: 1,
+      onDrop: (acceptedFiles) => {
+         setImg(acceptedFiles[0])
+      }
+   })
+
+
+   const [members, setMembers] = useState(chat?.members || [])
+
+   const onAddMember = (newMember) => {
+      setMembers([...members, newMember])
+   }
+   const onDelMember = (i) => {
+      setMembers(members.filter((el, index) => index !== i)) 
+   }
+ 
+ 
+   const [wrongAlert, setWrongAlert] = useState(false)
+ 
+   useEffect(() => {
+      if (titleError?.payload === null) {
+         setWrongAlert(true)
+      } else {
+         setWrongAlert(false)
+      }
+   },[titleError])
+
+   useEffect(() => {
+      setTitle(chat?.title || '')
+      setImg(null)
+      setMembers(chat?.members || [])
+   },[open])
+ 
+
+
+   function prepareMembers(members) {
+      const newMembers = []
+      for (const member of members) {
+         newMembers.push({_id: member._id})
+      }
+      return newMembers
+   }
+
+   const OpenBtn = render
+   return (
+     <>
+       <OpenBtn chat={chat} OPEN={handleOpen} />
+
+       <Modal
+         open={open}
+         onClose={handleClose}
+       >
+          
+         <Box sx={styleModalParrent}>
+
+            <Box sx={{ display: 'flex', justifyContent: 'space-between', borderBottom: '1px solid #999', pb: 1 }}>
+
+               <Box sx={{ display: 'flex', justifyContent: 'start' }}>
+                  <Typography variant="h6">
+                    {create ? 'Создание чата' : 'Редактирование чата' } 
+                  </Typography>
+               </Box>
+
+               <Box sx={{ display: 'flex', justifyContent: 'end' }}>
+                  <IconButton aria-label="delete" onClick={handleClose}>
+                     <CloseIcon />
+                  </IconButton>
+               </Box>
+
+            </Box>     
+
+           
+
+            <Box sx={{ display: 'flex', justifyContent: 'space-between', pl: 3, pr: 6 }} >
+
+                  <Box sx={{ display: 'flex', justifyContent: 'start', alignItems: 'start', flexBasis: "35%" }}> 
+                     <form action="/upload" method="post" encType="multipart/form-data" id='formChat'> 
+                        <section className="container">
+                           <Box {...getRootProps({className: 'dropzone'})} 
+                                 sx={{ cursor: 'pointer', height: '80px', display: 'flex' }}
+                                 className="avatarInModal" >   
+
+                               <ChatAvatar 
+                                       chat={{  title: title, avatar: {url: chat?.avatar?.url || ''},
+                                             localUrl: img && URL.createObjectURL(img)}} bigSize={true} /> 
+      
+                              <input {...getInputProps()} type="file" name="media" id='mediaUser' />
+                              <Box sx={{ pl: 4 }} >
+                                 <Typography sx={{ fontWeight: 500, fontSize: 18 }}>
+                                    Аватар
+                                 </Typography>                                 
+                              </Box>
+                           </Box>
+                        </section>
+                     </form>
+                  </Box>
+   
+                  <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'start', alignItems: 'stretch',
+                              flexBasis: "35%" }}>
+                     <Typography sx={{ fontWeight: 500, fontSize: 18 }}>
+                        Название
+                     </Typography>
+                     <TextField            
+                        onChange={(e) => {
+                              e.target.value = e.target.value.trim()
+                              setTitle(e.target.value)
+                           }
+                        }     
+                        onBlur={() => {
+                           setTitleBlur(true)              
+                        }
+                        } 
+                        onFocus={() => {
+                           setTitleBlur(false)              
+                           }
+                        }          
+                        error={titleBlur ? ((title?.length >= minTitle ) ? false : true) : false}
+                        helperText={printStrReq(title, minTitle)}            
+      
+                        inputProps={{
+                           maxLength: 50
+                        }}
+                        variant="standard"
+                        margin="none"
+                        fullWidth
+                        id="titleChat"
+                        label=""
+                        name="title"
+                        defaultValue={title}
+                        sx={{mt: 1}}
+                     />
+                  </Box>
+                  
+            </Box>
+
+
+            <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', height: '60%', mt: 2   }}>
+            
+               <Box sx={{   display: 'flex', justifyContent: 'center', mb: 1 }}>
+                  <Typography sx={{ fontWeight: 500, fontSize: 18 }}>
+                     Добавить участников
+                  </Typography>
+               </Box>
+
+               <Box sx={{ display: 'flex', justifyContent: 'space-between',  height: '100%', }} >
+
+                  <Box sx={{ flexBasis: "45%" }}>
+
+                     <Box sx={{  height: "40px", display: 'flex', justifyContent: 'center', pt: 1 }}>
+                        <Typography sx={{ fontWeight: 400, fontSize: 16 }}>
+                           Сейчас в чате
+                        </Typography>
+                     </Box>
+                     
+                     <Box 
+                        sx={{  height: "calc(100% - 40px)", overflowY: "auto" }}
+                        >
+                        <List
+                           sx={{ maxWidth: '100%', bgcolor: 'background.paper' }}
+                           >
+                           { members.map((member, i) => <UserCard key={member._id} user={member} 
+                                                                  render={DelBtn}  onAction={() => onDelMember(i)} />)}
+                        </List>
+                     </Box>
+                  </Box>     
+
+                  <CUserSearch open={open} alreadySearched={members} onAdd={onAddMember} />
+
+               </Box>
+
+            </Box>
+
+
+            <Box sx={{ mt: 2 }} >
+                     
+                  { wrongAlert &&
+                     <Typography component="p" variant="body2" my={2} ml={2} 
+                        sx={{fontWeight: 'medium', fontSize: '0.75rem', color: '#d32f2f'}}>
+                           Ошибка создания
+                     </Typography> 
+                  }
+      
+                  <Box sx={{ display: 'flex', justifyContent: 'center'}}>
+                     <Button  variant="contained" 
+                              disabled={(title?.length >= minTitle) ? false : true}
+                              onClick={() => onСonfirm( "media", img, title, 
+                                                         prepareMembers(members), chat?._id )} >
+                        Применить 
+                     </Button>
+                  </Box>
+
+            </Box>
+
+ 
+         </Box>
+       </Modal>
+     </>
+   )
+ }
+ export const CChatModal = connect(  state => ( { chatError: state.promise.updateChat || {} }), 
+                                        { onСonfirm: actionSetChatInfo })(ChatModal)
+ 

+ 1 - 6
src/components/FloatBtn.jsx

@@ -1,12 +1,6 @@
 import { Fab } from '@mui/material';
 import AddIcon from '@mui/icons-material/Add';
 
-import {
-  actionAddChat
-} from "../actions"
-import {store} from "../reducers"
-
-
 export const FloatBtn = ({}) => {
   return (
     <Fab color="primary" aria-label="add">
@@ -14,3 +8,4 @@ export const FloatBtn = ({}) => {
     </Fab>
   )
 }
+

+ 2 - 1
src/components/MainMenu.jsx

@@ -86,7 +86,8 @@ return (
                      justifyContent: 'start', alignItems: 'center', 
                      background: '#dddddd44' }}>
          
-         <Box sx={{ m: 2, mb: 1 }}>
+         <Box  sx={{ m: 2, mb: 1 }}
+               className="avatarInModal">
             <CMyAvatar bigSize={true} />
          </Box> 
 

+ 0 - 0
src/components/MediaModal.jsx


+ 7 - 2
src/components/MsgList.jsx

@@ -150,10 +150,15 @@ const Msg = ({ msg, prevOwner, prevTime, myProfile }) => {
                   {
                      (media && media.length !== 0) &&
                            <div>
-                              {media.map((mediaObj) => <img key={mediaObj.url}
+                              {media.map((mediaObj) => <div key={mediaObj.url} 
+                                                            style={{
+                                                            }} >
+                                                         <img key={mediaObj.url}
                                                             // type={mediaObj.type}
-                                                            style={{maxWidth: "400px"}}
+                                                            style={{ maxWidth: "100%", width: "auto",
+                                                                     maxHeight: "400px", height: "auto" }}
                                                             src={backURL + mediaObj.url } />
+                                                       </div>                                     
                                                                                              )}
                            </div> 
                   }

+ 9 - 12
src/components/ProfileModal.jsx

@@ -73,8 +73,6 @@ const PassModal = ({ onСonfirm, regError}) => {
          hideBackdrop
          open={open}
          onClose={handleClose}
-         aria-labelledby="child-modal-title"
-         aria-describedby="child-modal-description"
        >
          <Box sx={{ ...styleModalParrent,   width: '40%', maxWidth: '400px', }}>
             <Box sx={{ display: 'flex', justifyContent: 'end' }}>
@@ -157,13 +155,12 @@ const PassModal = ({ onСonfirm, regError}) => {
             </Box>
 
 
-            { wrongAlert ?
-                  <Typography component="p" variant="body2" mt={1} ml={2} 
-                     sx={{fontWeight: 'medium', fontSize: '0.75rem', color: '#d32f2f'}}>
-                        Неверный пароль
-                  </Typography> :
-                  <></>
-               }
+            { wrongAlert &&
+               <Typography component="p" variant="body2" mt={1} ml={2} 
+                  sx={{fontWeight: 'medium', fontSize: '0.75rem', color: '#d32f2f'}}>
+                     Неверный пароль
+               </Typography> 
+            }
 
             <Box sx={{ display: 'flex', justifyContent: 'end', mt: 2}}>
                <Button  variant="contained" 
@@ -192,7 +189,6 @@ const PassModal = ({ onСonfirm, regError}) => {
 
 const ProfileModal = ({minLog='2', myProfile, onСonfirm, logError}) => {
   const [open, setOpen] = useState(false)
-  // const {login, nick, avatar: {url}} = myProfile
 
    const [login, setLogin] = useState(myProfile.login)
    const [logBlur, setLogBlur] = useState(false)
@@ -207,7 +203,6 @@ const ProfileModal = ({minLog='2', myProfile, onСonfirm, logError}) => {
       accept: 'image/*', 
       maxFiles: 1,
       onDrop: (acceptedFiles) => {
-         // console.log(acceptedFiles)
          setImg(acceptedFiles[0])
       }
    });
@@ -256,7 +251,9 @@ const ProfileModal = ({minLog='2', myProfile, onСonfirm, logError}) => {
 
                <form action="/upload" method="post" encType="multipart/form-data" id='formUser'> 
                   <section className="container">
-                     <Box {...getRootProps({className: 'dropzone'})} sx={{ cursor: 'pointer', height: '70px', display: 'flex' }} >
+                     <Box {...getRootProps({className: 'dropzone'})} 
+                           sx={{ cursor: 'pointer', height: '80px', display: 'flex' }} 
+                           className="avatarInModal" >
 
                         <UserAvatar profile={{  login: login, nick: nick, avatar: {url: myProfile.avatar?.url || ''},
                                        localUrl: img && URL.createObjectURL(img)}} bigSize={true} />

+ 3 - 3
src/components/SearchBlock.jsx

@@ -35,15 +35,15 @@ const StyledInputBase = styled(InputBase)(({ theme }) => ({
   },
 }));
 
-export const SearchBlock = () => {
+export const SearchBlock = ({ setInput, text="Поиск" }) => {
    return (
    <Search>
       <SearchIconWrapper>
         <SearchIcon />
       </SearchIconWrapper>
       <StyledInputBase
-      placeholder="Поиск..."
-      inputProps={{ 'aria-label': 'search' }}
+        onChange={(e) => setInput(e.target.value)}
+        placeholder={text}
       />
    </Search>
    )

+ 37 - 0
src/components/UserCard.jsx

@@ -0,0 +1,37 @@
+import React from 'react';
+import ListItem from '@mui/material/ListItem';
+import Divider from '@mui/material/Divider';
+import ListItemText from '@mui/material/ListItemText';
+import ListItemAvatar from '@mui/material/ListItemAvatar';
+
+import { UserAvatar } from '.'
+
+
+
+export const UserCard= ({ user, render, onAction, disabled=false }) => {
+
+   const ManageBtn = render
+   return (
+      <>
+         <ListItem
+            sx={ disabled ? { bgcolor: '#ddd' } : {} }
+            secondaryAction={
+               disabled ?
+               <></> :
+               <ManageBtn onEvent={onAction} />
+            }
+            >
+
+            <ListItemAvatar>
+               <UserAvatar profile={user} />
+            </ListItemAvatar>
+
+            <ListItemText
+               primary={user.login}
+               secondary={user.nick}
+            />
+         </ListItem>
+      <Divider variant="inset" component="li" />
+     </>
+   )
+ }

+ 68 - 0
src/components/UserSearch.jsx

@@ -0,0 +1,68 @@
+import React, {useEffect, useState} from 'react';
+import List from '@mui/material/List';
+import Box from '@mui/material/Box';
+import IconButton from '@mui/material/IconButton';
+import AddIcon from '@mui/icons-material/Add';
+
+import { UserCard, SearchBlock } from '.'
+
+import { connect }  from 'react-redux'
+import { actionFindUsers } from '../actions'
+
+
+const AddBtn = ({onEvent}) => (
+   <IconButton edge="end" onClick={onEvent}>
+      <AddIcon />
+   </IconButton>
+)
+
+const UserSearch = ({ findedUsers, onSearch, alreadySearched=[], onAdd, open }) => {
+
+   const [finded, setFinded] = useState([])
+   const [input, setInput] = useState(null)
+
+   useEffect( () => {
+      let timeout
+      if (input !== null) {
+         timeout = setTimeout(() => {
+            onSearch(input)
+         }, 500)
+      }
+      return () => {
+         clearTimeout(timeout)
+      }
+   },[input])
+
+   useEffect(() => {
+      setFinded(findedUsers)
+   },[findedUsers])
+   
+   useEffect(() => {
+      setInput(null)
+      setFinded([])
+   },[open])
+
+   return (
+ 
+      <Box sx={{ flexBasis: "45%" }} >
+         <Box sx={{ height: "40px" }}>
+            <SearchBlock setInput={setInput} text={'Найти пользователя'} />
+         </Box>
+
+         <Box sx={{ height: "calc(100% - 40px)", overflowY: "auto" }}>
+            <List
+               sx={{ maxWidth: '100%', bgcolor: 'background.paper' }}
+               >
+               { finded.map((user) =>    
+                                    <UserCard key={user._id} user={user} 
+                                       render={AddBtn}  onAction={() => onAdd(user)} 
+                                          disabled={!!alreadySearched.find((searchedUser) => searchedUser._id === user._id)} />)}
+            </List>
+         </Box>
+      </Box>
+
+   )
+ }
+ export const CUserSearch = connect( state => ( { findedUsers: state.promise.findUsers?.payload || [] }), 
+                                        { onSearch: actionFindUsers } )(UserSearch)
+ 

+ 8 - 2
src/components/index.js

@@ -8,8 +8,11 @@ import {SearchBlock} from './SearchBlock'
 import {FloatBtn} from './FloatBtn'
 import {ChatAvatar, UserAvatar, CMyAvatar} from './Avatar'
 import {CProfileModal} from './ProfileModal'
-
+import {CChatModal} from './ChatModal'
+import {CUserSearch} from './UserSearch'
+import {UserCard} from './UserCard'
 import {CSendingField} from './SendingField'
+import {CChatMngHeader} from './ChatMngHeader'
 
 
 
@@ -22,5 +25,8 @@ export {SearchBlock}
 export {FloatBtn}  
 export {ChatAvatar, UserAvatar, CMyAvatar} 
 export {CProfileModal} 
-
+export {CChatModal} 
+export {CUserSearch} 
+export {UserCard} 
 export {CSendingField}  
+export {CChatMngHeader}  

+ 3 - 6
src/helpers/index.js

@@ -1,16 +1,13 @@
 // вспомогательные функции
 import {backURL, gql} from './getGql'
-import {printStrReq} from './printStrReq'
+import {printStrReq, printEnding} from './printStrReq'
 import {passReq} from './passReq'
 import {dateFromStamp} from './dateFuncs'
 import {stringColor} from './stringColorFuncs'
 
 
 export {backURL, gql} 
-export {printStrReq} 
+export {printStrReq, printEnding} 
 export {passReq} 
 export {dateFromStamp} 
-export {stringColor} 
-
-
-
+export {stringColor} 

+ 10 - 4
src/helpers/printStrReq.js

@@ -1,11 +1,17 @@
 
+
+
+export const printEnding = (amount) => {
+   return (((amount[amount.length - 1] == 1) && (amount[amount.length - 2] != 1)) ? '' :
+               ((amount[amount.length - 1] > 1) && (amount[amount.length - 1] < 5) && (amount[amount.length - 2] != 1)) ? 'а' :
+                  'ов')
+}
+
+
 export const printStrReq = (str, amount) => {
    if (str === undefined) {
       return ''
    } 
    return (str.length >= amount) ? "" : 
-   ` минимум ${amount} символ${(((amount[amount.length - 1] == 1) && (amount[amount.length - 2] != 1)) ? '' :
-      ((amount[amount.length - 1] > 1) && (amount[amount.length - 1] < 5) && (amount[amount.length - 2] != 1)) ? 'а' :
-         'ов')}`
-   
+   ` минимум ${amount} символ${ printEnding(amount) }`
 }

+ 4 - 2
src/pages/AsidePage.jsx

@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { useState } from 'react'
 
 import {
    MenuDrawer, 
@@ -17,13 +17,15 @@ const asidePageHeader = {
 
 export const AsidePage = ({children}) => {
 
+   const [input, setInput] = useState('')
+
    return (
       <div style={asidePageContainer}>
 
          <div style={asidePageHeader}>
             <Header>
                <MenuDrawer />
-               <SearchBlock />
+               <SearchBlock setInput={setInput} />
             </Header>  
          </div>
 

+ 4 - 4
src/pages/ChatsPage.jsx

@@ -57,8 +57,8 @@ const ChatsPage = ({  match:{params:{_id}}, auth,
      </div>
    )
  }
- export const CChatsPage= connect( state => ({  auth: state.auth, 
+ export const CChatsPage= connect( state => ( { auth: state.auth, 
                                                 allCount: state.promise?.chatsCount?.payload || 0 }),
-                                          { getChats: actionFullChatList,
-                                            getAllCount: actionChatsCount
-                                                })(ChatsPage)
+                                              { getChats: actionFullChatList,
+                                                getAllCount: actionChatsCount
+                                                    })(ChatsPage)

+ 4 - 4
src/pages/MsgPage.jsx

@@ -2,6 +2,7 @@ import React, {useState, useEffect, useRef} from 'react';
 
 import {
    Header, 
+   CChatMngHeader,
    CMsgList,
    CSendingField,   
 } from "../components"
@@ -57,10 +58,9 @@ const MsgBlock = ({ chatId, getMsgs, msgsCount=20 }) => {
 
             <div style={msgPageHeader}>
                <Header> 
-                  <div
-                     >
-                     шапка2: инфо о чате и управление, управление сообщениями
-                  </div>
+
+                  <CChatMngHeader chatId={chatId} />
+
                </Header>  
             </div>
 

+ 8 - 9
src/reducers/chatsReducer.js

@@ -71,21 +71,20 @@
 
           for (const chat of payload) {
 
-            const oldMsgs = oldChats[chat._id]?.messages 
+            const oldMsgs = oldChats[chat._id]?.messages
             const newMsgs = chat?.messages
 
-            if (oldMsgs && newMsgs && oldMsgs.length > 0) {
+            oldChats[chat._id] = chat
 
+            if (oldMsgs && newMsgs) {
+              
+              oldChats[chat._id].messages = refreshMsgs(newMsgs, oldMsgs)
 
-              chat.messages = refreshMsgs(newMsgs, oldMsgs)
+            } else if (oldMsgs) {
 
-              oldChats[chat._id] = chat
-     
-            } else {
-
-
-              oldChats[chat._id] = chat
+              oldChats[chat._id].messages = oldMsgs
             }
+
           }
           
           const newState = sortChats(oldChats)

+ 14 - 0
src/reducers/store.js

@@ -18,6 +18,7 @@ import {
 } from '../actions'
 
 import msgSound from '../assets/msgSound.ogg'
+import chatSound from '../assets/chatSound.ogg'
 // const { io } = require("socket.io-client");
 
 
@@ -47,6 +48,7 @@ socket.on('jwt_fail', (error) => {
    store.dispatch(actionFullLogout())
 })
 
+
 socket.on('msg', async (msg) => { 
    console.log('пришло смс')
    
@@ -63,6 +65,7 @@ socket.on('msg', async (msg) => {
    await store.dispatch(actionMsgOne(msg)) 
 
    if (chatId) {
+      console.log('ZASHLO')
       let chatUpdated = await store.dispatch(actionGetChatById(chatId))
       await store.dispatch(actionChatOne(chatUpdated))
    }
@@ -70,11 +73,22 @@ socket.on('msg', async (msg) => {
 
 socket.on('chat', (chat) => { 
    console.log('нас добавили в чат')
+   const inputChatAudio = new Audio(chatSound)
+   inputChatAudio.play()
+
    store.dispatch(actionChatOne(chat)) 
 })
 
 socket.on('chat_left', (chat) => { 
    console.log('нас выкинули из чата')
+
+   const state = store.getState()
+   const myId = state.auth.payload?.sub?.id
+   const ownerId = chat.owner?._id
+
+   const chatKickAudio = new Audio(chatSound)
+   chatKickAudio.play()
+
    store.dispatch(actionChatLeft(chat)) 
 })