Bläddra i källkod

add context-menu, change chat-reducer, fix msg

Vitalii Polishchuk 2 år sedan
förälder
incheckning
9600c9576f
7 ändrade filer med 204 tillägg och 143 borttagningar
  1. 36 2
      src/App.css
  2. 16 32
      src/actions/index.js
  3. 2 9
      src/components/chatEditForm.js
  4. 3 4
      src/components/chatWindow.js
  5. 87 24
      src/components/message.js
  6. 16 29
      src/pages/main.js
  7. 44 43
      src/reducers/chat.js

+ 36 - 2
src/App.css

@@ -85,8 +85,8 @@ main {
   list-style: none;
   border-radius: 10px;
   padding: 5px;
-  max-width: 200px;
-  min-width: 200px;
+  max-width: 300px;
+  min-width: 300px;
   background: honeydew;
 }
 
@@ -151,3 +151,37 @@ textarea {
 .edit-btn-container {
   display: flex;
 }
+
+.context-menu {
+  display: flex;
+  flex-direction: column;
+  background-color: #fff;
+  width: 100px;
+  height: auto;
+  margin: 0;
+  position: absolute;
+  opacity: 1;
+  transition: opacity 0.5s linear;
+}
+
+.msg-media {
+  display: flex;
+  flex-direction: column;
+  margin-top: 10px;
+  list-style: none;
+  align-items: center;
+  padding: 0;
+}
+
+.msg-media-img {
+  max-width: 50%;
+}
+
+.msg-media-video,
+.msg-media-audio {
+  max-width: 100%;
+}
+
+.msg-media span {
+  font-size: 10px;
+}

+ 16 - 32
src/actions/index.js

@@ -74,7 +74,9 @@ let userChats = async (id) => {
 let chatMSG = async (id) => {
   let query = `query getMSG($chatID: String){
         MessageFind(query: $chatID){
-         _id text createdAt owner{
+         _id text createdAt media{
+           url _id type
+         } owner{
           login _id nick
           } replies {
             _id text createdAt owner{
@@ -177,6 +179,8 @@ let editChat = async (chat_id, title, members) => {
         ChatUpsert(chat: $chat){
           _id title members{
             _id login nick
+          } avatar {
+            _id url
           }
         }
       }`
@@ -223,7 +227,7 @@ let newMSG = async (chat_id, text, media) => {
           _id text createdAt owner{
             _id nick login
           } media{
-            _id url
+            _id url type
           }
         }
       }`
@@ -242,17 +246,11 @@ let newMSG = async (chat_id, text, media) => {
   return result
 }
 
-let editMSG = async (id, text, media) => {
+let editMSG = async (id, text) => {
   let input = {}
   input._id = id
   input.text = text
 
-  if (media && Array.isArray(media)) {
-    let mediaQuery = []
-    media.map(item => mediaQuery.push({ "_id": item }))
-    input.media = mediaQuery
-  }
-
   let query = `mutation editMSG($msg: MessageInput){
         MessageUpsert(message: $msg){
           _id text media{
@@ -379,38 +377,26 @@ const actionGetChats = (id) => actionPromise("chats", userChats(id))
 
 const actionGetMessages = (chatID) => actionPromise("messages", chatMSG(chatID))
 
-const actionChats = (chat_id, title, createdAt, lastModified, avatar, messages, members) => ({ type: "ADD_CHAT", chat_id, title, createdAt, lastModified, avatar, messages, members })
+const actionChats = (chat_id, title, createdAt, lastModified, avatar, messages, members) => ({ type: "CHAT", chat_id, title, createdAt, lastModified, avatar, messages, members })
 
-export const actionEditChat = (chat_id, title, avatar, members) => ({ type: "EDIT_CHAT", chat_id, title, avatar, members })
+export const actionEditChat = (chat_id, title, avatar, members) => ({ type: "CHAT", chat_id, title, avatar, members })
 
 const actionEditChatBack = (title, avatar, members) => actionPromise("edit_chat", editChat(title, avatar, members))
 
-export const actionEditMSG = (chat_id, message_id, text, media) => ({ type: "EDIT_MESSAGE", chat_id, messages: [{ _id: message_id, text, media }] })
+export const actionEditMSG = (chat_id, msg_id, msg_text) => ({ type: "MESSAGE", chat_id, msg_id, msg_text })
 
-const actionEditMSGback = (message_id, text, media) => actionPromise("edit_message", editMSG(message_id, text, media))
+export const actionEditMSGback = (message_id, text, media) => actionPromise("edit_message", editMSG(message_id, text, media))
 
 export const actionAddChatBack = (title, members) => actionPromise("chat", newChat(title, members))
 
-export const actionAddChat = (chat_id, title, createdAt, lastModified, owner, avatar, messages, members) => ({ type: "ADD_CHAT", chat_id, title, createdAt, lastModified, owner, avatar, messages, members })
+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: "ADD_MESSAGE", chat_id, msg_id, msg_text, msg_createdAt, msg_owner, msg_media })
+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 actionAddMSGBack = (chat_id, text, media) => actionPromise("new_message", newMSG(chat_id, text, media))
 
 export const actionSetAvatar = (avatar_id, chat_id) => actionPromise("set_avatar", avatarSet(avatar_id, chat_id))
 
-// export const actionFullGetChats = (id) => {
-//   return async (dispatch) => {
-//     let chats = await dispatch(actionGetChats(id))
-
-//     chats.data.UserFindOne.chats.map(async (chat) => {
-//       let result = await dispatch(actionGetMessages(chat._id))
-//       let messages = result.data.MessageFind
-//       dispatch(actionChats(chat._id, chat.title, chat.createdAt, chat.lastModified, chat.avatar, messages, chat.members))
-//     })
-//   }
-// }
-
 export const actionUserSearch = (searchInput) => actionPromise("userSearch", userSearch(searchInput))
 
 export const actionFullGetChats = (id) => {
@@ -418,8 +404,6 @@ export const actionFullGetChats = (id) => {
     let chats = await dispatch(actionGetChats(id))
 
     chats.data.UserFindOne.chats.map(async (chat) => {
-      // let result = await dispatch(actionGetMessages(chat._id))
-      // let messages = result.data.MessageFind
       dispatch(actionChats(chat._id, chat.title, chat.createdAt, chat.lastModified, chat.avatar, [], chat.members))
     })
   }
@@ -452,9 +436,10 @@ export const actionFullEditChat = (chat_id, title, avatar, members) => {
   return async (dispatch) => {
     let result = await dispatch(actionEditChatBack(chat_id, title, members))
     let avatarResult = await dispatch(actionSetAvatar(avatar, chat_id))
+    console.log(avatarResult)
 
-    if (result.data?.ChatUpsert && avatarResult) {
-      dispatch(actionEditChat(chat_id, title, avatar, members))
+    if (result.data.ChatUpsert && avatarResult.data.MediaUpsert) {
+      dispatch(actionEditChat(chat_id, title, avatarResult.data.MediaUpsert, members))
     }
   }
 }
@@ -463,7 +448,6 @@ export const actionFullAddMessage = (chat_id, text, media) => {
   return async (dispatch) => {
     let result = await dispatch(actionAddMSGBack(chat_id, text, media))
 
-    // dispatch(actionAddMSG(result.))
   }
 }
 

+ 2 - 9
src/components/chatEditForm.js

@@ -7,7 +7,7 @@ import { MyDropzone } from "./dropzone"
 export const ChatEditForm = ({ chat_id, chat, searchState, onUpload, onUserSearch, onChangeChat }) => {
     let [edit, setEdit] = useState(false)
     let [newTitle, setNewTitle] = useState(chat.title || "")
-    let [newAvatar, setNewAvatar] = useState(chat.avatar)
+    let [newAvatar, setNewAvatar] = useState([])
     let [newMembers, setNewMembers] = useState(chat.members)
 
     useEffect(() => {
@@ -45,15 +45,8 @@ export const ChatEditForm = ({ chat_id, chat, searchState, onUpload, onUserSearc
             {edit && <UserSearch searchState={searchState} onUserSearch={onUserSearch} onAddMember={userAddHandler} />}
             <div className="edit-btn-container">
                 <button onClick={() => editChatHandler()} >{edit ? "Отменить редактирование" : "Редактировать чат"}</button>
-                {edit && <button onClick={() => onChangeChat(chat_id, newTitle, newAvatar._id, newMembers)}>Применить изменения</button>}
+                {edit && <button onClick={() => onChangeChat(chat_id, newTitle, newAvatar && newAvatar[0]._id, newMembers)}>Применить изменения</button>}
             </div>
         </div>
     )
 }
-
-export const ConnectChatEditForm = connect(state => ({ searchState: state.promise?.userSearch?.payload?.data?.UserFind }),
-    {
-        onUpload: actionUploadFile,
-        onUserSearch: actionUserSearch,
-        onChangeChat: actionFullEditChat
-    })(ChatEditForm)

+ 3 - 4
src/components/chatWindow.js

@@ -4,11 +4,10 @@ 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 }) => {
+const Chat = ({ chat_id, chat_title, user_id, messages, onUpload, onSend, onMSGEdit }) => {
     let [msg, setMSG] = useState("")
     let [isUpload, setIsUpload] = useState(false)
     let [files, setFiles] = useState([])
-    console.log(files)
 
     let filesHandler = (file) => {
         setFiles(file)
@@ -25,13 +24,13 @@ const Chat = ({ chat_id, chat_title, user_id, messages, onUpload, onSend }) => {
     }
 
     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} modified={message.modified} own={message.owner._id === user_id} />)}
+                {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>
 
             <div className="message-input">

+ 87 - 24
src/components/message.js

@@ -1,41 +1,104 @@
 import ReactTimeAgo from 'react-time-ago'
 import TimeAgo from 'javascript-time-ago'
 import ru from 'javascript-time-ago/locale/ru.json'
+import { useCallback, useState, useEffect, useRef } from 'react'
 
 TimeAgo.addLocale(ru)
-let handler = (e) => {
 
-    console.log(e)
-    e.preventDefault()
+const ContextMenu = ({ element, message, owner, onEdit, onReplyTo, onForwardTo }) => {
+    const [points, setPoints] = useState({ x: 0, y: 0 })
+    const [show, setShow] = useState("none")
+
+    const handler = useCallback(e => {
+        e.preventDefault()
+        setPoints({ x: e.pageX, y: e.pageY })
+        setShow("flex")
+    }, [setShow, setPoints])
+
+    const clickHandler = useCallback(() => setShow("none"), [setShow]);
+
+    useEffect(() => {
+        if (element) {
+            element.addEventListener("contextmenu", handler);
+            element.addEventListener("click", clickHandler);
+        }
+        return () => {
+            if (element) {
+                element.removeEventListener("contextmenu", handler);
+                element.removeEventListener("click", clickHandler);
+            }
+        }
+    });
+
+    return (
+        <div
+            className="context-menu"
+            style={{
+                display: show,
+                top: points.y,
+                left: points.x
+            }}
+        >
+            {!owner && <button onClick={() => onReplyTo(message)}>Ответить</button>}
+            <button onClick={() => onForwardTo(message)}>Переслать</button>
+            {owner && <button onClick={() => onEdit(true)}>Редактировать</button>}
+        </div>
+    )
 }
 
-export const Message = ({ nick, msg, date, media, modified, own = false, replies }) => {
+
+export const Message = ({ id, nick, msg, date, media, modified, own = false, replies, onMSGEdit, onMSGReply, onMSGForward }) => {
+    let [isEdit, setIsEdit] = useState(false)
+    let [messageEdit, setMessageEdit] = useState("" || msg)
+    let el = useRef(null)
+
+    let msgEditHandler = () => {
+        setIsEdit(false)
+        onMSGEdit(id, messageEdit)
+    }
 
     return (
-        <li className={own ? "msg-user" : "msg-someone"} onContextMenu={(e) => handler(e)}>
-            {media && media.length && <div>
-                <span>Прикрепленные файлы</span>
-                {media.map(file => <a href={"/" + file.url}>{file}</a>)}
-            </div>}
+        <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>
-            <span className="msg-text">{msg}</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 && media.map(file => {
-                file.type.includes("audio") &&
-                    <audio controls>
-                        <source src={"/" + file.url} type={file.type} />
-                    </audio>
-
-                file.type.includes("image") &&
-                    <img src={"/" + file.url} alt={file.originalFileName} />
-
-                file.type.includes("video") &&
-                    <video>
-                        <source src={"/" + file.url} type={file.type} />
-                    </video>
-            })}
+            {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>}
         </li>
     )
 }

+ 16 - 29
src/pages/main.js

@@ -3,13 +3,14 @@ import ChatsList from "../components/chatsList";
 import { useEffect } from "react";
 import io from 'socket.io-client';
 import { connect } from "react-redux";
-import { actionAddChat, actionAddChatBack, actionAddMSG, actionAddMSGBack, actionEditChat, actionEditMSG, actionFullEditChat, actionFullGetChats, actionFullGetMessages, actionUploadFile, actionUserSearch } from "../actions";
+import { actionAddChat, actionAddChatBack, actionAddMSG, actionAddMSGBack, actionEditChat, actionEditMSG, actionEditMSGback, actionFullEditChat, actionFullGetChats, actionFullGetMessages, actionUploadFile, actionUserSearch } from "../actions";
 import ConnectChatForm from "../components/chatForm";
 import { ChatEditForm } from "../components/chatEditForm";
 
 //все в куче
-const Main = ({ match: { params: { _id } }, userID, chats, getChat, getMessages, addChat, editChat, addMSG, editMSG, sendMSG, search, userSearch, changeChat, addFile }) => {
+const Main = ({ match: { params: { _id } }, userID, chats, getChat, getMessages, addChat, editChat, addMSG, editMSG, editMSGState, sendMSG, search, userSearch, changeChat, addFile }) => {
     let chat_id = _id.split(".")[1]
+
     useEffect(() => {
         getChat(userID)
         console.log("монтируем")
@@ -33,29 +34,14 @@ const Main = ({ match: { params: { _id } }, userID, chats, getChat, getMessages,
 
         socket.on("disconnect", () => console.log("дисконект", socket.id));
 
-        socket.on('msg', msg => {
-            console.log("это пришло по сокету (сообщение)", msg)
-
-            if (chats && chats[msg.chat._id] && chats[msg.chat._id]["messages"].length) {
-                let check = chats[msg.chat._id]["messages"].filter(({ _id }) => _id === msg._id).length
-                console.log(check)
-                if (check) {
-                    editMSG(msg.chat._id, msg._id, msg.text, msg.media)
-                } else {
-                    addMSG(msg.chat._id, msg._id, msg.text, msg.createdAt, msg.owner, msg.media)
-                }
-            }
-        })
-
         socket.on('chat', chat => {
             console.log("это пришло по сокету (чат)", chat)
-            let check = Object.keys(chats).filter(id => id === chat._id).length
-            console.log(check)
-            if (check) {
-                editChat(chat._id, chat.title, chat.avatar, chat.members)
-            } else {
-                addChat(chat._id, chat.title, chat.createdAt, chat.lastModified, chat.owner, chat.avatar, chat.messages, chat.members)
-            }
+            addChat(chat._id, chat.title, chat.createdAt, chat.lastModified, chat.owner, chat.avatar, chat.messages, chat.members)
+        })
+
+        socket.on('msg', msg => {
+            console.log("это пришло по сокету (сообщение)", msg)
+            addMSG(msg.chat._id, msg._id, msg.text, msg.createdAt, msg.owner, msg.media)
         })
 
         return () => {
@@ -79,8 +65,8 @@ const Main = ({ match: { params: { _id } }, userID, chats, getChat, getMessages,
                     user_id={userID}
                     messages={chats[chat_id].messages || []}
                     onUpload={addFile}
-                    onSend={sendMSG} />}
-
+                    onSend={sendMSG}
+                    onMSGEdit={editMSG} />}
 
             {chats[chat_id] && <ChatEditForm chat_id={chat_id}
                 chat={chats[chat_id]}
@@ -93,16 +79,17 @@ const Main = ({ match: { params: { _id } }, userID, chats, getChat, getMessages,
 }
 
 export const ConnectMain = connect(state => ({
-    userID: state.auth.payload.sub.id, chats: state.chat,
-    search: state.promise?.userSearch?.payload?.data?.UserFind
+    userID: state.auth.payload.sub.id,
+    search: state.promise?.userSearch?.payload?.data?.UserFind,
+    chats: state.chat,
 }),
     {
         getChat: actionFullGetChats,
         getMessages: actionFullGetMessages,
         addChat: actionAddChat,
-        editChat: actionEditChat,
         addMSG: actionAddMSG,
-        editMSG: actionEditMSG,
+        editMSG: actionEditMSGback,
+        editMSGState: actionEditMSG,
         sendMSG: actionAddMSGBack,
         userSearch: actionUserSearch,
         newChat: actionAddChatBack,

+ 44 - 43
src/reducers/chat.js

@@ -1,36 +1,37 @@
 function chatReducer(state = {}, { type, chat_id, title, createdAt, lastModified, avatar, members, messages, msg_id, msg_text, msg_createdAt, msg_owner, msg_media, msg_replies }) {
-    if (type === 'ADD_CHAT') {
-        return {
-            ...state,
+    if (type === 'CHAT') {
+        if (Object.keys(state).filter(id => id === chat_id).length === 0) { //новый чат
+            return {
+                ...state,
 
-            [chat_id]: {
-                title,
-                createdAt,
-                lastModified,
-                avatar,
-                messages,
-                members
+                [chat_id]: {
+                    title,
+                    createdAt,
+                    lastModified,
+                    avatar,
+                    messages,
+                    members
+                }
             }
         }
-    }
 
-    if (type === "EDIT_CHAT") {
-        return {
-            ...state,
+        if (Object.keys(state).filter(id => id === chat_id).length === 1) { //измененный чат
+            return {
+                ...state,
+
+                [chat_id]: {
+                    ...state[chat_id],
 
-            [chat_id]: {
-                title: title || state[chat_id].title,
-                createdAt: state[chat_id].createdAt,
-                lastModified: state[chat_id].lastModified,
-                avatar: avatar || state[chat_id].avatar,
-                messages: state[chat_id].messages,
-                members: members || state[chat_id].members
+                    title: title,
+                    avatar: avatar,
+                    members: members
+                }
             }
         }
     }
 
-    if (type === "ADD_MESSAGE") {
-        if ((state[chat_id]?.messages?.filter(msg => msg._id === msg_id).length === 0)) {
+    if (type === "MESSAGE") {
+        if ((state[chat_id]?.messages?.filter(msg => msg._id === msg_id).length === 0)) { //новое сообщение
             return {
                 ...state,
 
@@ -44,35 +45,35 @@ function chatReducer(state = {}, { type, chat_id, title, createdAt, lastModified
                 }
             }
         }
-    }
 
-    if (type === "EDIT_MESSAGE") {
-        let order;
+        if ((state[chat_id]?.messages?.filter(msg => msg._id === msg_id).length === 1)) { //измененное сообщение
+            let order;
 
-        for (let key in state[chat_id].messages) {
-            if (state[chat_id].messages[key]._id === messages[0]._id) {
-                order = key
+            for (let key in state[chat_id].messages) {
+                if (state[chat_id].messages[key]._id === msg_id) {
+                    order = key
+                }
             }
-        }
 
-        return {
-            ...state,
+            let newMessages = [...state[chat_id].messages]
+            newMessages[order].text = msg_text
+            newMessages[order].modified = true
 
-            [chat_id]: {
-                title: state[chat_id].title,
-                createdAt: state[chat_id].createdAt,
-                lastModified,
-                avatar: state[chat_id].avatar,
-                messages: [
-                    ...state[chat_id].messages,
+            return {
+                ...state,
 
-                    state[chat_id].messages[order].text = msg_text,
-                    state[chat_id].messages[order].modified = true
-                ],
-                members: state[chat_id].members
+                [chat_id]: {
+                    title: state[chat_id].title,
+                    createdAt: state[chat_id].createdAt,
+                    lastModified: state[chat_id.createdAt],
+                    avatar: state[chat_id].avatar,
+                    messages: newMessages,
+                    members: state[chat_id].members
+                }
             }
         }
     }
+
     return state
 }