diff --git a/phone/src/Phone.tsx b/phone/src/Phone.tsx index bde796a44..bcdb2c02d 100644 --- a/phone/src/Phone.tsx +++ b/phone/src/Phone.tsx @@ -10,7 +10,6 @@ import { useSimcardService } from './os/simcard/hooks/useSimcardService'; import { usePhoneService } from './os/phone/hooks/usePhoneService'; import { useApps } from './os/apps/hooks/useApps'; import { useNuiRequest } from 'fivem-nui-react-lib'; -import { useContactsService } from './apps/contacts/hooks/useContactsService'; import { useTwitterService } from './apps/twitter/hooks/useTwitterService'; import { useMatchService } from './apps/match/hooks/useMatchService'; import { useMarketplaceService } from './apps/marketplace/hooks/useMarketplaceService'; @@ -71,7 +70,6 @@ function Phone() { useKeyboardService(); usePhoneService(); useSimcardService(); - useContactsService(); useTwitterService(); useMatchService(); useMarketplaceService(); diff --git a/phone/src/apps/contacts/components/ContactsApp.tsx b/phone/src/apps/contacts/components/ContactsApp.tsx index 6419eb120..d4d97c0f2 100644 --- a/phone/src/apps/contacts/components/ContactsApp.tsx +++ b/phone/src/apps/contacts/components/ContactsApp.tsx @@ -5,7 +5,6 @@ import { AppContent } from '../../../ui/components/AppContent'; import { useApp } from '../../../os/apps/hooks/useApps'; import InjectDebugData from '../../../os/debug/InjectDebugData'; import { Route } from 'react-router-dom'; - import ContactsInfoPage from './views/ContactInfo'; import { ContactPage } from './views/ContactsPage'; import { ContactsThemeProvider } from '../providers/ContactsThemeProvider'; @@ -14,6 +13,7 @@ import PersonAddIcon from '@material-ui/icons/PersonAdd'; import Fab from '@material-ui/core/Fab'; import { useHistory } from 'react-router'; import { makeStyles, Theme } from '@material-ui/core/styles'; +import { LoadingSpinner } from '../../../ui/components/LoadingSpinner'; const useStyles = makeStyles((theme: Theme) => ({ absolute: { @@ -33,8 +33,10 @@ export const ContactsApp = () => { - - + }> + + + { ); }; - -InjectDebugData([ - { - app: 'CONTACTS', - method: ContactEvents.SEND_CONTACTS, - data: [ - { - id: 1, - display: 'Ruqen', - number: '555-15196', - }, - { - id: 2, - display: 'Taso', - number: '215-8139', - avatar: 'http://i.tasoagc.dev/i9Ig', - }, - { - id: 3, - display: 'Chip', - number: '603-275-8373', - avatar: 'http://i.tasoagc.dev/2QYV', - }, - { - id: 4, - display: 'Kidz', - number: '444-4444', - }, - ], - }, -]); diff --git a/phone/src/apps/contacts/components/List/ContactList.tsx b/phone/src/apps/contacts/components/List/ContactList.tsx index e376c45c7..d250c2c71 100644 --- a/phone/src/apps/contacts/components/List/ContactList.tsx +++ b/phone/src/apps/contacts/components/List/ContactList.tsx @@ -1,24 +1,21 @@ import React from 'react'; import ListItemText from '@material-ui/core/ListItemText'; import { Button, ListItemAvatar, Avatar as MuiAvatar, List, ListItem } from '@material-ui/core'; -import { useFilteredContacts } from '../../hooks/useFilteredContacts'; import PhoneIcon from '@material-ui/icons/Phone'; import ChatIcon from '@material-ui/icons/Chat'; import MoreVertIcon from '@material-ui/icons/MoreVert'; -import { useContacts } from '../../hooks/useContacts'; import { SearchContacts } from './SearchContacts'; import { useHistory } from 'react-router-dom'; import LogDebugEvent from '../../../../os/debug/LogDebugEvents'; import { CallEvents } from '../../../../../../typings/call'; import { useNuiRequest } from 'fivem-nui-react-lib'; +import { useFilteredContacts } from '../../hooks/state'; export const ContactList = () => { - const { filteredContacts } = useFilteredContacts(); + const filteredContacts = useFilteredContacts(); const history = useHistory(); const Nui = useNuiRequest(); - const { contacts } = useContacts(); - const openContactInfo = (contactId: number) => { history.push(`/contacts/${contactId}`); }; @@ -43,38 +40,31 @@ export const ContactList = () => { history.push(`/messages/new/${phoneNumber}`); }; - const filteredRegEx = new RegExp(filteredContacts, 'gi'); - return ( <> - {contacts - .filter( - (contact) => - contact.display.match(filteredRegEx) || contact.number.match(filteredRegEx), - ) - .map((contact) => ( - - - {contact.avatar ? ( - - ) : ( - {contact.display.slice(0, 1).toUpperCase()} - )} - - - - - - - ))} + {filteredContacts.map((contact) => ( + + + {contact.avatar ? ( + + ) : ( + {contact.display.slice(0, 1).toUpperCase()} + )} + + + + + + + ))} ); diff --git a/phone/src/apps/contacts/components/List/SearchContacts.tsx b/phone/src/apps/contacts/components/List/SearchContacts.tsx index dc6be54e5..4b3f3e344 100644 --- a/phone/src/apps/contacts/components/List/SearchContacts.tsx +++ b/phone/src/apps/contacts/components/List/SearchContacts.tsx @@ -1,21 +1,29 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Box } from '@material-ui/core'; import { useTranslation } from 'react-i18next'; -import { useFilteredContacts } from '../../hooks/useFilteredContacts'; import { SearchField } from '../../../../ui/components/SearchField'; +import { useDebounce } from '../../../../os/phone/hooks/useDebounce'; +import { useSetContactFilterInput } from '../../hooks/state'; export const SearchContacts = () => { - const { setFilteredContacts, filteredContacts } = useFilteredContacts(); const { t } = useTranslation(); + const setFilterVal = useSetContactFilterInput(); + const [inputVal, setInputVal] = useState(''); + + const debouncedVal = useDebounce(inputVal, 500); + + useEffect(() => { + setFilterVal(debouncedVal); + }, [debouncedVal, setFilterVal]); return ( setFilteredContacts(e.target.value)} + onChange={(e) => setInputVal(e.target.value)} placeholder={t('APPS_CONTACT_PLACEHOLDER_SEARCH_CONTACTS')} - value={filteredContacts} + value={inputVal} /> ); diff --git a/phone/src/apps/contacts/components/views/ContactInfo.tsx b/phone/src/apps/contacts/components/views/ContactInfo.tsx index e87785715..cce1ddfb5 100644 --- a/phone/src/apps/contacts/components/views/ContactInfo.tsx +++ b/phone/src/apps/contacts/components/views/ContactInfo.tsx @@ -3,13 +3,15 @@ import { Avatar as MuiAvatar, Box, Button, Paper } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import { useTranslation } from 'react-i18next'; import { useHistory, useParams } from 'react-router-dom'; -import { useContacts } from '../../hooks/useContacts'; -import { useNuiRequest } from 'fivem-nui-react-lib'; +import { useContactActions } from '../../hooks/useContactActions'; import ArrowBackIcon from '@material-ui/icons/ArrowBack'; import LogDebugEvent from '../../../../os/debug/LogDebugEvents'; import { useQueryParams } from '../../../../common/hooks/useQueryParams'; -import { ContactEvents } from '../../../../../../typings/contact'; +import { Contact, ContactEvents } from '../../../../../../typings/contact'; import { TextField } from '../../../../ui/components/Input'; +import { fetchNui } from '../../../../utils/fetchNui'; +import { ServerPromiseResp } from '../../../../../../typings/common'; +import { useSnackbar } from '../../../../ui/hooks/useSnackbar'; interface ContactInfoRouteParams { mode: string; @@ -49,11 +51,12 @@ const useStyles = makeStyles({ }); const ContactsInfoPage = () => { - const Nui = useNuiRequest(); const classes = useStyles(); const history = useHistory(); - const { getContact } = useContacts(); + const { getContact, addContact, updateContact, deleteContact } = useContactActions(); + + const { addAlert } = useSnackbar(); const { id } = useParams(); const { addNumber, referal } = useQueryParams({ @@ -81,12 +84,27 @@ const ContactsInfoPage = () => { data: contact, level: 2, }); - Nui.send(ContactEvents.ADD_CONTACT, { + fetchNui>(ContactEvents.ADD_CONTACT, { display: name, number, avatar, + }).then((serverResp) => { + if (serverResp.status !== 'ok') { + return addAlert({ + message: t('APPS_CONTACT_ADD_FAILED'), + type: 'error', + }); + } + + // Sanity checks maybe? + + addContact(serverResp.data); + addAlert({ + message: t('APPS_CONTACT_ADD_SUCCESS'), + type: 'error', + }); + history.replace(referal); }); - history.replace(referal); }; const handleContactSave = () => { @@ -95,13 +113,33 @@ const ContactsInfoPage = () => { data: contact, level: 2, }); - Nui.send(ContactEvents.UPDATE_CONTACT, { + fetchNui(ContactEvents.UPDATE_CONTACT, { id: contact.id, display: name, number, avatar, + }).then((resp) => { + if (resp.status !== 'ok') { + return addAlert({ + message: t('APPS_CONTACT_UPDATE_FAILED'), + type: 'error', + }); + } + + updateContact({ + id: contact.id, + display: name, + number, + avatar, + }); + + addAlert({ + message: t('APPS_CONTACT_UPDATE_SUCCESS'), + type: 'success', + }); + + history.goBack(); }); - history.goBack(); }; const handleContactDelete = () => { @@ -110,8 +148,20 @@ const ContactsInfoPage = () => { data: contact, level: 2, }); - Nui.send(ContactEvents.DELETE_CONTACT, contact.id); - history.goBack(); + fetchNui(ContactEvents.DELETE_CONTACT, { id: contact.id }).then((resp) => { + if (resp.status !== 'ok') { + return addAlert({ + message: t('APPS_CONTACT_DELETE_FAILED'), + type: 'error', + }); + } + history.goBack(); + deleteContact(contact.id); + addAlert({ + message: t('APPS_CONTACT_DELETE_SUCCESS'), + type: 'error', + }); + }); }; return ( diff --git a/phone/src/apps/contacts/hooks/state.ts b/phone/src/apps/contacts/hooks/state.ts index 1483da908..58079dc40 100644 --- a/phone/src/apps/contacts/hooks/state.ts +++ b/phone/src/apps/contacts/hooks/state.ts @@ -1,17 +1,57 @@ -import { atom } from 'recoil'; -import { Contact } from '../../../../../typings/contact'; +import { atom, selector, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { Contact, ContactEvents } from '../../../../../typings/contact'; +import { fetchNui } from '../../../utils/fetchNui'; +import { ServerPromiseResp } from '../../../../../typings/common'; +import { isEnvBrowser } from '../../../utils/misc'; +import LogDebugEvent from '../../../os/debug/LogDebugEvents'; +import { BrowserContactsState } from '../utils/constants'; export const contactsState = { contacts: atom({ key: 'contactsList', - default: [], + default: selector({ + key: 'contactsListDefault', + get: async () => { + try { + const resp = await fetchNui>(ContactEvents.GET_CONTACTS); + LogDebugEvent({ action: 'ContactsFetched', data: resp.data }); + return resp.data; + } catch (e) { + if (isEnvBrowser()) { + return BrowserContactsState; + } + console.error(e); + return []; + } + }, + }), }), - showModal: atom({ - key: 'showModal', - default: false, - }), - filterContacts: atom({ - key: 'filterContacts', + filterInput: atom({ + key: 'filterInput', default: '', }), + filteredContacts: selector({ + key: 'filteredContacts', + get: ({ get }) => { + const filterInputVal: string = get(contactsState.filterInput); + const contacts: Contact[] = get(contactsState.contacts); + + if (!filterInputVal) return contacts; + + const regExp = new RegExp(filterInputVal, 'gi'); + + return contacts.filter( + (contact) => contact.display.match(regExp) || contact.number.match(regExp), + ); + }, + }), }; + +export const useSetContacts = () => useSetRecoilState(contactsState.contacts); +export const useContacts = () => useRecoilState(contactsState.contacts); +export const useContactsValue = () => useRecoilValue(contactsState.contacts); + +export const useFilteredContacts = () => useRecoilValue(contactsState.filteredContacts); + +export const useContactFilterInput = () => useRecoilState(contactsState.filterInput); +export const useSetContactFilterInput = () => useSetRecoilState(contactsState.filterInput); diff --git a/phone/src/apps/contacts/hooks/useContactActions.ts b/phone/src/apps/contacts/hooks/useContactActions.ts new file mode 100644 index 000000000..8b6a9747b --- /dev/null +++ b/phone/src/apps/contacts/hooks/useContactActions.ts @@ -0,0 +1,91 @@ +import { useContacts } from './state'; + +import { Contact } from '../../../../../typings/contact'; +import { useCallback } from 'react'; + +interface UseContactsValue { + getDisplayByNumber: (number: string) => string; + getContactByNumber: (number: string) => Contact | null; + getContact: (id: number) => Contact | null; + getPictureByNumber: (number: string) => string | null; + deleteContact: (id: number) => void; + addContact: (contact: Contact) => void; + updateContact: (contact: Contact) => void; +} + +export const useContactActions = (): UseContactsValue => { + const [contacts, setContacts] = useContacts(); + + const getDisplayByNumber = useCallback( + (number: string) => { + const found = contacts.find((contact) => contact.number === number); + return found ? found.display : number; + }, + [contacts], + ); + + const getPictureByNumber = useCallback( + (number: string) => { + const found = contacts.find((contact) => contact.number === number); + return found ? found.avatar : null; + }, + [contacts], + ); + + const getContactByNumber = useCallback( + (number: string): Contact | null => { + for (const contact of contacts) { + if (contact.number === number) return contact; + } + return null; + }, + [contacts], + ); + + const getContact = useCallback( + (id: number): Contact | null => { + for (const contact of contacts) { + if (contact.id === id) return contact; + } + return null; + }, + [contacts], + ); + + const deleteContact = useCallback( + (id: number): void => { + const contactIndex = contacts.findIndex((contact) => contact.id === id); + const newContacts = [...contacts].slice(contactIndex); + setContacts(newContacts); + }, + [contacts, setContacts], + ); + + const addContact = useCallback( + (contact: Contact) => { + const newContactState = [...contacts, contact]; + setContacts(newContactState); + }, + [contacts, setContacts], + ); + + const updateContact = useCallback( + (updatedContact: Contact) => { + const targetContactIndex = contacts.findIndex((contact) => contact.id === updatedContact.id); + const newContactsArray = [...contacts]; + newContactsArray[targetContactIndex] = updatedContact; + setContacts(newContactsArray); + }, + [contacts, setContacts], + ); + + return { + getDisplayByNumber, + getContact, + getContactByNumber, + getPictureByNumber, + deleteContact, + updateContact, + addContact, + }; +}; diff --git a/phone/src/apps/contacts/hooks/useContacts.ts b/phone/src/apps/contacts/hooks/useContacts.ts deleted file mode 100644 index e1381a6b5..000000000 --- a/phone/src/apps/contacts/hooks/useContacts.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useRecoilValue } from 'recoil'; -import { contactsState } from './state'; - -import { Contact } from '../../../../../typings/contact'; -import { useCallback } from 'react'; - -interface IUseContacts { - contacts: Contact[]; - getDisplayByNumber: (number: string) => string; - getContactByNumber: (number: string) => Contact | null; - getContact: (id: number) => Contact | null; - getPictureByNumber: (number: string) => string | null; -} - -export const useContacts = (): IUseContacts => { - const contacts = useRecoilValue(contactsState.contacts); - - const getDisplayByNumber = useCallback( - (number: string) => { - const found = contacts.find((contact) => contact.number === number); - return found ? found.display : number; - }, - [contacts], - ); - - const getPictureByNumber = useCallback( - (number: string) => { - const found = contacts.find((contact) => contact.number === number); - return found ? found.avatar : null; - }, - [contacts], - ); - - const getContactByNumber = useCallback( - (number: string): Contact | null => { - for (const contact of contacts) { - if (contact.number === number) return contact; - } - return null; - }, - [contacts], - ); - - const getContact = useCallback( - (id: number): Contact | null => { - for (const contact of contacts) { - if (contact.id === id) return contact; - } - return null; - }, - [contacts], - ); - - return { contacts, getDisplayByNumber, getContact, getContactByNumber, getPictureByNumber }; -}; diff --git a/phone/src/apps/contacts/hooks/useContactsService.ts b/phone/src/apps/contacts/hooks/useContactsService.ts deleted file mode 100644 index 721553a9d..000000000 --- a/phone/src/apps/contacts/hooks/useContactsService.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useNuiEvent } from 'fivem-nui-react-lib'; -import { useSetRecoilState } from 'recoil'; -import { contactsState } from './state'; -import { useContacts } from './useContacts'; -import { IAlert, useSnackbar } from '../../../ui/hooks/useSnackbar'; -import { useTranslation } from 'react-i18next'; -import { ContactEvents } from '../../../../../typings/contact'; - -export const useContactsService = () => { - const setContacts = useSetRecoilState(contactsState.contacts); - const { addAlert } = useSnackbar(); - const { t } = useTranslation(); - - const handleAddAlert = ({ message, type }: IAlert) => { - addAlert({ - message: t(`APPS_${message}`), - type, - }); - }; - - useNuiEvent('CONTACTS', ContactEvents.SEND_CONTACTS, setContacts); - useNuiEvent('CONTACTS', ContactEvents.SEND_ALERT, handleAddAlert); - return useContacts(); -}; diff --git a/phone/src/apps/contacts/hooks/useFilteredContacts.ts b/phone/src/apps/contacts/hooks/useFilteredContacts.ts deleted file mode 100644 index 75f761bfd..000000000 --- a/phone/src/apps/contacts/hooks/useFilteredContacts.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useRecoilState } from 'recoil'; -import { contactsState } from './state'; - -interface FilterdContactsProps { - filteredContacts: string; - setFilteredContacts: (searchTerm: string) => void; -} - -export const useFilteredContacts = (): FilterdContactsProps => { - const [filteredContacts, setFilteredContacts] = useRecoilState(contactsState.filterContacts); - return { filteredContacts, setFilteredContacts }; -}; diff --git a/phone/src/apps/contacts/hooks/useModal.ts b/phone/src/apps/contacts/hooks/useModal.ts deleted file mode 100644 index 559187768..000000000 --- a/phone/src/apps/contacts/hooks/useModal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useRecoilState } from 'recoil'; -import { contactsState } from './state'; - -interface ModalProps { - showModal: boolean; - setShowModal: (show: boolean) => void; -} - -export const useModal = (): ModalProps => { - const [showModal, setShowModal] = useRecoilState(contactsState.showModal); - return { showModal, setShowModal }; -}; diff --git a/phone/src/apps/contacts/utils/constants.ts b/phone/src/apps/contacts/utils/constants.ts new file mode 100644 index 000000000..6062b18ca --- /dev/null +++ b/phone/src/apps/contacts/utils/constants.ts @@ -0,0 +1,24 @@ +export const BrowserContactsState = [ + { + id: 1, + display: 'Ruqen', + number: '555-15196', + }, + { + id: 2, + display: 'Taso', + number: '215-8139', + avatar: 'http://i.tasoagc.dev/i9Ig', + }, + { + id: 3, + display: 'Chip', + number: '603-275-8373', + avatar: 'http://i.tasoagc.dev/2QYV', + }, + { + id: 4, + display: 'Kidz', + number: '444-4444', + }, +]; diff --git a/phone/src/apps/dialer/components/DialerApp.tsx b/phone/src/apps/dialer/components/DialerApp.tsx index fe6e92c67..86649e230 100644 --- a/phone/src/apps/dialer/components/DialerApp.tsx +++ b/phone/src/apps/dialer/components/DialerApp.tsx @@ -12,6 +12,13 @@ import InjectDebugData from '../../../os/debug/InjectDebugData'; import { ContactList } from '../../contacts/components/List/ContactList'; import { DialerThemeProvider } from '../providers/DialerThemeProvider'; import { CallEvents } from '../../../../../typings/call'; +import { Box, CircularProgress } from '@material-ui/core'; + +const LoadingSpinner: React.FC = () => ( + + + +); export const DialerApp = () => { const history = useDialHistory(); @@ -27,7 +34,9 @@ export const DialerApp = () => { - + }> + + diff --git a/phone/src/apps/dialer/components/views/DialerHistory.tsx b/phone/src/apps/dialer/components/views/DialerHistory.tsx index e9aaba903..3b3e07cdd 100644 --- a/phone/src/apps/dialer/components/views/DialerHistory.tsx +++ b/phone/src/apps/dialer/components/views/DialerHistory.tsx @@ -7,7 +7,7 @@ import { List } from '../../../../ui/components/List'; import { ListItem } from '../../../../ui/components/ListItem'; import { useNuiRequest } from 'fivem-nui-react-lib'; import { useSimcard } from '../../../../os/simcard/hooks/useSimcard'; -import { useContacts } from '../../../contacts/hooks/useContacts'; +import { useContactActions } from '../../../contacts/hooks/useContactActions'; import { CallEvents, CallHistoryItem } from '../../../../../../typings/call'; import { useTranslation } from 'react-i18next'; import { Box, IconButton, ListItemIcon, ListItemText } from '@material-ui/core'; @@ -27,7 +27,7 @@ const useStyles = makeStyles((theme: Theme) => ({ export const DialerHistory = ({ calls }) => { const Nui = useNuiRequest(); const { number: myNumber } = useSimcard(); - const { getDisplayByNumber } = useContacts(); + const { getDisplayByNumber } = useContactActions(); const classes = useStyles(); diff --git a/phone/src/apps/messages/components/form/NewMessageGroupForm.tsx b/phone/src/apps/messages/components/form/NewMessageGroupForm.tsx index 6911c521c..fe95365cc 100644 --- a/phone/src/apps/messages/components/form/NewMessageGroupForm.tsx +++ b/phone/src/apps/messages/components/form/NewMessageGroupForm.tsx @@ -4,12 +4,13 @@ import { Box, Button } from '@material-ui/core'; import { useNuiRequest } from 'fivem-nui-react-lib'; import { useHistory } from 'react-router-dom'; import { Autocomplete } from '@material-ui/lab'; -import { useContacts } from '../../../contacts/hooks/useContacts'; +import { useContactActions } from '../../../contacts/hooks/useContactActions'; import { Contact } from '../../../../../../typings/contact'; import { MessageEvents } from '../../../../../../typings/messages'; import { PHONE_NUMBER_REGEX } from '../../../../../../typings/phone'; import { useSnackbar } from '../../../../ui/hooks/useSnackbar'; import { TextField } from '../../../../ui/components/Input'; +import { useContactsValue } from '../../../contacts/hooks/state'; const NewMessageGroupForm = ({ phoneNumber }: { phoneNumber?: string }) => { const Nui = useNuiRequest(); @@ -18,7 +19,8 @@ const NewMessageGroupForm = ({ phoneNumber }: { phoneNumber?: string }) => { const { addAlert } = useSnackbar(); const [participants, setParticipants] = useState([]); const [label, setLabel] = useState(''); - const { getContactByNumber, contacts } = useContacts(); + const { getContactByNumber } = useContactActions(); + const contacts = useContactsValue(); useEffect(() => { if (phoneNumber) { diff --git a/phone/src/apps/messages/components/modal/MessageGroupModal.tsx b/phone/src/apps/messages/components/modal/MessageGroupModal.tsx index de5ac1e52..4cfbfc4ba 100644 --- a/phone/src/apps/messages/components/modal/MessageGroupModal.tsx +++ b/phone/src/apps/messages/components/modal/MessageGroupModal.tsx @@ -4,6 +4,7 @@ import ArrowBackIcon from '@material-ui/icons/ArrowBack'; import useStyles from './modal.styles'; import NewMessageGroupForm from '../form/NewMessageGroupForm'; import { useHistory, useParams } from 'react-router-dom'; +import { LoadingSpinner } from '../../../../ui/components/LoadingSpinner'; const MessageGroupModal = () => { const classes = useStyles(); @@ -16,10 +17,12 @@ const MessageGroupModal = () => { return ( - - + }> + + + ); diff --git a/phone/src/apps/messages/components/modal/MessageModal.tsx b/phone/src/apps/messages/components/modal/MessageModal.tsx index 8673ddf61..57650312e 100644 --- a/phone/src/apps/messages/components/modal/MessageModal.tsx +++ b/phone/src/apps/messages/components/modal/MessageModal.tsx @@ -21,7 +21,7 @@ import { useNuiRequest } from 'fivem-nui-react-lib'; import { useHistory, useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import Modal from '../../../../ui/components/Modal'; -import { useContacts } from '../../../contacts/hooks/useContacts'; +import { useContactActions } from '../../../contacts/hooks/useContactActions'; import { useSimcard } from '../../../../os/simcard/hooks/useSimcard'; import { MessageEvents } from '../../../../../../typings/messages'; @@ -56,7 +56,7 @@ export const MessageModal = () => { const { t } = useTranslation(); const { groupId } = useParams<{ groupId: string }>(); const { messages, setMessages, activeMessageGroup, setActiveMessageGroup } = useMessages(); - const { getContactByNumber, getDisplayByNumber } = useContacts(); + const { getContactByNumber, getDisplayByNumber } = useContactActions(); const [isLoaded, setLoaded] = useState(false); diff --git a/phone/src/os/call/components/CallContactContainer.tsx b/phone/src/os/call/components/CallContactContainer.tsx index e2cd528ee..a1df6f559 100644 --- a/phone/src/os/call/components/CallContactContainer.tsx +++ b/phone/src/os/call/components/CallContactContainer.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Avatar, Box, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; -import { useContacts } from '../../../apps/contacts/hooks/useContacts'; +import { useContactActions } from '../../../apps/contacts/hooks/useContactActions'; import { useCall } from '../hooks/useCall'; import { useTranslation } from 'react-i18next'; @@ -17,7 +17,7 @@ const CallContactContainer = () => { const { call } = useCall(); const classes = useStyles(); - const { getDisplayByNumber, getPictureByNumber } = useContacts(); + const { getDisplayByNumber, getPictureByNumber } = useContactActions(); const getDisplayOrNumber = () => call.isTransmitter ? getDisplayByNumber(call?.receiver) : getDisplayByNumber(call?.transmitter); diff --git a/phone/src/os/phone/hooks/useDebounce.ts b/phone/src/os/phone/hooks/useDebounce.ts new file mode 100644 index 000000000..10a152df2 --- /dev/null +++ b/phone/src/os/phone/hooks/useDebounce.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from 'react'; + +export const useDebounce = (value: T, delay = 100) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}; diff --git a/phone/src/ui/components/LoadingSpinner.tsx b/phone/src/ui/components/LoadingSpinner.tsx new file mode 100644 index 000000000..bf0306795 --- /dev/null +++ b/phone/src/ui/components/LoadingSpinner.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { Box, CircularProgress } from '@material-ui/core'; + +export const LoadingSpinner: React.FC = () => ( + + + +); diff --git a/phone/src/utils/misc.ts b/phone/src/utils/misc.ts new file mode 100644 index 000000000..3462f9042 --- /dev/null +++ b/phone/src/utils/misc.ts @@ -0,0 +1,3 @@ +// Quickly determine whether we are in browser +export const isEnvBrowser = (): boolean => + process.env.NODE_ENV === 'development' && !(window as any).invokeNative; diff --git a/phone/yarn.lock b/phone/yarn.lock index c6f7cfdca..8dc4fb772 100644 --- a/phone/yarn.lock +++ b/phone/yarn.lock @@ -9991,10 +9991,10 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" -recoil@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.2.0.tgz#69344b5bec3129272560d8d9d6001ada3ee4d80c" - integrity sha512-VOJfYVQ3VgmfS7L5tV9QdOR+AJhvll8yGr1+3nJPCqADulImuScGZ2sJtejPps3zfTu/o98y5kO4lje8Tx6XHw== +recoil@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.3.1.tgz#40ef544160d19d76e25de8929d7e512eace13b90" + integrity sha512-KNA3DRqgxX4rRC8E7fc6uIw7BACmMPuraIYy+ejhE8tsw7w32CetMm8w7AMZa34wzanKKkev3vl3H7Z4s0QSiA== dependencies: hamt_plus "1.0.2" @@ -11559,10 +11559,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.9.7: - version "3.9.7" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" - integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw== +typescript@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" + integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== unbox-primitive@^1.0.0: version "1.0.0" diff --git a/resources/client/cl_contacts.ts b/resources/client/cl_contacts.ts index ddcb9f8fc..e7daf9dc0 100644 --- a/resources/client/cl_contacts.ts +++ b/resources/client/cl_contacts.ts @@ -1,40 +1,7 @@ -import { ContactEvents, PreDBContact } from '../../typings/contact'; -import { sendContactsEvent } from '../utils/messages'; +import { ContactEvents } from '../../typings/contact'; +import { RegisterNuiProxy } from './cl_utils'; -onNet(ContactEvents.SEND_CONTACTS, (contacts: any) => { - sendContactsEvent(ContactEvents.SEND_CONTACTS, contacts); -}); - -RegisterNuiCallbackType(ContactEvents.ADD_CONTACT); -on(`__cfx_nui:${ContactEvents.ADD_CONTACT}`, (data: PreDBContact, cb: Function) => { - emitNet(ContactEvents.ADD_CONTACT, data); - cb(); -}); - -onNet(ContactEvents.ADD_CONTACT_SUCCESS, () => { - emitNet(ContactEvents.GET_CONTACTS); -}); - -RegisterNuiCallbackType(ContactEvents.UPDATE_CONTACT); -on(`__cfx_nui:${ContactEvents.UPDATE_CONTACT}`, (data: any, cb: Function) => { - emitNet(ContactEvents.UPDATE_CONTACT, data); - cb(); -}); - -onNet(ContactEvents.UPDATE_CONTACT_SUCCESS, () => { - emitNet(ContactEvents.GET_CONTACTS); -}); - -RegisterNuiCallbackType(ContactEvents.DELETE_CONTACT); -on(`__cfx_nui:${ContactEvents.DELETE_CONTACT}`, (data: any, cb: Function) => { - emitNet(ContactEvents.DELETE_CONTACT, data); - cb(); -}); - -onNet(ContactEvents.DELETE_CONTACT_SUCCESS, () => { - emitNet(ContactEvents.GET_CONTACTS); -}); - -onNet(ContactEvents.ACTION_RESULT, (alert: any) => { - sendContactsEvent(ContactEvents.SEND_ALERT, alert); -}); +RegisterNuiProxy(ContactEvents.GET_CONTACTS); +RegisterNuiProxy(ContactEvents.ADD_CONTACT); +RegisterNuiProxy(ContactEvents.DELETE_CONTACT); +RegisterNuiProxy(ContactEvents.UPDATE_CONTACT); diff --git a/resources/client/cl_utils.ts b/resources/client/cl_utils.ts index 71fe136b3..2b39a5b40 100644 --- a/resources/client/cl_utils.ts +++ b/resources/client/cl_utils.ts @@ -12,7 +12,7 @@ interface ISettingsParams { export default class ClientUtils { private _settings: ISettings; private _defaultSettings: ISettings = { - promiseTimeout: 5000, + promiseTimeout: 15000, }; constructor(settings?: ISettingsParams) { diff --git a/resources/server/contacts/contacts.controller.ts b/resources/server/contacts/contacts.controller.ts index 1e46f826e..cab0f7b2d 100644 --- a/resources/server/contacts/contacts.controller.ts +++ b/resources/server/contacts/contacts.controller.ts @@ -1,32 +1,40 @@ -import { Contact, ContactEvents, PreDBContact } from '../../../typings/contact'; -import { getSource } from '../utils/miscUtils'; +import { Contact, ContactDeleteDTO, ContactEvents, PreDBContact } from '../../../typings/contact'; import ContactService from './contacts.service'; import { contactsLogger } from './contacts.utils'; +import { onNetPromise } from '../utils/PromiseNetEvents/onNetPromise'; -onNet(ContactEvents.GET_CONTACTS, async (limit: number) => { - const src = getSource(); - ContactService.handleFetchContact(src, limit).catch((e) => - contactsLogger.error(`Error occured in fetch contacts event (${src}), Error: ${e.message}`), - ); +onNetPromise(ContactEvents.GET_CONTACTS, (reqObj, resp) => { + ContactService.handleFetchContacts(reqObj, resp).catch((e) => { + contactsLogger.error( + `Error occured in fetch contacts event (${reqObj.source}), Error: ${e.message}`, + ); + resp({ status: 'error', errorMsg: 'INTERNAL_ERROR' }); + }); }); -onNet(ContactEvents.ADD_CONTACT, async (contact: PreDBContact) => { - const src = getSource(); - ContactService.handleAddContact(src, contact).catch((e) => - contactsLogger.error(`Error occured in fetch contacts event (${src}), Error: ${e.message}`), - ); +onNetPromise(ContactEvents.ADD_CONTACT, (reqObj, resp) => { + ContactService.handleAddContact(reqObj, resp).catch((e) => { + contactsLogger.error( + `Error occured in fetch contacts event (${reqObj.source}), Error: ${e.message}`, + ); + resp({ status: 'error', errorMsg: 'INTERNAL_ERROR' }); + }); }); -onNet(ContactEvents.UPDATE_CONTACT, async (contact: Contact) => { - const src = getSource(); - ContactService.handleUpdateContact(src, contact).catch((e) => - contactsLogger.error(`Error occured in update contact event (${src}), Error: ${e.message}`), - ); +onNetPromise(ContactEvents.UPDATE_CONTACT, (reqObj, resp) => { + ContactService.handleUpdateContact(reqObj, resp).catch((e) => { + contactsLogger.error( + `Error occured in update contact event (${reqObj.source}), Error: ${e.message}`, + ); + resp({ status: 'error', errorMsg: 'INTERNAL_ERROR' }); + }); }); -onNet(ContactEvents.DELETE_CONTACT, async (contactId: number) => { - const src = getSource(); - ContactService.handleDeleteContact(src, contactId).catch((e) => - contactsLogger.error(`Error occured in delete contact event (${src}), Error: ${e.message}`), - ); +onNetPromise(ContactEvents.DELETE_CONTACT, (reqObj, resp) => { + ContactService.handleDeleteContact(reqObj, resp).catch((e) => { + contactsLogger.error( + `Error occured in delete contact event (${reqObj.source}), Error: ${e.message}`, + ); + resp({ status: 'error', errorMsg: 'INTERNAL_ERROR' }); + }); }); diff --git a/resources/server/contacts/contacts.db.ts b/resources/server/contacts/contacts.db.ts index 6da31a635..175b38efe 100644 --- a/resources/server/contacts/contacts.db.ts +++ b/resources/server/contacts/contacts.db.ts @@ -1,23 +1,29 @@ import { pool } from '../db'; import { Contact, PreDBContact } from '../../../typings/contact'; -import { FetchDefaultLimits } from '../utils/ServerConstants'; +import { ResultSetHeader } from 'mysql2'; export class _ContactsDB { - async fetchAllContacts( - identifier: string, - limit = FetchDefaultLimits.CONTACTS_FETCH_LIMIT, - ): Promise { - const query = - 'SELECT * FROM npwd_phone_contacts WHERE identifier = ? ORDER BY display ASC'; - const [results] = await pool.query(query, [identifier, limit]); + async fetchAllContacts(identifier: string): Promise { + const query = 'SELECT * FROM npwd_phone_contacts WHERE identifier = ? ORDER BY display ASC'; + const [results] = await pool.query(query, [identifier]); return results; } - async addContact(identifier: string, { display, avatar, number }: PreDBContact): Promise { + async addContact( + identifier: string, + { display, avatar, number }: PreDBContact, + ): Promise { const query = 'INSERT INTO npwd_phone_contacts (identifier, number, display, avatar) VALUES (?, ?, ?, ?)'; - await pool.query(query, [identifier, number, display, avatar]); + const [setResult] = await pool.query(query, [identifier, number, display, avatar]); + + return { + id: (setResult).insertId, + number, + avatar, + display, + }; } async updateContact(contact: Contact, identifier: string): Promise { diff --git a/resources/server/contacts/contacts.interfaces.ts b/resources/server/contacts/contacts.interfaces.ts deleted file mode 100644 index 0b41b018c..000000000 --- a/resources/server/contacts/contacts.interfaces.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { FxServerRespError } from '../../../typings/phone'; -import { ContactEvents } from '../../../typings/contact'; - -export interface ContactServerResp { - data?: unknown; - error?: FxServerRespError; - action: ContactEvents; -} diff --git a/resources/server/contacts/contacts.service.ts b/resources/server/contacts/contacts.service.ts index fbc59a0d2..e91a0bc14 100644 --- a/resources/server/contacts/contacts.service.ts +++ b/resources/server/contacts/contacts.service.ts @@ -13,79 +13,57 @@ class _ContactService { contactsLogger.debug('Contacts service started'); } - private responseBuilder(src: number, { data, action, error }: ContactServerResp): void { - const serverRespObj: FxServerResponse = { - app: 'CONTACTS', - error, - status: data ? 'success' : 'failure', - action, - }; - - emitNet(ContactEvents.MAIN_CLIENT_LISTER, src, serverRespObj); - } - - async handleUpdateContact(src: number, contact: Contact): Promise { - const identifier = PlayerService.getIdentifier(src); + async handleUpdateContact( + reqObj: PromiseRequest, + resp: PromiseEventResp, + ): Promise { + const identifier = PlayerService.getIdentifier(reqObj.source); try { - await this.contactsDB.updateContact(contact, identifier); + await this.contactsDB.updateContact(reqObj.data, identifier); - emitNet(ContactEvents.UPDATE_CONTACT_SUCCESS, src); - - emitNet(ContactEvents.ACTION_RESULT, src, { - message: 'CONTACT_UPDATE_SUCCESS', - type: 'success', - }); + resp({ status: 'ok' }); } catch (e) { contactsLogger.error(`Error in handleUpdateContact (${identifier}), ${e.message}`); - emitNet(ContactEvents.ACTION_RESULT, src, { - message: 'CONTACT_UPDATE_FAILED', - type: 'error', - }); + resp({ status: 'error', errorMsg: 'DB_ERROR' }); } } - async handleDeleteContact(src: number, contactId: number): Promise { - const identifier = PlayerService.getIdentifier(src); + async handleDeleteContact( + reqObj: PromiseRequest, + resp: PromiseEventResp, + ): Promise { + const identifier = PlayerService.getIdentifier(reqObj.source); try { - await this.contactsDB.deleteContact(contactId, identifier); - emitNet(ContactEvents.DELETE_CONTACT_SUCCESS, src); - emitNet(ContactEvents.ACTION_RESULT, src, { - message: 'CONTACT_DELETE_SUCCESS', - type: 'success', - }); + await this.contactsDB.deleteContact(reqObj.data.id, identifier); + resp({ status: 'ok' }); } catch (e) { - emitNet(ContactEvents.ACTION_RESULT, src, { - message: 'CONTACT_DELETE_FAILED', - type: 'error', - }); + resp({ status: 'error', errorMsg: 'DB_ERROR' }); contactsLogger.error(`Error in handleDeleteContact (${identifier}), ${e.message}`); } } - async handleAddContact(src: number, contact: PreDBContact): Promise { - const identifier = PlayerService.getIdentifier(src); + async handleAddContact( + reqObj: PromiseRequest, + resp: PromiseEventResp, + ): Promise { + const identifier = PlayerService.getIdentifier(reqObj.source); try { - await this.contactsDB.addContact(identifier, contact); + const contact = await this.contactsDB.addContact(identifier, reqObj.data); - emitNet(ContactEvents.ADD_CONTACT_SUCCESS, src); - emitNet(ContactEvents.ACTION_RESULT, src, { - message: 'CONTACT_ADD_SUCCESS', - type: 'success', - }); + resp({ status: 'ok', data: contact }); } catch (e) { contactsLogger.error(`Error in handleAddContact, ${e.message}`); - emitNet(ContactEvents.ACTION_RESULT, src, { - message: 'CONTACT_ADD_FAILED', - type: 'error', - }); + resp({ status: 'error', errorMsg: 'DB_ERROR' }); } } - async handleFetchContact(src: number, limit?: number): Promise { - const identifier = PlayerService.getIdentifier(src); - + async handleFetchContacts( + reqObj: PromiseRequest, + resp: PromiseEventResp, + ): Promise { + const identifier = PlayerService.getIdentifier(reqObj.source); try { - const contacts = await this.contactsDB.fetchAllContacts(identifier, limit); - - emitNet(ContactEvents.SEND_CONTACTS, src, contacts); + const contacts = await this.contactsDB.fetchAllContacts(identifier); + resp({ status: 'ok', data: contacts }); } catch (e) { + resp({ status: 'error', errorMsg: 'DB_ERROR' }); contactsLogger.error(`Error in handleFetchContact (${identifier}), ${e.message}`); } } diff --git a/typings/contact.ts b/typings/contact.ts index 42ab22e61..94a3a5e20 100644 --- a/typings/contact.ts +++ b/typings/contact.ts @@ -21,6 +21,10 @@ export interface Contact extends PreDBContact { id: number; } +export interface ContactDeleteDTO { + id: number; +} + export enum ContactEvents { SEND_CONTACTS = 'npwd:sendContacts', GET_CONTACTS = 'npwd:getContacts',