7 コミット 6e4f2e3157 ... 4c12244ef7

作者 SHA1 メッセージ 日付
  serg1557733 4c12244ef7 fix user avatarUrl and div keys attr 1 年間 前
  serg1557733 35f9b8d325 fix messages edit and some codefix 1 年間 前
  serg1557733 ffca5e9136 start working on edit and delete messages 1 年間 前
  serg1557733 33472077a6 add youtube video regexp and visualisation 1 年間 前
  serg1557733 9eb65091d1 add userTyping socket event and visual element to front 1 年間 前
  serg1557733 49007ab1d5 add userTyping socket event and visual element to front 1 年間 前
  serg1557733 f985eda2ae fix online users om frontend and add visual dot to avatars 1 年間 前

+ 12 - 7
backend/app.js

@@ -26,7 +26,6 @@ const io = require("socket.io")(server, {
         origin: "http://localhost:3000" //client endpoint and port
     }
 });
-const randomColor = require('randomcolor'); 
 
 const PORT = process.env.PORT || 5000;
 const TOKEN_KEY = process.env.TOKEN_KEY || 'rGH4r@3DKOg06hgj'; 
@@ -144,6 +143,7 @@ io.use( async (socket, next) => {
         usersOnline.push(sock.user);
     }) 
 
+
    
     try {
         const user = jwt.verify(token, TOKEN_KEY);
@@ -155,7 +155,6 @@ io.use( async (socket, next) => {
             return;
         }
         socket.user = user;
-        socket.user.color = randomColor();
         const exist = sockets.find((current) => current.user.userName == socket.user.userName)
 
         if(exist) {  //&& !user.isAdmin  - add for two or more admins 
@@ -173,10 +172,12 @@ io.on("connection", async (socket) => {
     const userName = socket.user.userName;
     const sockets = await io.fetchSockets();
     const dbUser = await getOneUser(userName);
-    
-    io.emit('usersOnline', sockets.map((sock) => sock.user)); // send array online users  
+
+
+    io.emit('usersOnline', sockets.map(sock => sock.user)); // send array online users  
+
     socket.emit('connected', dbUser); //socket.user
-   
+  
     if(socket.user.isAdmin){
          getAllDbUsers(socket); 
     }//sent all users from db to admin
@@ -224,11 +225,10 @@ io.on("connection", async (socket) => {
     try {
         socket.on("disconnect", async () => {
             const sockets = await io.fetchSockets();
-            io.emit('usersOnline', sockets.map((sock) => sock.user));
+            io.emit('usersOnline', sockets.map(sock => sock.user));
             console.log(`user :${socket.user.userName} , disconnected to socket`); 
         });
             console.log(`user :${socket.user.userName} , connected to socket`); 
-        
         socket.on("muteUser",async (data) => {
             if(!socket.user.isAdmin){
                 return;
@@ -265,6 +265,11 @@ io.on("connection", async (socket) => {
                 }
             // }
            });
+
+           socket.on('userWriting', async () => {
+                let isTyping = true;
+                io.emit('writing', {userName, isTyping})
+           })
     } catch (e) {
         console.log(e);
     }

+ 21 - 6
frontend/src/components/chatPage/ChatPage.jsx

@@ -10,6 +10,7 @@ import {getSocket} from'../../reducers/socketReducer';
 import { sendMessage, storeMessage } from '../../reducers/messageReducer';
 import { editMessage } from '../../reducers/messageReducer';
 import { SwitchButton } from './SwitchButton';
+import { MessageEditorMenu } from './MessageEditorMenu.jsx';
 import './chatPage.scss';
 
 
@@ -26,28 +27,42 @@ export const ChatPage = () => {
     let showUserInfoBox = useSelector(state => state.messageReducer.showUserInfoBox)// || localStorage.getItem('showBox');
 
     const [message, setMessage] = useState({message: ''});
+    const [isUserTyping, setUserTyping] = useState([]);
     
     const isTabletorMobile = (window.screen.width < 730);
 
     useEffect(() => {
+        if(socket) {
+            socket.on('writing', (data) => { 
+                    setUserTyping(data) 
+                    setTimeout(() => setUserTyping([]), 500 )
+                })  
+        }
+   }, [socket])
+
+
+    useEffect(() => {
+   
         if(token){
-            SOCKET_EVENTS.map(event => dispatch(getSocket(event)))       
+            SOCKET_EVENTS.map(event => dispatch(getSocket(event)))   
         }
-    }, [token, editOldMessage, showUserInfoBox, ])
+    }, [token, editOldMessage, showUserInfoBox])
  
     return (
         <div className='rootContainer'>
 
             <Box className = 'rootBox'>
 
-
                 { isTabletorMobile ? <SwitchButton/> : null}
                 
                 <Box className ={isTabletorMobile ? 'rootMessageFormMobile':'rootMessageForm'} >
                     
                     <MessageForm/>
 
+                    {isUserTyping.isTyping && (isUserTyping.userName !== user.userName)? <span> User {isUserTyping.userName} typing..</span> : ""}
+
                     <Box 
+
                         component="form" 
                         onSubmit = {e  => {
                                         e.preventDefault()
@@ -64,7 +79,7 @@ export const ChatPage = () => {
                             margin: '20px 5px'}
                            
                         }>
-            
+
                         <TextareaAutosize
                             id="outlined-basic" 
                             label="Type a message..." 
@@ -85,6 +100,7 @@ export const ChatPage = () => {
                             }}
                             onChange={e => { 
                                 dispatch(storeMessage({message: e.target.value}))
+                                socket.emit('userWriting');
                                 setMessage({message: e.target.value})}
                             } 
                         
@@ -115,8 +131,8 @@ export const ChatPage = () => {
                         variant="outlined"
                         onClick={()=> {
                                 localStorage.removeItem('token');
-                                socket.disconnect(); 
                                 dispatch(removeToken());
+                                socket.disconnect(); 
                                 }}>
                         Logout
                     </Button>
@@ -124,7 +140,6 @@ export const ChatPage = () => {
                     <UserInfo/>
 
                 </Box>
-
             </Box>
         </div>
     )

+ 26 - 0
frontend/src/components/chatPage/MessageEditorMenu.jsx.jsx

@@ -0,0 +1,26 @@
+import { useDispatch } from 'react-redux';
+import { editMessage } from '../../reducers/messageReducer';
+
+
+export const MessageEditorMenu = () => {
+
+    const dispatch = useDispatch();
+
+
+    return (
+        <div>
+            <button> Edit</button>
+            <button> Delete </button>
+            <button
+                onClick={() => {
+                    dispatch(editMessage({editMessage: '', showButtons: false, messageId: '' }))  
+                }}
+            > cancel</button>
+        </div>
+    )
+}
+
+
+
+
+

+ 63 - 42
frontend/src/components/chatPage/messageForm/MessegaForm.jsx

@@ -1,78 +1,99 @@
-import {Avatar, Box} from '@mui/material';
+import { Avatar, Box, StyledBadge } from '@mui/material';
 import { dateFormat } from '../utils/dateFormat';
 import { useSelector } from 'react-redux';   
-import { Fragment, useRef, useEffect, useMemo} from 'react';
+import { useRef, useEffect, useState} from 'react';
 import { scrollToBottom } from '../utils/scrollToBottom';
 import { useDispatch } from 'react-redux';
 import { editMessage } from '../../../reducers/messageReducer';
-import { TimeAgoMessage } from '../TimeAgoMessage';
+import { StyledAvatar } from './StyledAvatar';
+import { MessageEditorMenu } from '../MessageEditorMenu.jsx';
 
 export const MessageForm = () => {
 
-    const randomColor = require('randomcolor');  
     const dispatch = useDispatch();
+
     const SERVER_URL = process.env.REACT_APP_SERVER_URL|| 'http://localhost:5000/';
 
     const startMessages = useSelector(state => state.getUserSocketReducer.startMessages)
     const user = useSelector(state => state.getUserSocketReducer.socketUserData)
     const usersOnline = useSelector(state => state.getUserSocketReducer.usersOnline)
-    const userColor = useMemo(() => randomColor(),[]);
+    const userNamesOnlineSet =  new Set(usersOnline.map( i => i.userName))
+    const storeMessageId = useSelector(state => state.messageReducer.messageId)
+
 
-    const endMessages = useRef(null);
+    const endMessages =useRef(null);
+
+    const regYoutube = /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?‌​[\w\?‌​=]*)?/; //for youtube video
 
-    
 
     useEffect(() => {
         scrollToBottom(endMessages)
       }, [startMessages, usersOnline]);
                   
     return (             
-            <Box className='messageBox'>                     
+            <Box className='messageBox'>  
                 {
                 startMessages.map((item, i) =>
-                    <div key={i} className={ 
-                        (item.userName === user.userName)? 'message myMessage' :'message'}>    
-                        {console.log(item)}
-                        <Avatar 
-                      
-                            src= {SERVER_URL + item?.user?.avatar}
-                            sx={
-                                (item.userName == user.userName)
-                                ? 
-                                {
-                                    alignSelf: 'flex-end',
-                                    backgroundColor: userColor
+                    <div key={i + 1} className={ 
+                        (item.userName === user.userName)? 'message myMessage' :'message'}
+                        onClick = {(e) => {
+                            if(e.target.className.includes('myMessage') && (item.userName === user.userName) && (item.text === e.target.textContent)){
+                                e.currentTarget.className += ' editMessage'  
+                                dispatch(editMessage({editMessage: e.target.textContent, messageId: item._id}))   
                                 }
-                                :
-                                {
-                                    backgroundColor:  (usersOnline.map(current => {
-                                        if(item.userName === current.userName ) {
-                                            return current.color
-                                        }
-                                    } )),
+                        }}
+                        > 
+
+                        {storeMessageId === item._id ? <MessageEditorMenu/> : ""} 
+
+                        <StyledAvatar
+
+                            anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}  
+                            variant = {userNamesOnlineSet.has(item.userName)? 'dot' : ''}
+                                   
+                            >
+                            <Avatar 
+                                key={i} 
+                                src= {SERVER_URL + item?.user?.avatar}
+                                sx={
+                                    (item.userName == user.userName)
+                                    ? 
+                                    {
+                                        alignSelf: 'flex-end',
+                                    }
+                                    :
+                                    {}
                                 }
-                            }> 
-                            {item?.userName.slice(0, 1)}
-                        </Avatar>   
+                                
+                                > 
+                                {item?.userName.slice(0, 1)}
+                            </Avatar>   
 
+                        
+                        </StyledAvatar>
                         <div 
-                            key={item._id}
-                            onClick = {(e) => {
-                                if(e.target.className.includes('myMessage')){
-                                    e.currentTarget.className += ' editMessage' 
-                                    startMessages.map( item => {
-                                        if((item.userName === user.userName) && (item.text === e.target.textContent)){
-                                            console.log('edit message',e.target.textContent )
-                                            dispatch(editMessage({editMessage: e.target.textContent}))                                        
-                                        }
-                                        })}
-                            }}
+                            key={i/10}
+                        
                             className={ 
                                 (item.userName === user.userName)? 'message myMessage' :'message'}>
-
+                           
+                           { 
+                           item.text.match(regYoutube) ?
+                           <iframe 
+                                width="280" 
+                                height="160" 
+                                src={`https://www.youtube.com/embed/`+ (item.text.match(regYoutube)[1])}
+                                title="YouTube video player" 
+                                allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" 
+                                allowFullScreen> 
+                                
+                            </iframe>
+                            :
                             <p>{item.text}</p>  
+                           }
 
                         </div>
+
                         <div className={ 
                                 (item.userName === user.userName)? 'myDate' :'date'}>
                                 {dateFormat(item).time}

+ 31 - 0
frontend/src/components/chatPage/messageForm/StyledAvatar.jsx

@@ -0,0 +1,31 @@
+import { styled } from '@mui/material/styles';
+import Badge from '@mui/material/Badge';
+
+export const StyledAvatar = styled(Badge)(({ theme }) => ({
+    '& .MuiBadge-badge': {
+      backgroundColor: '#44b700',
+      color: '#44b700',
+      boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
+      '&::after': {
+        position: 'absolute',
+        top: 0,
+        left: 0,
+        width: '100%',
+        height: '100%',
+        borderRadius: '50%',
+        animation: 'ripple 1.2s infinite ease-in-out',
+        border: '1px solid currentColor',
+        content: '""',
+      },
+    },
+    '@keyframes ripple': {
+      '0%': {
+        transform: 'scale(.8)',
+        opacity: 1,
+      },
+      '100%': {
+        transform: 'scale(2.4)',
+        opacity: 0,
+      },
+    },
+  }));

+ 20 - 19
frontend/src/components/chatPage/userInfo/UserInfo.jsx

@@ -1,13 +1,13 @@
 import {Button,Avatar} from '@mui/material';
 import { useSelector } from 'react-redux';
 import { banUser } from '../service/banUser';
-import Input from '@mui/material/Input';
 import { muteUser } from '../service/muteUser';
 import './userInfo.scss';
 import { useDispatch } from 'react-redux';
 import { getUserAvatar } from '../../../reducers/userDataReducer';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
 import { store } from '../../../store';
+import { getSocket } from '../../../reducers/socketReducer';
 
 
 export const UserInfo = () => {
@@ -38,27 +38,32 @@ export const UserInfo = () => {
 
     const allUsers = useSelector(state => state.getUserSocketReducer.allUsers)
     const user = useSelector(state => state.getUserSocketReducer.socketUserData)
-    const usersOnline = [...new Set(useSelector(state => state.getUserSocketReducer.usersOnline))];//Set?
+    const usersOnline = useSelector(state => state.getUserSocketReducer.usersOnline);
     const socket = useSelector(state => state.getUserSocketReducer.socket)
     const isTabletorMobile = (window.screen.width < 730);
     const storeUserAvatar = useSelector(state => state.userDataReducer.avatar)
 
-    let userAvatarUrl = SERVER_URL + (storeUserAvatar || user.avatar);
+
+
+    let userAvatarUrl = storeUserAvatar || user.avatar;
+
+    
+    const userNamesOnlineSet =  new Set(usersOnline.map( i => i.userName))
 
     const inputHandler = (e) => {
         const file = e.target.files[0]
         dispatch(getUserAvatar(file))
         setDisplayType('none')
     }
-    //add delete avatar function later 
-
 
     return (
             <>  
+                <h4> Hello, {user.userName} </h4>
                 <Avatar
                     sx={isTabletorMobile ? MOBILE_AVATAR_STYLE : PC_AVATAR_STYLE} //add deleting function after update avatar
                     onClick={() => loadAvatarHandler()}
-                    src={userAvatarUrl} >
+                    src={userAvatarUrl ? SERVER_URL + userAvatarUrl : ""}
+                    >
                 </Avatar>  
                 <input
                         type="file"
@@ -70,17 +75,12 @@ export const UserInfo = () => {
                         onChange = {e => inputHandler(e)}
                        />
                     {user.isAdmin ? 
-                        allUsers.map((item) =>
+                        allUsers.map((item, key) =>
                             <div 
                                 key={item._id}
                                 className='online'>
-                                <div style={{color: (usersOnline.map(current => {
-                                                if(item.userName === current.userName) {
-                                                    return current.color
-                                                }}))
-                                            }}>
-
-                                            {item.userName}
+                                <div>
+                                    {item.userName}
                                 </div>
 
                                 <div>
@@ -125,10 +125,11 @@ export const UserInfo = () => {
                                     </>}
                             
                                 </div>
-                                    {usersOnline.map((user, i) => {
-                                        if(item.userName === user.userName){
-                                            return <span key={i} style={{color: 'green'}}>online</span>
-                                        }})}
+                                    {
+                                    userNamesOnlineSet.has(item.userName)? 
+                                    <span key={key} style={{color: 'green'}}>online</span>
+                                    : ''
+                                    }
                             </div>) 
                     :
                     usersOnline.map((item, i) =>

+ 9 - 2
frontend/src/reducers/messageReducer.js

@@ -3,7 +3,8 @@ import { createSlice} from '@reduxjs/toolkit';
 const initialState = {
     startMessages: [],
     message:'',
-    editMessage: ''
+    editMessage: '',
+    messageId: '', 
 }
 
 export const sendMessageToSocket = (state, data) => {
@@ -20,12 +21,18 @@ export const editMessageToSocket = (state, data) => {
        } 
 };
 
+
+
+
 const messageReducerSlice = createSlice({
     name: 'messageReducer',
     initialState,
     reducers: {
         storeMessage: (state, action) => {state.message = action.payload.message},
-        editMessage: (state, action) => {state.editMessage = action.payload.editMessage},
+        editMessage: (state, action) => {
+            state.editMessage = action.payload.editMessage;
+            state.messageId = action.payload.messageId;
+            },
         sendMessage: (state, action) => sendMessageToSocket(state, action.payload),
         clearMessage: (state) => {state.message = ''}
         

+ 26 - 10
frontend/src/reducers/socketReducer.js

@@ -10,7 +10,9 @@ const initialState = {
     socketUserData: {},
     usersOnline: [],
     startMessages: [],
-    allUsers: []
+    allUsers: [],
+    writing: false,
+    usersWriting: []
 }
 
 const SOCKET_URL =  process.env.REACT_APP_SERVER_URL || 'http://localhost:5000'; 
@@ -19,22 +21,18 @@ const connectToSocket = (event) => {
         try {
             const token = localStorage.getItem('token');
             if(token){
-                const socket = io.connect( 
+                const socket = io.connect(    //need to add other function for connecting
                     SOCKET_URL, 
                     {auth: {token}})
                     socket.on('connected', data => {
                                 store.dispatch(getUser(data));
+                               // socketEventsDispatch(socket)
                             })
                             .on(event, (data) => {
                                    switch (event){
                                     case 'allmessages':
                                         store.dispatch(getAllMessages(data));
                                         break;
-                            
-                                    case 'usersOnline':
-                                        store.dispatch(getUsersOnline(data));
-                                        break;
-
                                     case 'allDbUsers':
                                         store.dispatch(getAllUsers(data));
                                         break;
@@ -49,6 +47,9 @@ const connectToSocket = (event) => {
                                 store.dispatch(removeToken()); 
                                 localStorage.removeItem('token');
                                 })
+                            .on('usersOnline', (data) => {
+                                    store.dispatch(getUsersOnline(data))
+                                })
                             .on('disconnect', (data) => {
                                 if( data === 'io server disconnect') {
                                     socket.disconnect();
@@ -56,6 +57,7 @@ const connectToSocket = (event) => {
                                 }
                             })
                             .on('error', e => {console.log('On connected', e)}); 
+                            
                 return socket;       
             }   
         } catch (error) {
@@ -63,7 +65,15 @@ const connectToSocket = (event) => {
         } 
     };
 
-    
+// const socketEventsDispatch = (socket) => {
+//     socket
+//         .on('writing', (data) => {
+//                 console.log(data)
+//                 store.dispatch(writing(data));   
+//      })
+// }
+
+
 export const getUserSocketSlice = createSlice({
     name: 'userSocket',
     initialState,
@@ -79,7 +89,12 @@ export const getUserSocketSlice = createSlice({
         getAllMessages: (state, action) => {state.startMessages = action.payload},
         getUsersOnline: (state, action) => {state.usersOnline = action.payload},
         getAllUsers: (state, action) => {state.allUsers = action.payload},
-        addNewMessage: (state, action) => {state.startMessages.push(action.payload)}
+        addNewMessage: (state, action) => {state.startMessages.push(action.payload)}, 
+        // writing: (state, action) => {
+        //                             state.writing = true;
+        //                             state.usersWriting.push(action.payload)                  
+        //     }
+        
         }
     }
 );
@@ -96,5 +111,6 @@ export const {
     getAllMessages,
     getUsersOnline,
     addNewMessage,
-    getAllUsers
+    getAllUsers,
+    writing
 } = actions;