Sfoglia il codice sorgente

done notification and typing

unknown 3 anni fa
parent
commit
d63c984c6b

File diff suppressed because it is too large
+ 1 - 1
.eslintcache


+ 23 - 10
src/App.tsx

@@ -7,11 +7,12 @@ import DateFnsUtils from '@date-io/date-fns';
 
 import s from './App.module.css';
 import { getToken } from './redux/authorization/selector'
+import { getChat } from './redux/chat/selector'
 import { asyncLogout, asyncCurrentUser } from './redux/authorization/operations'
-import { setToken,onlineUser } from './api-data'
+import { setToken, onlineUser,typingChat } from './api-data'
 import PrivateRoute from './components/reusableComponents/Routes/PrivateRoute';
 import PublicRoute from './components/reusableComponents/Routes/PublicRoute';
-import {Load,CLoad} from './components/reusableComponents/Loader/Loader';
+import { Load,CLoad } from './components/reusableComponents/Loader/Loader';
 
 const HomePage = lazy(
   () =>
@@ -29,27 +30,39 @@ const AuthPage = lazy(
 
 function App() {
   const token = useSelector(getToken)
+  const { companionId } = useSelector(getChat)
   const dispatch = useDispatch()
 
   useEffect(() => {
     if(!localStorage.isChecked) localStorage.isChecked = 'true'
   }, [])
 
-  useEffect(() => {
-    if (token) {
+   useEffect(() => {
+    if (token && localStorage.isChecked === 'true') {
       setToken.set(token)
       dispatch(asyncCurrentUser())
     }
+   }, [dispatch, token])
+
+  useEffect(() => {
     const handleKeepIn = async () => {
       if (localStorage.isChecked === 'false') {
         dispatch(asyncLogout())
-      } else if(localStorage.isChecked === 'true') {
-        await onlineUser()
+      } else if (localStorage.isChecked === 'true') {
+          await onlineUser()
       }
     }
     window.addEventListener("beforeunload",handleKeepIn)
-    return () => window.removeEventListener("beforeunload",handleKeepIn)
-  }, [dispatch,token])
+    return () => window.removeEventListener("beforeunload", handleKeepIn)
+
+  }, [dispatch])
+
+  useEffect(() => {
+    const handleTypingChat = async () => await typingChat(companionId,false)
+    window.addEventListener("beforeunload",handleTypingChat)
+    return () => window.removeEventListener("beforeunload", handleTypingChat)
+
+  }, [companionId])  
 
   return (
   <div className={s.appWrapper}>
@@ -68,7 +81,7 @@ function App() {
         <CLoad/>
       </Suspense>
       <ToastContainer
-      position="bottom-right"
+      position="top-right"
       autoClose={5000}
       hideProgressBar={false}
       newestOnTop={false}
@@ -77,7 +90,7 @@ function App() {
       pauseOnFocusLoss
       draggable
       pauseOnHover
-      />      
+      />    
     </MuiPickersUtilsProvider>
   </div>
 )}

+ 32 - 0
src/api-data/index.ts

@@ -33,6 +33,13 @@ const setToken = {
   },
 };
 
+const forbidden = ({ message }: any) => {
+  if (message.slice(-3) === '403') {
+    localStorage.removeItem('persist:auth')
+    window.location.reload(true);
+  }
+}
+
 const authorizeUser = async (number:string,country:string):Promise<string | undefined> => {
   try {
     const { data : {data} } = await axios.post('/auth/register', { number,country });
@@ -57,6 +64,7 @@ const logoutUser = async <T>():Promise<T | undefined> => {
     const { data } = await axios.post('/auth/logout');
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -66,6 +74,7 @@ const onlineUser = async <T>():Promise<T | undefined> => {
     const { data } = await axios.post('/auth/online');
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -75,6 +84,7 @@ const updateCredentials = async <T>(name: string,lastName: string):Promise<T | u
     const { data : {data} } = await axios.patch('/users/current', { name, lastName });
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -84,6 +94,7 @@ const updateUserAvatar = async <T>(formData: object): Promise<T | undefined> =>
     const { data : {data} } = await axios.patch('/users/avatars', formData);
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   } 
 };
@@ -93,6 +104,7 @@ const currentUser = async <T>(): Promise<T | undefined> => {
     const { data : {data} } = await axios.get('/users/current');
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   } 
 };
@@ -102,6 +114,7 @@ const addContact = async <T>(number:string): Promise<T | undefined> => {
     const { data : {data} } = await axios.post('/contacts', { number });
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -111,6 +124,7 @@ const getContacts = async <T>(): Promise<T | undefined> => {
     const { data : {data} } = await axios.get('/contacts');
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -120,6 +134,7 @@ const startChat = async <T>(id:string): Promise<T | undefined> => {
     const { data : {data} } = await axios.post('/chats',{id});
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -129,6 +144,7 @@ const getChat = async <T>(id:string): Promise<T | undefined> => {
     const { data: { data } } = await axios.get(`/chats/${id}`);
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -138,6 +154,7 @@ const muteChat = async <T>(id:string): Promise<T | undefined> => {
     const { data: { data } } = await axios.post('/chats/mute', {id});
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -147,6 +164,17 @@ const seenChat = async <T>(id:string): Promise<T | undefined> => {
     const { data: { data } } = await axios.post('/chats/seen', { id });
     return data
   } catch (e) {
+    forbidden(e)
+    return undefined
+  }
+};
+
+const typingChat = async <T>(id:string,typing:boolean): Promise<T | undefined> => {
+  try {
+    const { data: { data } } = await axios.post('/chats/typing', { id,typing});
+    return data
+  } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -156,6 +184,7 @@ const getChats = async <T>(): Promise<T | undefined> => {
     const { data: { data } } = await axios.get('/chats');
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -165,6 +194,7 @@ const sentMessageById = async <T>(id:string,message:any): Promise<T | undefined>
     const { data: { data } } = await axios.post('/messages', { id, message });
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -174,6 +204,7 @@ const getMessagesById = async <T>(id:string): Promise<T | undefined> => {
     const { data : {data} } = await axios.get(`/messages/${id}`);
     return data
   } catch (e) {
+    forbidden(e)
     return undefined
   }
 };
@@ -195,6 +226,7 @@ export {
   getChat,
   muteChat,
   seenChat,
+  typingChat,
   getChats,
   sentMessageById,
   getMessagesById

+ 28 - 8
src/components/HomePage/LeftBar/ChatsList/index.tsx

@@ -7,13 +7,13 @@ import { styled } from '@mui/material/styles';
 import Badge from '@mui/material/Badge';
 import VolumeOffIcon from '@mui/icons-material/VolumeOff';
 import { makeStyles, Typography } from '@material-ui/core'
-import { useState,useEffect } from 'react';
+import { useState,useEffect,useRef } from 'react';
 import shortid from 'shortid';
 import { useSelector, useDispatch } from 'react-redux';
 
 import AlertInfo from '../../../reusableComponents/AlertInfo'
 import DoneAllIcon from '@mui/icons-material/DoneAll';
-import { firstLetter, slicedWord, timeStamp } from '../../../../helpers'
+import { firstLetter, slicedWord, timeStamp,notification,playNotificationWithoutPermission } from '../../../../helpers'
 import { getState } from '../../../../redux/chats/selector'
 import { asyncGetChats } from '../../../../redux/chats/operations'
 import { asyncStartChatById } from '../../../../redux/chat/operations'
@@ -160,6 +160,7 @@ const useStyles = makeStyles({
 const ChatsList = () => {
   const classes = useStyles()
   const dispatch = useDispatch()
+  const ref = useRef<any>(null)
   const [selectedIndex, setSelectedIndex] = useState<number>(1);
   const { total, chats , lastMessages,lastOnline } = useSelector(getState)
  
@@ -170,7 +171,7 @@ const ChatsList = () => {
     setSelectedIndex(i);
   }
 
-  const handleNewMsgS = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>, i: number,companionId: string) => {
+  const handleNewMsgs = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>, i: number,companionId: string) => {
     e.stopPropagation()
     dispatch(asyncStartChatById(companionId))
     dispatch(actionScroll(true))
@@ -187,11 +188,29 @@ const ChatsList = () => {
     dispatch(asyncGetChats())
   }, [dispatch])
 
+  useEffect(() => {
+    const handleNotification= (companionId: string) => {
+      dispatch(asyncStartChatById(companionId))
+      dispatch(actionScroll(true))
+   }
+    if (ref.current) {
+      ref.current.forEach(({total,seen}: any,i:number) => {
+        const oldDifferent = total - seen
+        const chat = chats[i]
+        const newDifferent = chat.total - chat.seen
+        if (newDifferent > oldDifferent && !chat.mute) {
+          playNotificationWithoutPermission('http://localhost:3000/recive.mp3')
+          notification(chat.name,() => handleNotification(chat.companionId))
+        }
+      })
+    }
+      ref.current = chats
+  }, [chats,dispatch])  
 
   return total !== '0' ? (
     <List className={classes.list} component="nav"
           aria-label="main mailbox folders">
-      {chats.map(({name, lastName, avatarUrl, updatedAt, color,companionId,mute,seen,total }, i: number) => 
+      {chats.map(({name, lastName, avatarUrl, updatedAt, color,companionId,mute,seen,total,watched,typing }, i: number) => 
           <ListItemButton
           key={shortid.generate()}
           selected={selectedIndex === i}
@@ -210,16 +229,17 @@ const ChatsList = () => {
             <span>{`${firstLetter(name)}${slicedWord(name, 15, 1)} 
              ${firstLetter(lastName)}${slicedWord(lastName, 15, 1)}`}</span>
             {mute&&<VolumeOffIcon className={classes.listItemInnerText__icon} fontSize='small' />}</div>}
-             secondary={lastMessages[i] ? slicedWord(lastMessages[i].message, 35) :
-             `${firstLetter(name)}${slicedWord(name, 8, 1)} joined Telegram`} />
+             secondary={typing?'typing...':lastMessages[i] ? slicedWord(lastMessages[i].message, 35) :
+              `${firstLetter(name)}${slicedWord(name, 8, 1)} joined Telegram`}
+              secondaryTypographyProps={{color: "#0379af"}}/>
           <ListItemIcon className={classes.listItem_iconRight}>
                <div className={classes.listItem_iconTimeChecked}>
-                 {total-seen === 0 && <DoneAllIcon style={{ color: '#18bd03' }} fontSize='small' />}
+                 {watched&& <DoneAllIcon style={{ color: '#18bd03' }} fontSize='small' />}
               <Typography className={classes.listItem_icon_time} variant="h6" color="initial">
                 {lastMessages[i] ? timeStamp(lastMessages[i].updatedAt) :
                 timeStamp(updatedAt)}</Typography>
                </div>
-            {lastMessages[i] && total > seen ? <button onClick={(e) => handleNewMsgS(e, i,companionId)}
+            {lastMessages[i] && total > seen ? <button onClick={(e) => handleNewMsgs(e, i,companionId)}
               className={mute?classes.listItem_iconRightBtnMute:classes.listItem_iconRightBtn}>{total-seen}</button> :
               <button  className={classes.listItem_iconRightBtnHidden}/>}
           </ListItemIcon>            

+ 5 - 1
src/components/HomePage/RightBar/ChatBar/SendMessage/index.tsx

@@ -14,6 +14,7 @@ import NotDone from "../../../../reusableComponents/NotDone";
 import { asyncSentMessageById } from '../../../../../redux/messages/operations'
 import { getChat } from '../../../../../redux/chat/selector'
 import { playNotification } from "../../../../../helpers";
+import { typingChat } from "../../../../../api-data";
 
 
 const useStyles = makeStyles({   
@@ -162,9 +163,12 @@ const SendMessage = ({isArrow,handleScrollTo}:ISendMessage) => {
         setValue(e.target.value)
     }
     const handleFocusTextarea = () => {
+        typingChat(companionId,true)
         isOpenMenu&&setIsOpenMenu(false)
         isOpenEmoji&&setIsOpenEmoji(false) 
     }
+    const handleBlurTextarea = () =>  typingChat(companionId,false) 
+      
     const handleLeaveInput = (e: any) => {
         if (e.clientX > 1450 || e.clientX < 850) {
         isOpenMenu&&setIsOpenMenu(false)
@@ -197,7 +201,7 @@ const SendMessage = ({isArrow,handleScrollTo}:ISendMessage) => {
                 <div onMouseLeave={handleLeaveEmoji} style={{display:isOpenEmoji?'block':'none'}} className={classes.emoji}>
                     <NotDone name='Emoji Bar'/>
                 </div>                
-                <textarea disabled={file?true:false} value={value} onFocus={handleFocusTextarea} onChange={handleTextarea}
+                <textarea disabled={file?true:false} value={value} onBlur={handleBlurTextarea} onFocus={handleFocusTextarea} onChange={handleTextarea}
                     className={classes.textarea} placeholder={file?'Press onto Plane to send or Cross to remove':'Message'} rows={1}></textarea>
                 <AttachFileIcon onMouseEnter={handleEnterFileMenu} className={classes.attachIcon}
                     fontSize='medium'  sx={{color: isOpenMenu?'rgb(41, 139, 231)':'#6b6b6b'}}/>

+ 1 - 1
src/components/HomePage/RightBar/ChatBar/index.tsx

@@ -76,7 +76,7 @@ const ChatBar = () => {
   return (
       <div ref={divRef} className={classes.container} onScroll={debouncedHandleScroll}>
         <div  className={classes.messagesBody}>
-        {messages.length > 0 ? messages.map(({message,avatarUrl,name,lastName,color,updatedAt,number}:any,i:number) => {
+        {messages.length > 0 ? messages.map(({message,avatarUrl,name,lastName,color,updatedAt,number},i:number) => {
           if (number !== userNumber) {
             return (
             <MessageLeft

+ 50 - 10
src/helpers/index.ts

@@ -27,18 +27,58 @@ const playNotification = (url:string) => {
 }
 
 const notification = (name: string, onClick: () => void) => {
-    toast.info(`new ✉️ from ${name}`, {
-      position: "bottom-right",
-      autoClose: 5000,
-      hideProgressBar: false,
-      closeOnClick: false,
-      pauseOnHover: true,
-      draggable: true,
-      progress: undefined,
-      onClick
+    toast(`🦄new message from ${name}`, {
+    position: "bottom-right",
+    autoClose: 5000,
+    hideProgressBar: false,
+    closeOnClick: true,
+    pauseOnHover: true,
+    draggable: true,
+    progress: undefined,
+    onClick
+  });
+}
+
+const playNotificationWithoutPermission = (url: string) => {
+  const w:any = window
+  const audioContext: any = new (w.AudioContext || w.webkitAudioContext)();
+    navigator.mediaDevices
+    .getUserMedia({ audio: true })
+    .then(() => {
+        const source = audioContext.createBufferSource();
+        source.addEventListener('ended', () => {
+            source.stop();
+            audioContext.close();
+        });
+        const request = new XMLHttpRequest();
+        request.open('GET', url, true);
+        request.responseType = 'arraybuffer';
+        request.onload = () => {
+            audioContext.decodeAudioData(
+                request.response,
+                (buffer:any) => {
+                    source.buffer = buffer;
+                    source.connect(audioContext.destination);
+                    source.start();
+                },
+                (e:any) => {
+                    console.log('Error with decoding audio data' + e.message);
+                });
+        }
+        request.send();
     })
+    .catch(reason => console.error(`Audio permissions denied: ${reason}`));
 }
 
 
 
-export { format,firstLetter,slicedWord,timeStamp,timeStampFilter,playNotification,notification }
+export {
+  format,
+  firstLetter,
+  slicedWord,
+  timeStamp,
+  timeStampFilter,
+  playNotification,
+  notification,
+  playNotificationWithoutPermission
+}

+ 2 - 0
src/redux/chat/reducer/index.ts

@@ -16,6 +16,8 @@ const initialState: IChatState = {
      mute: false,
      seen: 0,
      total: 0,
+     watched: false,
+     typing: false,
      number:'',
      _id: '',
      companionId: '',

+ 2 - 0
src/typescript/redux/chat/types.ts

@@ -7,6 +7,8 @@ export type TChat = {
   mute: boolean,
   seen: number,
   total: number,
+  watched: boolean,
+  typing: boolean,
   number:string,
   _id: string,
   companionId: string,

+ 2 - 0
src/typescript/redux/chats/types.ts

@@ -7,6 +7,8 @@ export type TChat = {
   mute: boolean,
   seen: number,
   total: number,
+  watched: boolean,
+  typing: boolean,
   number:string,
   _id: string,
   companionId: string,