CFE-22 Merge branch 'feature/CFE-22_call_widget_btn_dialog_socket' into 'develop'

This commit is contained in:
AmirHossein Mahmoodi
2023-10-07 11:37:48 +00:00
20 changed files with 320 additions and 83 deletions

View File

@@ -15,6 +15,8 @@
"between": "میان",
"online_message": "شما به اینترنت وصل هستید",
"offline_message": "اتصال شما به اینترنت قطع شده است",
"socket_is_connect_message": "شما به سوکت وصل هستید",
"socket_is_not_connect_message": "اتصال شما به سوکت قطع شده است",
"header": {
"open_profile": "پروفایل",
"edit_profile": " پروفایل",

View File

@@ -1,10 +1,12 @@
import PermPhoneMsgIcon from '@mui/icons-material/PermPhoneMsg';
import StyledFab from "@/core/components/StyledFab";
import useCallDialog from "@/lib/callWidget/hooks/useCallDialog";
const CallWidgetButton = ({setOpen}) => {
const CallWidgetButton = () => {
const {setOpenCallDialog} = useCallDialog()
return (
<StyledFab color="primary" aria-label="open-dialog"
onClick={() => setOpen(true)}>
onClick={() => setOpenCallDialog(true)}>
<PermPhoneMsgIcon/>
</StyledFab>
)

View File

@@ -0,0 +1,7 @@
const CallActions = ({tab}) => {
return (
<>CallActions - {tab.id}</>
)
}
export default CallActions

View File

@@ -0,0 +1,7 @@
const CallHistory = ({tab}) => {
return (
<>CallHistory - {tab.id}</>
)
}
export default CallHistory

View File

@@ -0,0 +1,19 @@
import {Grid} from "@mui/material";
import CallActions from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions";
import CallHistory from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory";
const CallTabPanel = ({tab}) => {
return (
<Grid container columns={{xs: 3}} sx={{display: !tab.active && 'none'}} role="tabpanel"
id={`answer-tabpanel-${tab.id}`} aria-labelledby={`answer-tab-${tab.id}`}>
<Grid item xs={2}>
<CallActions tab={tab}/>
</Grid>
<Grid item xs={1}>
<CallHistory tab={tab}/>
</Grid>
</Grid>
)
}
export default CallTabPanel

View File

@@ -0,0 +1,23 @@
import {IconButton, Stack, Typography, Zoom} from "@mui/material";
import CloseIcon from '@mui/icons-material/Close';
import useAnswers from "@/lib/callWidget/hooks/useAnswers";
const CallTabLabel = ({tab}) => {
const {closeAnswerTab} = useAnswers()
const handleCloseClick = (id) => {
closeAnswerTab(id)
};
return (
<Stack sx={{width: '100%'}} direction={'row'} alignItems={'center'} justifyContent={'space-between'}>
<Typography>{tab.phone_number} - {tab.id}</Typography>
<Zoom in={tab.active}>
<IconButton onClick={() => handleCloseClick(tab.id)}>
<CloseIcon/>
</IconButton>
</Zoom>
</Stack>
)
}
export default CallTabLabel

View File

@@ -0,0 +1,27 @@
import {Box, Tab, Tabs} from "@mui/material";
import CallTabLabel
from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabLabel";
import useAnswers from "@/lib/callWidget/hooks/useAnswers";
const CallTabsList = () => {
const {answersList, changeActiveTabAnswers, activeTab} = useAnswers()
const handleChange = (event, newValue) => {
changeActiveTabAnswers(newValue)
};
return (
<Box sx={{borderBottom: 1, borderColor: 'divider'}}>
<Tabs
value={activeTab}
onChange={handleChange}
variant={'fullWidth'}
>
{answersList.map((answer) => (
<Tab component={'div'} sx={{py: 0.5, pr: 0,}} label={<CallTabLabel tab={answer}/>} key={answer.id}/>
))}
</Tabs>
</Box>
)
}
export default CallTabsList

View File

@@ -0,0 +1,17 @@
import CallTabsList from "src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList";
import CallTabPanel from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel";
import useAnswers from "@/lib/callWidget/hooks/useAnswers";
const CallTabs = () => {
const {answersList} = useAnswers()
return (
<>
<CallTabsList/>
{answersList.map((answer) => (
<CallTabPanel key={answer.id} tab={answer}/>
))}
</>
)
}
export default CallTabs

View File

@@ -1,23 +1,47 @@
import {Dialog} from "@mui/material";
import {DialogTransition} from "@/core/components/DialogTransition";
import {useEffect} from "react";
import useCallDialog from "@/lib/callWidget/hooks/useCallDialog";
import CallTabs from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs";
import useAnswers from "@/lib/callWidget/hooks/useAnswers";
import useSocket from "@/lib/app/hooks/useSocket";
const CallWidgetDialog = ({open, setOpen}) => {
const CallWidgetDialog = () => {
const {openCallDialog, setOpenCallDialog} = useCallDialog()
const {answersList, newAnswerTab} = useAnswers()
const socket = useSocket()
const handleClose = () => {
setOpen(false);
};
useEffect(() => {
const answerCall = (data, act) => {
newAnswerTab({
id: data.id,
phone_number: data.phone_number
})
act()
}
socket.on('answer', answerCall)
return () => {
socket.off('answer', answerCall)
}
}, []);
useEffect(() => {
if (answersList.length === 0) {
setOpenCallDialog(false)
return
}
setOpenCallDialog(true)
}, [answersList.length]);
return (
<>
<Dialog
fullScreen
open={open}
onClose={handleClose}
open={openCallDialog}
TransitionComponent={DialogTransition}
>
<CallTabs/>
</Dialog>
</>
)
}

View File

@@ -1,14 +1,27 @@
import CallWidgetButton from "@/components/layouts/Dashboard/CallWidget/CallWidgetButton";
import CallWidgetDialog from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog";
import {useState} from "react";
import {AnswersProvider} from "@/lib/callWidget/contexts/answers";
import useSocket from "@/lib/app/hooks/useSocket";
import {useEffect} from "react";
const CallWidget = () => {
const [open, setOpen] = useState(false)
const socket = useSocket()
useEffect(() => {
if (socket.connected) return
socket.connect()
return () => {
socket.disconnect()
}
}, []);
return (
<>
<CallWidgetButton setOpen={setOpen}/>
<CallWidgetDialog open={open} setOpen={setOpen}/>
</>
<AnswersProvider>
<CallWidgetButton/>
<CallWidgetDialog/>
</AnswersProvider>
)
}

View File

@@ -6,13 +6,13 @@ import WifiOffIcon from '@mui/icons-material/WifiOff';
import {useTranslations} from "next-intl";
const NetworkComponent = () => {
const toastId = useRef(null);
const networkToastId = useRef(null);
const network = useNetwork()
const t = useTranslations()
useEffect(() => {
if (network.online) {
toast.update(toastId.current, {
toast.update(networkToastId.current, {
type: toast.TYPE.SUCCESS,
render: t('online_message'),
autoClose: 2000,
@@ -22,8 +22,7 @@ const NetworkComponent = () => {
});
return
}
toast.dismiss()
toastId.current = toast.warn(t('offline_message'), {
networkToastId.current = toast.warn(t('offline_message'), {
autoClose: false, closeButton: false, closeOnClick: false, icon: <WifiOffIcon/>
})
}, [network.online]);

View File

@@ -5,7 +5,6 @@ const StyledFab = styled(Fab)`
bottom: 16px;
/* @noflip */
left: 16px;
z-index: 1500;
`;
export default StyledFab;

View File

@@ -22,26 +22,23 @@ const MuiLayout = ({children, isBot}) => {
<ThemeProvider theme={theme}>
<GlobalStyles
styles={{
"*:not(.MuiTableContainer-root)::-webkit-scrollbar": {
display: "none",
},
"*::-webkit-scrollbar": {
height: "8px",
},
"*:not(.MuiTableContainer-root)::-webkit-scrollbar": {
display: "none",
},
"*::-webkit-scrollbar-thumb": {
background: `${theme.palette.primary.light}80`,
borderRadius: "3px",
},
"*:not(.MuiTableContainer-root)": {
scrollbarWidth: "thin",
scrollbarColor: "transparent transparent",
},
"*": {
scrollbarWidth: "thin",
scrollbarColor: `${theme.palette.primary.light}80 transparent`,
},
"*:not(.MuiTableContainer-root)": {
scrollbarWidth: "none",
},
"*::-moz-scrollbar-thumb": {
backgroundColor: `${theme.palette.primary.light}80`,
},

View File

@@ -1,9 +1,18 @@
import {createContext, useMemo} from "react";
import {createContext, useEffect, useMemo, useRef, useState} from "react";
import {io} from "socket.io-client";
import useUser from "@/lib/app/hooks/useUser";
import {useTranslations} from "next-intl";
import {toast} from "react-toastify";
import PowerIcon from "@mui/icons-material/Power";
import PowerOffIcon from "@mui/icons-material/PowerOff";
export const SocketContext = createContext()
export const SocketProvider = ({children}) => {
const {user, isAuth} = useUser()
const [connectionError, setConnectionError] = useState(false)
const socketToastId = useRef(null);
const t = useTranslations()
const socket = useMemo(() => io(process.env.NEXT_PUBLIC_SERVER_SOCKET_URL, {
autoConnect: false,
auth: {
@@ -11,5 +20,48 @@ export const SocketProvider = ({children}) => {
}
}), [])
useEffect(() => {
if (isAuth) {
socket.auth.token = user.telephone_id
return
}
socket.auth.token = ''
}, [isAuth]);
useEffect(() => {
const connectErrorHandler = () => {
setConnectionError(true)
}
const connectHandler = () => {
setConnectionError(false)
}
socket.on("connect_error", connectErrorHandler);
socket.on("connect", connectHandler);
return () => {
socket.off("connect_error", connectErrorHandler);
socket.off("connect", connectHandler);
}
}, []);
useEffect(() => {
if (!connectionError) {
toast.update(socketToastId.current, {
type: toast.TYPE.SUCCESS,
render: t('socket_is_connect_message'),
autoClose: 2000,
closeButton: true,
closeOnClick: true,
icon: <PowerIcon/>
});
return
}
socketToastId.current = toast.warn(t('socket_is_not_connect_message'), {
draggable: false,
autoClose: false, closeButton: false, closeOnClick: false, icon: <PowerOffIcon/>
})
}, [connectionError]);
return <SocketContext.Provider value={{socket}}>{children}</SocketContext.Provider>
}

View File

@@ -2,7 +2,6 @@ import {GET_USER_ROUTE} from "@/core/data/apiRoutes";
import axios from "axios";
import {createContext, useCallback, useEffect, useReducer} from "react";
import {errorRequest, errorResponse, errorSetting} from "@/core/utils/errorHandler";
import useSocket from "@/lib/app/hooks/useSocket";
const initialUser = {
isAuth: false,
@@ -40,7 +39,6 @@ const reducer = (state, action) => {
export const UserContext = createContext();
export const UserProvider = ({children}) => {
const socket = useSocket()
const [state, dispatch] = useReducer(reducer, initialUser);
const clearUser = useCallback(() => {
@@ -102,16 +100,12 @@ export const UserProvider = ({children}) => {
clearUser();
changeAuthState(false);
changeLanguageState(false);
socket.auth.token = ""
return;
}
getUser((data) => {
changeUser(data);
changeAuthState(true);
changeLanguageState(true);
socket.auth.token = data.telephone_id
});
}, [state.token]);

View File

@@ -1,29 +1,5 @@
import {useEffect, useState} from "react";
function getNetworkConnection() {
return (
navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection ||
null
);
}
function getNetworkConnectionInfo() {
const connection = getNetworkConnection();
if (!connection) {
return {};
}
return {
rtt: connection.rtt,
type: connection.type,
saveData: connection.saveData,
downLink: connection.downLink,
downLinkMax: connection.downLinkMax,
effectiveType: connection.effectiveType,
};
}
function useNetwork() {
const [state, setState] = useState(() => {
return {

View File

@@ -0,0 +1,31 @@
import {createContext, useReducer, useState} from "react";
const reducer = (state, action) => {
switch (action.type) {
case "CHANGE_ACTIVE_TAB":
return state.map((answer, index) => action.newValue === index ? {...answer, active: true} : {
...answer,
active: false
});
case "CHANGE_LIST":
return [...action.newList]
default:
return state;
}
}
export const AnswersContext = createContext()
export const AnswersProvider = ({children}) => {
const [openCallDialog, setOpenCallDialog] = useState(false)
const [activeTab, setActiveTab] = useState(0)
const [state, dispatch] = useReducer(reducer, []);
return <AnswersContext.Provider
value={{
openCallDialog,
setOpenCallDialog,
state,
dispatch,
activeTab,
setActiveTab
}}>{children}</AnswersContext.Provider>
}

View File

@@ -0,0 +1,38 @@
import {useContext} from "react";
import {AnswersContext} from "@/lib/callWidget/contexts/answers";
const useAnswers = () => {
const {state, dispatch, activeTab, setActiveTab} = useContext(AnswersContext)
const changeActiveTabAnswers = (newValue) => {
dispatch({type: 'CHANGE_ACTIVE_TAB', newValue})
setActiveTab(newValue)
}
const newAnswerTab = (data) => {
const newAnswer = {
id: data.id,
phone_number: data.phone_number,
active: true
}
const newList = [...state, newAnswer]
dispatch({type: 'CHANGE_LIST', newList})
changeActiveTabAnswers(newList.length - 1)
}
const closeAnswerTab = (id) => {
const index = state.findIndex(answer => answer.id === id)
const newIndex = index === 0 ? 0 : index - 1
const newList = [...state]
newList.splice(index, 1);
if (newList.length !== 0) {
newList[newIndex].active = true
setActiveTab(newIndex)
}
dispatch({type: 'CHANGE_LIST', newList})
}
return {answersList: state, activeTab, setActiveTab, changeActiveTabAnswers, closeAnswerTab, newAnswerTab}
}
export default useAnswers

View File

@@ -0,0 +1,10 @@
import {useContext} from "react";
import {AnswersContext} from "@/lib/callWidget/contexts/answers";
const useCallDialog = () => {
const {openCallDialog, setOpenCallDialog} = useContext(AnswersContext)
return {openCallDialog, setOpenCallDialog}
}
export default useCallDialog

View File

@@ -12,7 +12,6 @@ import {SocketProvider} from "@/lib/app/contexts/socket";
const App = ({Component, pageProps}) => {
return (<>
<SocketProvider>
<UserProvider>
<LanguageProvider>
<NextIntlProvider locale={pageProps.locale} messages={pageProps.messages || {}}>
@@ -20,14 +19,15 @@ const App = ({Component, pageProps}) => {
{pageProps.messages ? <TitlePage text={pageProps.title}/> : ''}
<LoadingProvider>
<AppLayout isBot={pageProps.isBot}>
<SocketProvider>
<Component {...pageProps} />
</SocketProvider>
</AppLayout>
</LoadingProvider>
</MuiLayout>
</NextIntlProvider>
</LanguageProvider>
</UserProvider>
</SocketProvider>
</>)
};