Pārlūkot izejas kodu

add scroll with update messages

Vitalii Polishchuk 3 gadi atpakaļ
vecāks
revīzija
9b09a6b812

+ 38 - 5
src/App.css

@@ -76,18 +76,21 @@ main {
   margin-bottom: 5px;
 }
 
-.msg-user,
-.msg-someone {
+.msg {
   display: flex;
   flex-direction: column;
   border: 1px solid black;
-  margin: 10px;
-  list-style: none;
   border-radius: 10px;
+  background: honeydew;
   padding: 5px;
+}
+
+.msg-user,
+.msg-someone {
+  margin: 10px;
+  list-style: none;
   max-width: 300px;
   min-width: 300px;
-  background: honeydew;
 }
 
 .msg-user {
@@ -185,3 +188,33 @@ textarea {
 .msg-media span {
   font-size: 10px;
 }
+
+.msg-short {
+  display: flex;
+  align-items: flex-end;
+  background-color: #fff;
+  border: 1px solid #000;
+}
+
+.msg-short .msg-nick {
+  flex-grow: 1;
+}
+
+.msg-short .msg-date {
+  flex-grow: 0;
+}
+
+.msg-short .msg-text {
+  flex-grow: 0;
+}
+
+.msg-reply {
+  background: grey;
+  border: 1px solid black;
+  border-radius: 10px;
+  padding: 5px;
+}
+
+.btn-scroll {
+  position: fixed;
+}

+ 44 - 28
src/actions/index.js

@@ -78,7 +78,7 @@ let chatMSG = async (id) => {
            url _id type
          } owner{
           login _id nick
-          } replies {
+          } replyTo {
             _id text createdAt owner{
               login _id nick
               }
@@ -107,19 +107,23 @@ let msgCount = async (chat_id) => {
   return result
 }
 
-let chatSortMSG = async (id) => {
-  let query = `query getMSGWithSort($query: String){
-        MessageFind(query: $query){
-         _id text createdAt owner{
+let chatSortMSG = async (id, skipCount) => {
+  let query = `query getMSG($query: String){
+    MessageFind(query: $query){
+     _id text createdAt media{
+       url _id type
+     } owner{
+      login _id nick
+      } replyTo {
+        _id text createdAt owner{
           login _id nick
-        } forwarded{
-          _id text
-        }
-        }
-      }`
+          }
+      }
+    }
+  }`
 
   let qVariables = {
-    "query": JSON.stringify([{ "chat._id": id }, { sort: [{ title: -1 }], skip: [100], limit: [100] }])
+    "query": JSON.stringify([{ "chat._id": id }, { sort: [{ _id: -1 }], skip: [skipCount], limit: [30] }])
   }
 
   //меняем skip по событию скролла вверх
@@ -213,13 +217,31 @@ let avatarSet = async (avatar_id, chat_id) => {
   return result
 }
 
-let newMSG = async (chat_id, text, media) => {
-  let mediaQuery = []
+let newMSG = async (chat_id, text, media, replyTo, forwardTo) => {
+  let input = {}
+
+  input.text = text
+  input.media = []
+  input.chat = {
+    "_id": chat_id
+  }
+
+  if (replyTo) {
+    input.replyTo = {
+      "_id": replyTo
+    }
+  }
+
+  if (forwardTo) {
+    input.forwarded = {
+      "_id": forwardTo
+    }
+  }
 
   if (media && Array.isArray(media)) {
-    media.map(item => mediaQuery.push({ "_id": item._id }))
+    media.map(item => input.media.push({ "_id": item._id }))
   } else {
-    mediaQuery.push({})
+    input.media.push({})
   }
 
   let query = `mutation newMSG($msg: MessageInput){
@@ -233,13 +255,7 @@ let newMSG = async (chat_id, text, media) => {
       }`
 
   let qVariables = {
-    "msg": {
-      "text": text,
-      "media": mediaQuery,
-      "chat": {
-        "_id": chat_id
-      }
-    }
+    "msg": input
   }
 
   let result = await gql(query, qVariables)
@@ -375,7 +391,7 @@ export const actionGetFile = (id) => actionPromise("fileFound", fileFound(id))
 
 const actionGetChats = (id) => actionPromise("chats", userChats(id))
 
-const actionGetMessages = (chatID) => actionPromise("messages", chatMSG(chatID))
+const actionGetMessages = (chatID, skipCount) => actionPromise("messages", chatSortMSG(chatID, skipCount))
 
 const actionChats = (chat_id, title, createdAt, lastModified, avatar, messages, members) => ({ type: "CHAT", chat_id, title, createdAt, lastModified, avatar, messages, members })
 
@@ -391,9 +407,9 @@ export const actionAddChatBack = (title, members) => actionPromise("chat", newCh
 
 export const actionAddChat = (chat_id, title, createdAt, lastModified, owner, avatar, messages, members) => ({ type: "CHAT", chat_id, title, createdAt, lastModified, owner, avatar, messages, members })
 
-export const actionAddMSG = (chat_id, msg_id, msg_text, msg_createdAt, msg_owner, msg_media) => ({ type: "MESSAGE", chat_id, msg_id, msg_text, msg_createdAt, msg_owner, msg_media })
+export const actionAddMSG = (socket, chat_id, msg_id, msg_text, msg_createdAt, msg_owner, msg_media, msg_replyTo) => ({ type: "MESSAGE", socket, chat_id, msg_id, msg_text, msg_createdAt, msg_owner, msg_media, msg_replyTo })
 
-export const actionAddMSGBack = (chat_id, text, media) => actionPromise("new_message", newMSG(chat_id, text, media))
+export const actionAddMSGBack = (chat_id, text, media, replyTo, forwardTo) => actionPromise("new_message", newMSG(chat_id, text, media, replyTo, forwardTo))
 
 export const actionSetAvatar = (avatar_id, chat_id) => actionPromise("set_avatar", avatarSet(avatar_id, chat_id))
 
@@ -409,11 +425,11 @@ export const actionFullGetChats = (id) => {
   }
 }
 
-export const actionFullGetMessages = (id) => {
+export const actionFullGetMessages = (id, skip) => {
   return async (dispatch) => {
-    let result = await dispatch(actionGetMessages(id))
+    let result = await dispatch(actionGetMessages(id, skip))
     let messages = result.data.MessageFind
-    messages.map(message => dispatch(actionAddMSG(id, message._id, message.text, message.createdAt, message.owner, message.media)))
+    messages.map(message => dispatch(actionAddMSG(false, id, message._id, message.text, message.createdAt, message.owner, message.media, message.replyTo)))
   }
 }
 

+ 0 - 2
src/components/chatEditForm.js

@@ -17,8 +17,6 @@ export const ChatEditForm = ({ chat_id, chat, searchState, onUpload, onUserSearc
         setNewMembers(chat.members)
     }, [chat])
 
-    console.log(chat_id)
-
     let editChatHandler = () => {
         setEdit(!edit)
         setNewMembers(chat.members)

+ 87 - 15
src/components/chatWindow.js

@@ -1,22 +1,75 @@
-import { useState } from "react"
-import { Message } from "./message"
+import { useEffect, useRef, useState } from "react"
+import { Message, ShortMessage } from "./message"
 import { MyDropzone } from "./dropzone"
-import ScrollableFeed from 'react-scrollable-feed'
-import { Link } from "react-router-dom"
 
-const Chat = ({ chat_id, chat_title, user_id, messages, onUpload, onSend, onMSGEdit }) => {
+const updateScroll = (el) => {
+    el.scrollTop = el.scrollHeight
+}
+
+const Chat = ({ chat_id, chat_title, user_id, messages, onUpload, onSend, onMSGEdit, onScrollTopComplete }) => {
     let [msg, setMSG] = useState("")
     let [isUpload, setIsUpload] = useState(false)
     let [files, setFiles] = useState([])
+    let [isForward, setIsForward] = useState([false, ""])
+    let [isReply, setIsReply] = useState([false, ""])
+    let [skip, setSkip] = useState(20)
+    let [isScroll, setIsScroll] = useState(true)
+
+    let scrollRef = useRef(null)
+    let input = useRef(null)
+
+    useEffect(() => {
+        if (isScroll) {
+            updateScroll(scrollRef.current)
+        }
+    }, [messages])
+
+    useEffect(() => {
+        input.current && input.current.focus()
+    }, [isReply, isForward])
 
     let filesHandler = (file) => {
         setFiles(file)
     }
 
-    let handler = (e) => {
+    let buttonScrollHandler = () => {
+        setIsScroll(true)
+        updateScroll(scrollRef.current)
+    }
+
+    let scrollHandler = (el) => {
+        setIsScroll(Math.round(el.scrollTop + el.clientHeight) === el.scrollHeight)
+
+        if (el && el.scrollTop === 0) {
+            el.scrollTop = el.scrollHeight / 30
+            !isScroll && onScrollTopComplete(chat_id, skip)
+            setSkip(skip + 20)
+        }
+    }
+
+    let sendHandler = (e) => {
+        if (e.key === "Escape") {
+            setIsReply([false, ""])
+            setMSG("")
+        }
+
         if ((e.key === "Enter" && !e.shiftKey) || e.type === "click") {
-            onSend(chat_id, msg, files)
+            if (!isReply[0] && !isForward[0]) { //отправить обычное сообщение
+                onSend(chat_id, msg, files)
+            }
+
+            if (isReply[0]) {
+                onSend(chat_id, msg, files, isReply[1]) //ответить на сообщение (isReply[1] - id msg на какое отвечаем)
+                setIsReply([false, ""])
+            }
+
+            if (isForward[0]) {
+                onSend(chat_id, msg, files, false, isForward[1]) //переслать (isForward[1] - id чата куда переслать)
+                setIsForward([false, ""])
+            }
+
             setMSG("")
+            updateScroll(scrollRef.current)
             setFiles([])
             setIsUpload(false)
             e.preventDefault()
@@ -24,25 +77,44 @@ const Chat = ({ chat_id, chat_title, user_id, messages, onUpload, onSend, onMSGE
     }
 
     return (
-        <div className="chat-window" >
+        <div className="chat-window">
             <div className="chat-nav">
                 <span>{chat_title}</span>
             </div>
 
-            <ScrollableFeed className="message-list" forceScroll>
-                {messages.length > 0 && messages.map(message => <Message key={message._id} id={message._id} nick={message.owner.nick || message.owner.login} msg={message.text} date={message.createdAt} media={message.media} modified={message.modified} own={message.owner._id === user_id} onMSGEdit={onMSGEdit} />)}
-            </ScrollableFeed>
+            <ul ref={scrollRef} onScroll={() => scrollHandler(scrollRef.current)} className="message-list">
+                {messages.length > 0 &&
+                    messages.map(message => {
+                        return <Message key={message._id}
+                            id={message._id}
+                            nick={message.owner.nick || message.owner.login}
+                            msg={message.text}
+                            date={message.createdAt}
+                            media={message.media}
+                            own={message.owner._id === user_id}
+                            replyTo={message.replyTo}
+                            onMSGEdit={onMSGEdit}
+                            onMSGReply={setIsReply}
+                            onMSGForward={setIsForward} />
+                    })}
+            </ul>
+
+            {isReply[0] &&
+                messages.filter(message => message._id === isReply[1]).map(m =>
+                    <ShortMessage key={m._id} nick={m.owner.nick || m.owner.login}
+                        text={m.text}
+                        date={m.createdAt} />)}
+
+            {!isScroll && <button className="btn-scroll" onClick={() => buttonScrollHandler()}>Вниз</button>}
 
             <div className="message-input">
-                <textarea placeholder="Введите сообщение..." value={msg} onInput={(e) => setMSG(e.target.value)} onKeyDown={(e) => handler(e)} />
+                <textarea ref={input} placeholder="Введите сообщение..." value={msg} onFocus={() => updateScroll(scrollRef.current)} onInput={(e) => setMSG(e.target.value)} onKeyDown={(e) => sendHandler(e)} />
                 <button onClick={() => setIsUpload(!isUpload)}>Прикрепить файл</button>
                 {isUpload && <MyDropzone onUpload={onUpload} onSet={filesHandler} />}
-                <button onClick={(e) => handler(e)}>Отправить</button>
+                <button onClick={(e) => sendHandler(e)}>Отправить</button>
             </div>
         </div >
     )
 }
 
-
-
 export default Chat

+ 61 - 41
src/components/message.js

@@ -39,15 +39,27 @@ const ContextMenu = ({ element, message, owner, onEdit, onReplyTo, onForwardTo }
                 left: points.x
             }}
         >
-            {!owner && <button onClick={() => onReplyTo(message)}>Ответить</button>}
-            <button onClick={() => onForwardTo(message)}>Переслать</button>
+            <button onClick={() => onReplyTo([true, message])}>Ответить</button>
+            <button onClick={() => onForwardTo([true, ""])}>Переслать</button>
             {owner && <button onClick={() => onEdit(true)}>Редактировать</button>}
         </div>
     )
 }
 
+export const ShortMessage = ({ nick, text, date }) => {
+    return (
+        <>
+
+            <div className="msg-short">
+                <span className="msg-nick">Ответ на сообщение {nick}</span>
+                <span className="msg-text">{text}</span>
+                <ReactTimeAgo className="msg-date" date={+date} locale="ru" timeStyle="twitter" />
+            </div>
+        </>
+    )
+}
 
-export const Message = ({ id, nick, msg, date, media, modified, own = false, replies, onMSGEdit, onMSGReply, onMSGForward }) => {
+export const Message = ({ id, nick, msg, date, media, own = false, replyTo, onMSGEdit, onMSGReply, onMSGForward }) => {
     let [isEdit, setIsEdit] = useState(false)
     let [messageEdit, setMessageEdit] = useState("" || msg)
     let el = useRef(null)
@@ -61,44 +73,52 @@ export const Message = ({ id, nick, msg, date, media, modified, own = false, rep
         <li ref={el} className={own ? "msg-user" : "msg-someone"} >
             <ContextMenu message={id} element={el.current} owner={own} onEdit={setIsEdit} onForwardTo={onMSGForward} onReplyTo={onMSGReply} />
 
-            <span className="msg-nick">{nick}</span>
-
-            {isEdit ?
-                <>
-                    <input value={messageEdit} onChange={(e) => setMessageEdit(e.target.value)} />
-                    <button onClick={() => msgEditHandler()} >Сохранить изменения</button>
-                </> :
-                <span className="msg-text">{msg}</span>}
-
-            <ReactTimeAgo className="msg-date" date={+date} locale="ru" timeStyle="round" />
-            {modified && <span className="mutated-msg">Сообщение изменено</span>}
-            {replies && replies.map(repMSG => <Message nick={repMSG.login} msg={repMSG.text} date={repMSG.createdAt} />)}
-            {media && media.length > 0 &&
-                <ul className="msg-media">
-                    {media.map(file => {
-                        return (<li key={file.url}>
-                            {file.type.includes("audio") &&
-                                <audio className="msg-media-audio" preload="metadata" controls>
-                                    <source src={"/" + file.url} type={file.type} />
-                                    <a href={"/" + file.url} />
-                                </audio>
-                            }
-
-                            {
-                                file.type.includes("image") &&
-                                <img className="msg-media-img" src={"/" + file.url} alt="image" />
-                            }
-
-                            {
-                                file.type.includes("video") &&
-                                <video className="msg-media-video" preload="metadata" controls>
-                                    <source src={"/" + file.url} type={file.type} />
-                                    <a href={"/" + file.url} />
-                                </video>
-                            }
-                        </li>)
-                    })}
-                </ul>}
+            {replyTo && <div className="msg-reply">В ответ на сообщение: {replyTo.text}</div>}
+
+            <div className="msg">
+                <span className="msg-nick">{nick}</span>
+
+                {isEdit ?
+                    <>
+                        <input value={messageEdit} onChange={(e) => setMessageEdit(e.target.value)} />
+                        <button onClick={() => msgEditHandler()} >Сохранить изменения</button>
+                    </> :
+                    <span className="msg-text">{msg}</span>}
+
+                <ReactTimeAgo className="msg-date" date={+date} locale="ru" timeStyle="round" />
+                {media && media.length > 0 &&
+                    <ul className="msg-media">
+                        {media.map(file => {
+                            return (<li key={file.url}>
+                                {file.type && file.type.includes("audio") &&
+                                    <audio className="msg-media-audio" preload="metadata" controls>
+                                        <source src={"/" + file.url} type={file.type} />
+                                        <a href={"/" + file.url} />
+                                    </audio>
+                                }
+
+                                {
+                                    file.type && file.type.includes("image") &&
+                                    <img className="msg-media-img" src={"/" + file.url} alt="image" />
+                                }
+
+                                {
+                                    file.type && file.type.includes("video") &&
+                                    <video className="msg-media-video" preload="metadata" controls>
+                                        <source src={"/" + file.url} type={file.type} />
+                                        <a href={"/" + file.url} />
+                                    </video>
+                                }
+
+                                {
+                                    file.type === null && <a href={"/" + file.url} />
+                                }
+
+                            </li>)
+                        })}
+                    </ul>}
+            </div>
+
         </li>
     )
 }

+ 4 - 3
src/pages/main.js

@@ -41,7 +41,7 @@ const Main = ({ match: { params: { _id } }, userID, chats, getChat, getMessages,
 
         socket.on('msg', msg => {
             console.log("это пришло по сокету (сообщение)", msg)
-            addMSG(msg.chat._id, msg._id, msg.text, msg.createdAt, msg.owner, msg.media)
+            addMSG(true, msg.chat._id, msg._id, msg.text, msg.createdAt, msg.owner, msg.media, msg.replyTo)
         })
 
         return () => {
@@ -50,7 +50,7 @@ const Main = ({ match: { params: { _id } }, userID, chats, getChat, getMessages,
     }, [])
 
     useEffect(() => {
-        chat_id && getMessages(chat_id)
+        chat_id && getMessages(chat_id, 0)
     }, [_id])
 
     return (
@@ -66,7 +66,8 @@ const Main = ({ match: { params: { _id } }, userID, chats, getChat, getMessages,
                     messages={chats[chat_id].messages || []}
                     onUpload={addFile}
                     onSend={sendMSG}
-                    onMSGEdit={editMSG} />}
+                    onMSGEdit={editMSG}
+                    onScrollTopComplete={getMessages} />}
 
             {chats[chat_id] && <ChatEditForm chat_id={chat_id}
                 chat={chats[chat_id]}

+ 2 - 4
src/reducers/chat.js

@@ -1,4 +1,4 @@
-function chatReducer(state = {}, { type, chat_id, title, createdAt, lastModified, avatar, members, messages, msg_id, msg_text, msg_createdAt, msg_owner, msg_media, msg_replies }) {
+function chatReducer(state = {}, { type, chat_id, title, createdAt, lastModified, avatar, members, messages, msg_id, msg_text, msg_createdAt, msg_owner, msg_media, msg_replyTo, socket = false }) {
     if (type === 'CHAT') {
         if (Object.keys(state).filter(id => id === chat_id).length === 0) { //новый чат
             return {
@@ -21,7 +21,6 @@ function chatReducer(state = {}, { type, chat_id, title, createdAt, lastModified
 
                 [chat_id]: {
                     ...state[chat_id],
-
                     title: title,
                     avatar: avatar,
                     members: members
@@ -40,7 +39,7 @@ function chatReducer(state = {}, { type, chat_id, title, createdAt, lastModified
                     createdAt: state[chat_id].createdAt,
                     lastModified: msg_createdAt,
                     avatar: state[chat_id].avatar,
-                    messages: [...state[chat_id].messages, { _id: msg_id, text: msg_text, createdAt: msg_createdAt, owner: msg_owner, media: msg_media, replies: msg_replies }],
+                    messages: socket ? [...state[chat_id].messages, { _id: msg_id, text: msg_text, createdAt: msg_createdAt, owner: msg_owner, media: msg_media, replyTo: msg_replyTo }] : [{ _id: msg_id, text: msg_text, createdAt: msg_createdAt, owner: msg_owner, media: msg_media, replyTo: msg_replyTo }, ...state[chat_id].messages],
                     members: state[chat_id].members
                 }
             }
@@ -57,7 +56,6 @@ function chatReducer(state = {}, { type, chat_id, title, createdAt, lastModified
 
             let newMessages = [...state[chat_id].messages]
             newMessages[order].text = msg_text
-            newMessages[order].modified = true
 
             return {
                 ...state,