Pārlūkot izejas kodu

chats&msgs & rebuilded reducer

Ivar 2 gadi atpakaļ
vecāks
revīzija
b4171c2d55

+ 3 - 2
src/App.js

@@ -16,7 +16,7 @@ import {
   Main,
   AsidePage,
   CChatsPage,
-  CMsgPage
+  MsgPage
 } from "./pages"
 
 import Grid from '@mui/material/Grid'
@@ -38,6 +38,7 @@ const AuthSwitch = ({ token }) => {
           <Grid item xs={12} sm={4}>
               <AsidePage>
                 <Switch> 
+                  <Route path="/main/:_id" component={CChatsPage} /> 
                   <Route path="/find" component={PageNoChat} />
                   <Route path="*" component={CChatsPage} /> 
                 </Switch>
@@ -47,7 +48,7 @@ const AuthSwitch = ({ token }) => {
           <Grid item xs={12} sm={8}>
             <Switch> 
               <Route path="/main" component={PageNoChat} exact/>
-              <Route path="/main/:_id" component={CMsgPage} />
+              <Route path="/main/:_id" component={MsgPage} />
               <Route path="*" component={PageNoChat} /> 
             </Switch>
           </Grid>

+ 6 - 0
src/App.scss

@@ -0,0 +1,6 @@
+
+
+
+.chatItem:hover {
+   background-color: #eee;
+}

+ 33 - 4
src/actions/chatsActions.js

@@ -5,6 +5,7 @@ import {
    actionChatOne,
    actionChatLeft
 } from '../reducers'
+import { actionGetAllLastMsg } from './msgActions'
 
 
 // в массив newMemders передавать объекты только с полем _id
@@ -68,15 +69,12 @@ export const actionGetChatsByUser = (userId, skipCount=0, limitCount=50) => (
 )
 
 
-
-
 export const actionFullChatList = (userId, currentCount, limitCount=50) => (
    async (dispatch) => {
       let payload = await dispatch(actionGetChatsByUser(userId, currentCount, limitCount))
       if (payload) {
-
          await dispatch(actionChatList(payload))
-
+         await dispatch(actionGetAllLastMsg(payload))
       }
    }
 )
@@ -84,6 +82,37 @@ export const actionFullChatList = (userId, currentCount, limitCount=50) => (
 
 
 
+export const actionGetChatById = (chatId) => (
+   actionPromise('chatById', gql(`query chatById($q: String) {
+      ChatFindOne (query: $q){
+         _id
+         title
+         avatar {
+            url
+         }
+         owner {
+            login
+         }
+         members {
+            _id
+            login
+         }
+            messages {
+               _id
+               owner {
+                  login
+               }
+               text
+            }
+         lastModified
+      }     
+   }`, { 
+         q: JSON.stringify([ { _id: chatId } ])
+      }
+   ))
+)
+
+
 
 
 

+ 4 - 0
src/actions/index.js

@@ -10,6 +10,7 @@ import {
    actionUpdateChat,
    actionGetChatsByUser,
    actionGetAllChats,
+   actionGetChatById,
 
    actionFullChatList
 } from './chatsActions'
@@ -17,6 +18,7 @@ import {
    actionGetMsgsByChat,
    actionMsgsCount,
    actionSendMsg,
+   actionGetAllLastMsg,
 
    actionFullMsgsByChat,
 } from './msgActions'
@@ -41,6 +43,7 @@ export {
    actionUpdateChat,
    actionGetChatsByUser,
    actionGetAllChats,
+   actionGetChatById,
 
    actionFullChatList,
 }
@@ -48,6 +51,7 @@ export {
    actionGetMsgsByChat,
    actionMsgsCount,
    actionSendMsg,
+   actionGetAllLastMsg,
 
    actionFullMsgsByChat,
 }

+ 44 - 32
src/actions/msgActions.js

@@ -2,11 +2,15 @@ import {gql} from '../helpers'
 import {   
    actionPromise,
    actionMsgList,
-   actionMsgOne
+   actionMsgOne,
+   actionChatOne
 } from '../reducers'
 import {   
-   actionUploadFile
+   actionUploadFile,
 } from './mediaActions'
+import {   
+   actionGetChatById
+} from './chatsActions'
 
 
 
@@ -18,15 +22,22 @@ export const actionGetMsgsByChat = (chatId, skipCount=0, limitCount=50) => (
          owner {
             _id
             login
+            nick
+            avatar {
+               url
+            }
          }
          text
          chat {
             _id
          }
-
          media {
+            _id
             url
+            type
+            originalFileName            
          }
+
          forwardWith {
             _id
          }
@@ -67,7 +78,7 @@ export const actionFullMsgsByChat = (chatId, currentCount, limitCount=50) => (
 
 const actionLastMsgByChat = (chatId) => (
    actionPromise('lastMsg', gql(`query lastMsg($q: String) {
-      MessageFindOne (query: $q){
+      MessageFind (query: $q){
          _id
          createdAt
          owner {
@@ -78,28 +89,18 @@ const actionLastMsgByChat = (chatId) => (
          chat {
             _id
          }
-
          media {
             url
-         }
-         forwardWith {
-            _id
-         }
-         replies {
-            _id
-         }
-
-         replyTo {
-            _id
-         }
-         forwarded {
-            _id
+            originalFileName
+            type
          }
       }     
    }`, { 
-         q: JSON.stringify([  { chat: {_id: chatId} },
+         q: JSON.stringify([  { "chat._id": chatId },
                               { 
-                                 sort: [{_id: -1}]
+                                 sort: [{_id: -1}],
+                                 skip: [0], 
+                                 limit: [1] 
                               } 
                            ])
       }
@@ -112,9 +113,9 @@ export const actionGetAllLastMsg = (chats) => (
       let msgReq = chats.map((chat) => dispatch(actionLastMsgByChat(chat._id)))
       let msgRes = await Promise.all(msgReq)
 
-      if (msgRes) {
-         for (const msg of msgRes) {
-            await dispatch(actionMsgOne(msg))
+      if (msgRes) {        
+         for (const msg of msgRes.flat()) {
+            dispatch(actionMsgOne(msg))
          }
       }
    }
@@ -128,7 +129,7 @@ export const actionMsgsCount = (chatId) => (
    actionPromise('msgsCount', gql(`query msgsCount($q: String) {
       MessageCount (query: $q)  
    }`, { 
-         q: JSON.stringify([ { chat: { _id: chatId } } ])
+         q: JSON.stringify([ { "chat._id": chatId } ])
       }
    ))
 )
@@ -143,15 +144,23 @@ const actionUpdateMsg = (chatId, text, media, msgId) => (
          owner {
             _id
             login
+            nick
+            avatar {
+               url
+            }
          }
          text
          chat {
             _id
          }
-
          media {
+            _id
             url
+            type
+            originalFileName            
          }
+
+
          forwardWith {
             _id
          }
@@ -173,8 +182,7 @@ const actionUpdateMsg = (chatId, text, media, msgId) => (
 
 // медиа - массив объектов с ид медиа
 export const actionSendMsg = (chatId, text, inputName, files, msgId) => (
-   async (dispatch, getState) => {
-
+   async (dispatch) => {
 // тут нужно отделить уже залитые файлы от тех которые лежат локально
 // локальные залить и получить ид, с залитых просто получить ид
       const media = []
@@ -183,17 +191,21 @@ export const actionSendMsg = (chatId, text, inputName, files, msgId) => (
             let fileObj = await dispatch(actionUploadFile(inputName, file))
             media.push({_id: fileObj?._id})
          } else {
-            media.push({})
+            let fileObj = file
+            media.push({_id: fileObj?._id})
          }
       }
-
-      await dispatch(actionUpdateMsg(chatId, text, media, msgId))
+      let payload = await dispatch(actionUpdateMsg(chatId, text, media, msgId))
+      if (payload) {
+         await dispatch(actionMsgOne(payload))
+         let chatUpdated = await dispatch(actionGetChatById(chatId))
+         await dispatch(actionChatOne(chatUpdated))
+      }
    }
 )
 
 
-
-   
+  
    // const actionRemoveMsg = (msgId) => (
    //    actionPromise('removeMsg', gql(`mutation removeMsg($msg: MessageInput) {
    //       MessageDelete(message: $msg) {

+ 43 - 88
src/components/Avatar.jsx

@@ -1,113 +1,68 @@
-import React from 'react';
+import React, { useRef } from 'react';
 import Avatar from '@mui/material/Avatar';
 
-import {backURL}  from '../helpers'
-import {connect}  from 'react-redux'
-
+import { backURL, stringColor }  from '../helpers'
+import { connect }  from 'react-redux'
 
 const small = {
   height: '40px', 
   width: '40px'
 }
-
 const big = {
   height: '70px', 
   width: '70px'
 }
 
 
-function stringToColor(string) {
-  if (!string) {
-    return '#' + Math.floor(Math.random()*16777215).toString(16);
-  }
-  let hash = 0;    
-  for (let i = 0; i < string?.length; i += 1) {
-    hash = string.charCodeAt(i) + ((hash << 5) - hash);
-  }    
-  let color = '#'; 
-  for (let i = 0; i < 3; i += 1) {
-    const value = (hash >> (i * 8)) & 0xff;
-    color += `00${value.toString(16)}`.substr(-2);
-  }   
-  return color;
-}
-
-function stringSplit(str) {
-  if (!str) {
-    return 
-  }
-  let titleStr = ''
-  let letterAmount = 2
-  for (const word of str.split(' ')) {
-    if (letterAmount <= 0) {
-      break
-    }
-    letterAmount--
-    if (word) {
-      titleStr += word[0].toUpperCase()
-    }  
-  }
-  return titleStr
-}
-
-function stringAvatar(name) { 
-  return {
-    sx: {
-      bgcolor: stringToColor(name),
-    },
-    children: stringSplit(name)
-  };
-}
-
-
 export const UserAvatar = ({  profile, bigSize=false }) => {
   //  console.log(profile)
    
-    function getUrl() {
-      if (profile.localUrl) {
-        return profile.localUrl
-      } else if (profile.avatar?.url) {
-        return backURL + profile.avatar?.url
-      } else {
-        return false
-      }
+  function getUrl() {
+    if (profile.localUrl) {
+      return profile.localUrl
+    } else if (profile.avatar?.url) {
+      return backURL + profile.avatar?.url
+    } else {
+      return false
     }
+  }
 
-   return (
-      <>
-      {
-         getUrl() ?
-         <Avatar  sx={ bigSize ? big : small } 
-                  alt={profile.nick || profile.login } src={getUrl()} /> :
-         <Avatar  sx={ bigSize ? big : small }
-                  {...stringAvatar(profile.nick || profile.login)} /> 
-      }        
-      </>
-   ) 
+  return (
+    <>
+    {
+        getUrl() ?
+        <Avatar  sx={ bigSize ? big : small } 
+                alt={profile.nick || profile.login } src={getUrl()} /> :
+        <Avatar  sx={ bigSize ? big : small }
+                {...stringColor.stringAvatar(profile.nick || profile.login)} /> 
+    }        
+    </>
+  ) 
 }
-export const CUserAvatar = connect( state => ({profile: state.promise.myProfile?.payload || {}}))(UserAvatar)
+export const CMyAvatar = connect( state => ({profile: state.promise.myProfile?.payload || {}}))(UserAvatar)
 
 
 
 export const ChatAvatar = ({ chat, bigSize=false }) => {
-   
-    function getUrl() {
-       if (chat.avatar?.url) {
-        return backURL + chat.avatar?.url
-      } else {
-        return false
-      }
+ 
+  function getUrl() {
+      if (chat.avatar?.url) {
+      return backURL + chat.avatar?.url
+    } else {
+      return false
     }
+  }
+
+  return (
+    <>
+    {
+        getUrl() ?
+        <Avatar  sx={ bigSize ? big : small } 
+                alt={chat.title } src={getUrl()} /> :
+        <Avatar  sx={ bigSize ? big : small }
+                {...stringColor.stringAvatar(chat.title)} /> 
+    }        
+    </>
+  ) 
+}
 
-   return (
-      <>
-      {
-         getUrl() ?
-         <Avatar  sx={ bigSize ? big : small } 
-                  alt={chat.title } src={getUrl()} /> :
-         <Avatar  sx={ bigSize ? big : small }
-                  {...stringAvatar(chat.title)} /> 
-      }        
-      </>
-   ) 
-}

+ 65 - 12
src/components/ChatList.jsx

@@ -10,37 +10,90 @@ import {Link} from 'react-router-dom'
 import { FloatBtn, ChatAvatar } from "../components"
 import { connect }  from 'react-redux'
 
+const selectedChat = {
+  color: "#fff",
+  backgroundColor: "#1976d2dd" 
+}
+
+const notSelectedChat = {
+  color: "#1976d2",
+  backgroundColor: "#fff"
+}
+
 
-const Chat = ({chat}) => {
+const Chat = ({ chat, currChat }) => {
 
-  const {_id, title, owner, members} = chat
+  const [msgText, setMsgText] = useState( chat.messages && chat.messages[0]?.text || 'нема')
+
+  // const {_id, title, owner, members, messages} = chat
+  // const { text, owner: msgOwner, media, createdAt } = messages
+
+  useEffect(() => {
+    setMsgText( chat.messages && chat.messages[0]?.text || 'нема')
+  }, [chat])
 
   return (
-    <Link style={{textDecoration: 'none', color: "#1976d2"}} to={`/main/${_id}`}>
-      <ListItem >
+    <Link 
+      style={{ textDecoration: 'none' }}
+      to={`/main/${chat._id}`}
+      >
+      <ListItem 
+        className="chatItem"
+        style={ currChat ? selectedChat : notSelectedChat }
+        
+        >
         <ListItemAvatar>
           <ChatAvatar chat={chat} />
         </ListItemAvatar>
-        <ListItemText primary={title} secondary={'затычка'} />
+        <ListItemText 
+          primary={chat.title} 
+          secondary={msgText} />
       </ListItem>
+
+      {/* <div
+        style={ currChat ? selectedChat : notSelectedChat }
+        >
+          <strong>
+            {chat.title}
+          </strong>         
+          <div>
+            {msgText}
+          </div>
+          
+      </div> */}
     </Link>
   )
 }
 
-const ChatList = ({ chats=[] }) => {
+const ChatList = ({ chats=[], currChatId }) => {
+
+  // const [chatsInput, setChats] = useState(chats)
+
+  // useEffect(() => {
+  //   setChats(chats)
+  // }, [chats])
 
   return (
       <List        
         sx={{ width: '100%', bgcolor: 'background.paper', position: 'relative', zIndex: 2, }}
         >
-        <Box sx={{  position: 'fixed', top: '90%', left: '25%', zIndex: 10}} >
-          <FloatBtn />
-        </Box>
-        {chats.map(chat => <Chat key={chat._id} chat={chat}/>)}  
+          <div>
+            <Box sx={{  position: 'fixed', top: '90%', left: '25%', zIndex: 10}} >
+              <FloatBtn />
+            </Box>
+
+              {chats.map(chat =>          
+                  (currChatId === chat._id) ? 
+                    <Chat key={chat._id} chat={chat} currChat={true} /> :
+                      <Chat key={chat._id} chat={chat} currChat={false} />
+              )}  
+          </div>  
+        </List>
+
+    
       
-      </List>
   )
 }
-export const CChatList = connect( state => ({ chats: Object.values(state.chats) }))(ChatList)
+export const CChatList = connect( state => ({ chats: Object.values(state.chats) || [] }))(ChatList)
 
 

+ 2 - 2
src/components/MainMenu.jsx

@@ -15,7 +15,7 @@ import ListItemText from '@mui/material/ListItemText';
 import {actionFullLogout} from "../actions"
 import {connect}  from 'react-redux';
 
-import {CProfileModal, CUserAvatar} from '.'
+import {CProfileModal, CMyAvatar} from '.'
 
 
 const LogoutBtn = ({onLogout}) => (
@@ -115,7 +115,7 @@ export const MenuDrawer = ({}) => {
             role="presentation"
             >
                <Box sx={{ m: 2, display: 'flex', justifyContent: 'center' }}>
-                  <CUserAvatar bigSize={true} />
+                  <CMyAvatar bigSize={true} />
                </Box>               
 
                <List>

+ 206 - 12
src/components/MsgList.jsx

@@ -1,43 +1,237 @@
 import React, {useState, useEffect, useRef} from 'react'
 
 import { connect } from 'react-redux'
+import { dateFromStamp, stringColor, backURL } from '../helpers'
+
+import { UserAvatar } from '.'
 
-import { actionFullMsgsByChat } from '../actions'
 
 const msgsWrapper = {
    display: "flex",
    flexDirection: "column-reverse",
    justifyContent: "flex-start",
    alignItems: "stretch",
+   paddingTop: "10px",
+   paddingBottom: "5px"
+}
+
+const msgBlock = {
+   alignSelf: "start",
+   display: "flex",
+   justifyContent: "flex-start",
+   alignItems: "start",
+   margin: "2px",
+   marginRight: "5%",
+   maxWidth: "calc(95% - 2px)",
+   minWidth: "40%",
+   wordWrap: "break-word",
+}
+
+const myMsgBlock = {
+   alignSelf: "end",
+   display: "flex",
+   justifyContent: "flex-start",
+   alignItems: "start",
+   margin: "2px",
+   marginLeft: "5%",
+   maxWidth: "calc(95% - 2px)",
+   minWidth: "40%",
+   wordWrap: "break-word",
+}
+
+const avBlock = {
+   alignSelf: "end",
+   width: "40px",
+   marginRight: "10px"
+}
+
+const bodyBlock = {
+   display: "flex",
+   flexDirection: "column",
+   justifyContent: "flex-start",
+   borderRadius: "5px",
+   backgroundColor: "#fff",
+   width: "calc(100% - 50px)",
+   padding: "10px"
+}
+
+const myBodyBlock = {
+   display: "flex",
+   flexDirection: "column",
+   justifyContent: "flex-start",
+   borderRadius: "5px",
+   backgroundColor: "#1976d255",
+   width: "calc(100% - 50px)",
+   padding: "10px"
+}
+
+const nameBlock = {
+   fontWeight: 600,
+   fontSize: "12px",
+   alignSelf: "start",
+   maxWidth: "100%",
+   marginBottom: "5px",
 }
+const contentBlock = {
+   alignSelf: "stretch",
+   maxWidth: "100%",
+}
+const dateBlock = {
+   alignSelf: "end",
+   maxWidth: "100%",
+   fontSize: "10px",
+   color: "#555"
+}
+
 
 
-const Msg = ({msg}) => {
 
-   const {_id, text} = msg
+const Msg = ({ msg, prevOwner, prevTime, myId }) => {
 
+   const { _id, text, owner, media, createdAt } = msg
+   const {nick, login, avatar} = owner
+
+   const allMedia = {}
+   if (media) for (const file of media) {
+      if (file.type) {
+
+         const objName = file.type.split('/')[0]
+         if (allMedia.hasOwnProperty(objName)) {
+
+            allMedia[objName].push(file)
+         } else {
+
+            allMedia[objName] = [file]
+         }
+      }  
+   }
+
+   // console.log(allMedia)
+
+
+
+   const prOwner = prevOwner.current
+   prevOwner.current = owner._id
+
+   const prTime = prevTime.current
+   prevTime.current = createdAt
+
+   // console.log( prTime - createdAt,  text)
+   const nameBlockNew = {...nameBlock, color: stringColor.stringToColor(nick || login)}
    return (
       <>
-         <p>
-            {text}
-         </p> 
+         <div 
+            style={(myId === owner._id) ? 
+               ( (prOwner === owner._id) ? 
+                  {...myMsgBlock, marginBottom: "2px"} : {...myMsgBlock, marginBottom: "15px"}) : 
+                  ( (prOwner === owner._id) ? 
+                     {...msgBlock, marginBottom: "2px"} : {...msgBlock, marginBottom: "15px"})
+               }
+            >
+            <div style={avBlock} >
+               { (prOwner === owner._id && 
+                     prTime - createdAt < 600000) ? 
+                           <></> : <UserAvatar profile={owner} /> }
+            </div>
+            <div style={(myId === owner._id) ? myBodyBlock : bodyBlock} >
+
+               <div style={(myId === owner._id) ? nameBlock : nameBlockNew} >
+                  {nick || login}
+               </div>
+
+               <div style={contentBlock} >
+
+                  {
+                     (media && media.length !== 0) &&
+                           <div>
+                              {media.map((mediaObj) => <img key={mediaObj._id}
+                                                            // type={mediaObj.type}
+                                                            style={{maxWidth: "400px"}}
+                                                            src={backURL + mediaObj.url } />
+                                                                                             )}
+                           </div> 
+                  }
+
+                  <pre>
+                     {text}
+                  </pre>
+                  
+               </div>
+
+               <div style={dateBlock} >
+                  {dateFromStamp(createdAt)}
+               </div>
+            </div>
+         </div> 
       </>
    )
 }
 
 
-const MsgList = ({chats, chatId}) => {
+const MsgList = ({chats={}, chatId, myId }) => {
 
-   const msgArr =  chats[chatId]?.messages
+   const prevOwner = useRef('')
+   const prevTime = useRef('')
+
+   useEffect(() => {
+      prevOwner.current = ''
+      prevTime.current = ''
+   })
+
+   // надо ли писать реф?
+   const msgArr = chats[chatId]?.messages
 
    return (
-      <div style={msgsWrapper}>
+      <div 
+         style={msgsWrapper}
+         >
          { msgArr ?
-            msgArr.map(msg => <Msg key={msg._id} msg={msg}/>) :
-               <div>сообщений нема</div>         
+
+            msgArr.map(msg => <Msg               
+                                 key={msg._id} 
+                                 msg={msg} 
+                                 prevOwner={prevOwner}
+                                 prevTime={prevTime}
+                                 myId={myId}
+                               />) :
+               
+                     <div>сообщений нема</div>         
          }  
 
       </div>
    )
 }
-export const CMsgList = connect(state => ({chats: state.chats}))(MsgList)
+export const CMsgList = connect(state => ( { chats: state.chats,
+                                             myId: state.auth.payload?.sub?.id || null} ))(MsgList)
+
+
+//tg
+
+{/* <div class="media-inner dark interactive" style="">
+   <canvas class="thumbnail" width="1304" height="720" style="width: 480px; height: 265px;">
+   </canvas>
+   <img class="thumbnail opacity-transition slow" alt="" 
+      draggable="true" style="width: 480px; height: 265px;">
+   <video class="full-media" width="480" height="265" autoplay="" loop="" playsinline="" draggable="true">
+      <source src="blob:https://web.telegram.org/464cc156-4e7f-415e-bec5-5fdf5b6c2d98">
+   </video>
+   <div class="message-media-duration">5:27</div>
+</div> */}
+
+
+{/* <div class="File interactive">
+   <div class="file-icon-container">
+      <div class="file-icon orange">
+         <span class="file-ext" dir="auto">
+            zip
+         </span>
+            </div>
+               <i class="action-icon icon-download"></i>
+            </div>
+         <div class="file-info">
+            <div class="file-title" dir="auto">2.1 01-02.zip</div>
+            <div class="file-subtitle" dir="auto">
+               <span>0.6 KB</span>
+            </div>
+         </div>
+      </div> */}

+ 25 - 6
src/components/SendingField.jsx

@@ -104,7 +104,7 @@ const thumbsContainer = {
 
 
 
-const MsgDropZone = ({setText, setFiles, files}) => {
+const MsgDropZone = ({ setText, setFiles, files, text, onEnter }) => {
 
    const {
       acceptedFiles,
@@ -170,13 +170,22 @@ const MsgDropZone = ({setText, setFiles, files}) => {
 
                   <AttachFileIcon fontSize="large" sx={{ cursor: 'pointer' }} />
                   
-                  <TextareaAutosize                     
+                  <TextareaAutosize  
+                     value={text}                   
                      minRows={4}
                      maxRows={10}
-                     placeholder="Сообщение"
+                     placeholder="Написать сообщение..."
                      style={{ width: '100%' }}
                      onClick={(e) => e.stopPropagation()}
                      onChange={(e) => setText(e.target.value)}
+                     onKeyDown={(e) => {
+                        if (e.keyCode === 13 && !e.shiftKey) {                           
+                           (text.match(/^\s*$/) && files.length === 0) ||
+                                 onEnter()
+                           setText('')
+                           setFiles([])
+                        }
+                     }}
                   />
             </div>
 
@@ -187,7 +196,7 @@ const MsgDropZone = ({setText, setFiles, files}) => {
 }
 
 
-const SendingField = ({onSend}) => {
+const SendingField = ({ chatId, onSend }) => {
    const [text, setText] = useState('')
    const [files, setFiles] = useState([])
 
@@ -197,7 +206,12 @@ const SendingField = ({onSend}) => {
       
          <Box sx={{ flexGrow: 1, flexShrink: 1, overflow: 'auto', backgroundColor: '#fff' }}>
             
-            <MsgDropZone setText={setText} setFiles={setFiles} files={files} />
+            <MsgDropZone 
+               setText={setText} 
+               setFiles={setFiles} 
+               files={files} 
+               text={text} 
+               onEnter={() => onSend(chatId, text.trim(), "media", files)} />
          
          </Box>         
          <Box sx={{ flexGrow: 1, flexShrink: 1}}>
@@ -205,7 +219,12 @@ const SendingField = ({onSend}) => {
                sx={{ width: "100%" }}
                variant="contained" 
                endIcon={<SendIcon />} 
-               onClick={() => onSend(null, text, "media", files)}
+               onClick={() => {
+                  (text.match(/^\s*$/) && files.length === 0) ||
+                        onSend(chatId, text.trim(), "media", files) 
+                  setText('')
+                  setFiles([])
+               }}
             >
                Отправить
             </Button>

+ 2 - 2
src/components/index.js

@@ -6,7 +6,7 @@ import {MainMenu, MenuDrawer} from './MainMenu'
 import {Header} from './Header'
 import {SearchBlock} from './SearchBlock'
 import {FloatBtn} from './FloatBtn'
-import {ChatAvatar, UserAvatar, CUserAvatar} from './Avatar'
+import {ChatAvatar, UserAvatar, CMyAvatar} from './Avatar'
 import {CProfileModal} from './ProfileModal'
 
 import {CSendingField} from './SendingField'
@@ -20,7 +20,7 @@ export {MainMenu, MenuDrawer}
 export {Header}  
 export {SearchBlock}  
 export {FloatBtn}  
-export {ChatAvatar, UserAvatar, CUserAvatar} 
+export {ChatAvatar, UserAvatar, CMyAvatar} 
 export {CProfileModal} 
 
 export {CSendingField}  

+ 21 - 0
src/helpers/dateFuncs.js

@@ -0,0 +1,21 @@
+
+
+
+
+export function dateFromStamp(createdAt) {
+   let date = new Date(Number(createdAt))
+   // console.log(createdAt, date)
+   let dayDate = (date.getDate() < 10) ? '0' + date.getDate() : date.getDate() 
+   let yearDate = date.getFullYear() 
+
+   let monthDate = date.getMonth()
+   let months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
+   let curMonth = months[monthDate] 
+
+   let hours = (date.getHours() < 10) ? '0' + date.getHours()  : date.getHours() 
+   let minutes = (date.getMinutes() < 10) ? '0' + date.getMinutes()  : date.getMinutes()
+
+   let dateInFormat = hours + ':' + minutes + ' - ' + dayDate + '.' + curMonth + '.' +  yearDate
+
+   return dateInFormat
+}

+ 8 - 2
src/helpers/index.js

@@ -2,9 +2,15 @@
 import {backURL, gql} from './getGql'
 import {printStrReq} from './printStrReq'
 import {passReq} from './passReq'
-
+import {dateFromStamp} from './dateFuncs'
+import {stringColor} from './stringColorFuncs'
 
 
 export {backURL, gql} 
 export {printStrReq} 
-export {passReq} 
+export {passReq} 
+export {dateFromStamp} 
+export {stringColor} 
+
+
+

+ 49 - 0
src/helpers/stringColorFuncs.js

@@ -0,0 +1,49 @@
+
+
+export const stringColor = {
+
+   stringToColor(string) {
+      if (!string) {
+        // return '#' + Math.floor(Math.random()*16777215).toString(16);
+        return '#aaa'
+      }
+      let hash = 0;    
+      for (let i = 0; i < string?.length; i += 1) {
+        hash = string.charCodeAt(i) + ((hash << 5) - hash);
+      }    
+      let color = '#'; 
+      for (let i = 0; i < 3; i += 1) {
+        const value = (hash >> (i * 8)) & 0xff;
+        color += `00${value.toString(16)}`.substr(-2);
+      }   
+      return color;
+    },
+    
+    stringSplit(str) {
+      if (!str) {
+        return 
+      }
+      let titleStr = ''
+      let letterAmount = 2
+      for (const word of str.split(' ')) {
+        if (letterAmount <= 0) {
+          break
+        }
+        letterAmount--
+        if (word) {
+          titleStr += word[0].toUpperCase()
+        }  
+      }
+      return titleStr
+    },
+    
+    stringAvatar(name) { 
+      return {
+        sx: {
+          bgcolor: this.stringToColor(name),
+        },
+        children: this.stringSplit(name)
+      };
+    }
+}
+

+ 12 - 0
src/index.scss

@@ -11,3 +11,15 @@ code {
   font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
     monospace;
 }
+
+pre {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+  'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+  sans-serif;
+  word-wrap: break-word;
+  white-space: pre-wrap;
+  white-space: -moz-pre-wrap; 
+  white-space: -pre-wrap;     
+  white-space: -o-pre-wrap;
+}

+ 7 - 6
src/pages/ChatsPage.jsx

@@ -2,7 +2,7 @@ import React, {useState, useEffect, useRef} from 'react';
 
 import { connect }  from 'react-redux'
 import {
-  actionFullChatList
+  actionFullChatList,
 } from "../actions"
 
 import { CChatList } from '../components'
@@ -12,13 +12,13 @@ const chatsPage= {
    overflowY: "auto"
  }
 
-const ChatsPage = ({auth, getChats, chatsCount=50}) => {
+const ChatsPage = ({ match:{params:{_id}}, auth, getChats, chatsCount=50 }) => {
    const [chatBlock, setChatBlock] = useState(0)
  
-   useEffect(() => {
+   useEffect(async () => {
      const userId = auth.payload?.sub?.id 
      if (userId) {
-       getChats(userId, chatBlock, chatsCount)
+      await getChats(userId, chatBlock, chatsCount)
      } 
    },[chatBlock])
  
@@ -32,10 +32,11 @@ const ChatsPage = ({auth, getChats, chatsCount=50}) => {
        }}
        >
 
-         <CChatList />
+         <CChatList currChatId={_id} />
 
      </div>
    )
  }
  export const CChatsPage= connect( state => ({ auth: state.auth }),
-                                         {getChats: actionFullChatList})(ChatsPage)
+                                          { getChats: actionFullChatList
+                                                })(ChatsPage)

+ 28 - 9
src/pages/MsgPage.jsx

@@ -1,4 +1,4 @@
-import React, {useState, useEffect} from 'react';
+import React, {useState, useEffect, useRef} from 'react';
 
 import {
    Header, 
@@ -31,6 +31,8 @@ const msgPageBody = {
 const msgs = {
    flexGrow: 1,
    overflowY: "auto",
+   display: "flex",
+   flexDirection: "column-reverse",
 }
 const msgSend = {
    flexGrow: 0,
@@ -38,11 +40,14 @@ const msgSend = {
    maxHeight: "60%",
 }
 
-const MsgPage = ({ match:{params:{_id}}, getData }) => {
+const MsgBlock = ({ chatId, getMsgs, msgsCount=20 }) => {
+
+   const [msgsBlock, setMsgsBlock] = useState(0)
 
+ 
    useEffect(() => {
-      getData(_id, 0, 50)
-   },[_id])
+      getMsgs(chatId, msgsBlock, msgsCount)
+   },[msgsBlock, chatId])
 
    return (
       <>  
@@ -50,7 +55,8 @@ const MsgPage = ({ match:{params:{_id}}, getData }) => {
 
             <div style={msgPageHeader}>
                <Header> 
-                  <div>
+                  <div
+                     >
                      шапка2: инфо о чате и управление, управление сообщениями
                   </div>
                </Header>  
@@ -58,12 +64,19 @@ const MsgPage = ({ match:{params:{_id}}, getData }) => {
 
             <div style={msgPageBody}>
 
-               <div style={msgs}>
-                  <CMsgList key={_id} chatId={_id} />
+               <div 
+                  style={msgs}
+                  onScroll={(e) => {
+                     if (e.target.scrollTop*(-1) >= e.target.scrollHeight - e.target.clientHeight) {
+                        setMsgsBlock(msgsBlock => msgsBlock + msgsCount)
+                     }
+                  }}
+                  >
+                  <CMsgList chatId={chatId} />
                </div>
 
                <div style={msgSend}>
-                  <CSendingField key={_id} chatId={_id} />
+                  <CSendingField chatId={chatId} />
                </div>       
 
             </div>
@@ -72,4 +85,10 @@ const MsgPage = ({ match:{params:{_id}}, getData }) => {
       </>
    )
 }
-export const CMsgPage = connect( null, {getData: actionFullMsgsByChat})(MsgPage)
+const CMsgBlock = connect( null, {getMsgs: actionFullMsgsByChat})(MsgBlock)
+
+export const MsgPage = ({ match:{params:{_id}} }) => (
+   <>
+      <CMsgBlock chatId={_id} key={_id} />
+   </>
+)

+ 2 - 2
src/pages/index.js

@@ -4,7 +4,7 @@ import {Register} from './Register'
 import {Main} from './Main'
 import {AsidePage} from './AsidePage'
 import {CChatsPage} from './ChatsPage'
-import {CMsgPage} from './MsgPage'
+import {MsgPage} from './MsgPage'
 
 
 export {Login} 
@@ -12,5 +12,5 @@ export {Register}
 export {Main} 
 export {AsidePage} 
 export {CChatsPage} 
-export {CMsgPage} 
+export {MsgPage} 
 

+ 51 - 27
src/reducers/chatsReducer.js

@@ -20,19 +20,62 @@
         return {}
     }
 
+    function refreshMsgs(newMsgs, oldMsgs) {
+      const msgState = oldMsgs
+
+      for (const newMsg of newMsgs || []) {   
+        const currIndex = msgState.findIndex(oldMsg => oldMsg._id === newMsg._id)
+
+        if (currIndex === -1) {
+          msgState.push(newMsg)                
+        } else {
+          msgState[currIndex] = newMsg
+        }
+    }
+
+      const newMsgState = msgState.sort((a, b) => {
+        if (a._id > b._id) {
+          return -1
+        }
+        if (a._id < b._id) {
+          return 1
+        }
+        return 0
+      })
+
+      return newMsgState
+    }
+
+
     const types = {
 
       CHATS() {
-        if (payload && payload.length > 0) {
+        if (payload) {
+
+          const oldChats = {...state}
 
-          const newChats = {...state}     
           for (const chat of payload) {
-            newChats[chat._id] = chat
-          }
 
+            const oldMsgs = oldChats[chat._id]?.messages 
+            const newMsgs = chat?.messages
+
+            if (oldMsgs && newMsgs && oldMsgs.length > 0) {
+
+
+              chat.messages = refreshMsgs(newMsgs, oldMsgs)
+
+              oldChats[chat._id] = chat
+     
+            } else {
+
+
+              oldChats[chat._id] = chat
+            }
+          }
+          
           const newState = Object.fromEntries(
-            Object.entries(newChats).sort((a, b) => {
-              if (a[1].lastModified > b[1].lastModified) {
+            Object.entries(oldChats).sort((a, b) => {
+              if (a[1].lastModified > b[1].lastModified) {                
                 return -1
               }
               if (a[1].lastModified < b[1].lastModified) {
@@ -42,6 +85,7 @@
             })
           )
 
+          // console.log(newState, 'dasdasdasdasd')
           return newState
         }
         return state
@@ -59,25 +103,7 @@
 
           const msgState = state[chatId]?.messages || []
           
-          for (const newMsg of payload || []) {   
-              const currIndex = msgState.findIndex(oldMsg => oldMsg._id === newMsg._id)
-
-              if (currIndex === -1) {
-                msgState.push(newMsg)                
-              } else {
-                msgState[currIndex] = newMsg
-              }
-          }
-
-          const newMsgState = msgState.sort((a, b) => {
-            if (a._id > b._id) {
-              return -1
-            }
-            if (a._id < b._id) {
-              return 1
-            }
-            return 0
-          })
+          const newMsgState = refreshMsgs(payload, msgState)
 
           const newState = {
             ...state,
@@ -123,5 +149,3 @@
 
 
 
-
-