diff --git a/package.json b/package.json index 748cf32d41..cc1e769e3a 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.8.3", "@mui/material": "^5.14.1", + "@mui/private-theming": "^5.14.13", + "@mui/system": "^5.14.12", "@mui/x-charts": "^6.0.0-alpha.13", "@mui/x-data-grid": "^6.8.0", "@mui/x-date-pickers": "^6.6.0", diff --git a/public/locales/en.json b/public/locales/en.json index eac995857e..ba7670d2a7 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -631,5 +631,15 @@ "EXlink": "Ex. http://yourwebsite.com/photo", "register": "Create Advertisement", "close": "Close " + }, + "userChat": { + "chat": "Chat", + "search": "Search", + "contacts": "Contacts" + }, + "userChatRoom": { + "selectContact": "Select a contact to start conversation", + "sendMessage": "Send Message" + } } diff --git a/public/locales/fr.json b/public/locales/fr.json index e27461dceb..60756082f7 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -612,5 +612,14 @@ "archievedAds": "Campagnes terminées", "pMessage": "Aucune publicité n'est présente pour cette campagne.", "delete": "Supprimer" + }, + "userChat": { + "chat": "Chat", + "search": "Recherche", + "contacts": "Contacts" + }, + "userChatRoom": { + "selectContact": "Sélectionnez un contact pour démarrer la conversation", + "sendMessage": "Envoyer le message" } } diff --git a/public/locales/hi.json b/public/locales/hi.json index 4f4b0a8a99..63037b86f8 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -613,5 +613,14 @@ "archievedAds": "संपन्न अभियान", "pMessage": "इस अभियान के लिए कोई विज्ञापन नहीं हैं।", "delete": "हटाएँ" + }, + "userChat": { + "chat": "बात", + "search": "खोज", + "contacts": "संपर्क" + }, + "userChatRoom": { + "selectContact": "बातचीत शुरू करने के लिए एक संपर्क चुनें", + "sendMessage": "मेसेज भेजें" } } diff --git a/public/locales/sp.json b/public/locales/sp.json index 2b4c4093f0..04a932094a 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -613,5 +613,14 @@ "archievedAds": "Campañas completadas", "pMessage": "No hay anuncios disponibles para esta campaña.", "delete": "Eliminar" + }, + "userChat": { + "chat": "Charlar", + "search": "Buscar", + "contacts": "Contactos" + }, + "userChatRoom": { + "selectContact": "Seleccione un contacto para iniciar una conversación", + "sendMessage": "Enviar mensaje" } } diff --git a/public/locales/zh.json b/public/locales/zh.json index bf461a1956..5506b5a7bf 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -613,5 +613,13 @@ "archievedAds": "已完成的广告活动", "pMessage": "此广告活动没有相关广告。", "delete": "删除" + }, + "chat": "聊天", + "search": "搜尋", + "contacts": "聯絡方式" + }, + "userChatRoom": { + "selectContact": "選擇聯絡人開始對話", + "sendMessage": "傳訊息" } } diff --git a/src/App.tsx b/src/App.tsx index 4752f65982..1c69256c32 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,6 +33,7 @@ import Donate from 'screens/UserPortal/Donate/Donate'; import Events from 'screens/UserPortal/Events/Events'; import Tasks from 'screens/UserPortal/Tasks/Tasks'; import Advertisements from 'components/Advertisements/Advertisements'; +import Chat from 'screens/UserPortal/Chat/Chat'; function app(): JSX.Element { /*const { updatePluginLinks, updateInstalled } = bindActionCreators( @@ -129,6 +130,7 @@ function app(): JSX.Element { + diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts index 6421736071..32ffddc9d5 100644 --- a/src/GraphQl/Mutations/mutations.ts +++ b/src/GraphQl/Mutations/mutations.ts @@ -643,6 +643,17 @@ export const UNLIKE_COMMENT = gql` } } `; + +export const CREATE_DIRECT_CHAT = gql` + mutation createDirectChat($userIds: [ID!]!, $organizationId: ID!) { + createDirectChat( + data: { userIds: $userIds, organizationId: $organizationId } + ) { + _id + } + } +`; + //Plugin WebSocket listner export const PLUGIN_SUBSCRIPTION = gql` subscription onPluginUpdate { diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index 5469f45b0f..d758b6c6be 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -726,3 +726,45 @@ export const USER_TASKS_LIST = gql` } } `; + +export const DIRECT_CHATS_LIST = gql` + query DirectChatsByUserID($id: ID!) { + directChatsByUserID(id: $id) { + _id + creator { + _id + firstName + lastName + email + } + messages { + _id + createdAt + messageContent + receiver { + _id + firstName + lastName + email + } + sender { + _id + firstName + lastName + email + } + } + organization { + _id + name + } + users { + _id + firstName + lastName + email + image + } + } + } +`; diff --git a/src/components/EventStats/Statistics/AverageRating.test.tsx b/src/components/EventStats/Statistics/AverageRating.test.tsx index 79328965f6..53f0acacba 100644 --- a/src/components/EventStats/Statistics/AverageRating.test.tsx +++ b/src/components/EventStats/Statistics/AverageRating.test.tsx @@ -52,7 +52,7 @@ describe('Testing Average Rating Card', () => { ); await waitFor(() => - expect(queryByText('Rated 5.00 / 10')).toBeInTheDocument() + expect(queryByText('Rated 5.00 / 5')).toBeInTheDocument() ); }); }); diff --git a/src/components/EventStats/Statistics/AverageRating.tsx b/src/components/EventStats/Statistics/AverageRating.tsx index dac7b208df..a3769934d8 100644 --- a/src/components/EventStats/Statistics/AverageRating.tsx +++ b/src/components/EventStats/Statistics/AverageRating.tsx @@ -10,7 +10,7 @@ type ModalPropType = { data: { event: { _id: string; - averageFeedbackScore: number | null; + averageFeedbackScore: number; feedback: FeedbackType[]; }; }; @@ -41,12 +41,12 @@ export const AverageRating = ({ data }: ModalPropType): JSX.Element => {

Average Review Score

- Rated {(data.event.averageFeedbackScore || 0).toFixed(2)} / 10 + Rated {data.event.averageFeedbackScore.toFixed(2)} / 5 } diff --git a/src/components/EventStats/Statistics/Feedback.tsx b/src/components/EventStats/Statistics/Feedback.tsx index 6a78e29884..ff4fe7a647 100644 --- a/src/components/EventStats/Statistics/Feedback.tsx +++ b/src/components/EventStats/Statistics/Feedback.tsx @@ -25,15 +25,10 @@ type FeedbackType = { export const FeedbackStats = ({ data }: ModalPropType): JSX.Element => { const ratingColors = [ '#57bb8a', // Green - '#73b87e', '#94bd77', - '#b0be6e', '#d4c86a', - '#f5ce62', '#e9b861', - '#ecac67', '#e79a69', - '#e2886c', '#dd776e', // Red ]; @@ -45,13 +40,13 @@ export const FeedbackStats = ({ data }: ModalPropType): JSX.Element => { }); const chartData = []; - for (let rating = 0; rating <= 10; rating++) { + for (let rating = 0; rating <= 5; rating++) { if (rating in count) chartData.push({ id: rating, value: count[rating], label: `${rating} (${count[rating]})`, - color: ratingColors[10 - rating], + color: ratingColors[5 - rating], }); } diff --git a/src/components/EventStats/Statistics/Review.tsx b/src/components/EventStats/Statistics/Review.tsx index 0528de79d3..b5aecd61de 100644 --- a/src/components/EventStats/Statistics/Review.tsx +++ b/src/components/EventStats/Statistics/Review.tsx @@ -42,7 +42,7 @@ export const ReviewStats = ({ data }: ModalPropType): JSX.Element => { reviews.map((review) => (
- +

{review.review}

diff --git a/src/components/UserPortal/ChatRoom/ChatRoom.module.css b/src/components/UserPortal/ChatRoom/ChatRoom.module.css new file mode 100644 index 0000000000..592004d523 --- /dev/null +++ b/src/components/UserPortal/ChatRoom/ChatRoom.module.css @@ -0,0 +1,13 @@ +.chatAreaContainer { + padding: 10px; + flex-grow: 1; + background-color: rgba(196, 255, 211, 0.3); +} + +.backgroundWhite { + background-color: white; +} + +.grey { + color: grey; +} diff --git a/src/components/UserPortal/ChatRoom/ChatRoom.tsx b/src/components/UserPortal/ChatRoom/ChatRoom.tsx new file mode 100644 index 0000000000..c7ada20a13 --- /dev/null +++ b/src/components/UserPortal/ChatRoom/ChatRoom.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import type { ChangeEvent } from 'react'; +import { Paper } from '@mui/material'; +import SendIcon from '@mui/icons-material/Send'; +import { Button, Form, InputGroup } from 'react-bootstrap'; +import styles from './ChatRoom.module.css'; +import PermContactCalendarIcon from '@mui/icons-material/PermContactCalendar'; +import { useTranslation } from 'react-i18next'; + +interface InterfaceChatRoomProps { + selectedContact: string; +} + +export default function chatRoom(props: InterfaceChatRoomProps): JSX.Element { + const { t } = useTranslation('translation', { + keyPrefix: 'userChatRoom', + }); + + const [newMessage, setNewMessage] = React.useState(''); + + const handleNewMessageChange = (e: ChangeEvent): void => { + const newMessageValue = e.target.value; + + setNewMessage(newMessageValue); + }; + + return ( +
+ {!props.selectedContact ? ( +
+ +
{t('selectContact')}
+
+ ) : ( + <> +
+ + My message + + + Other message + +
+
+ + + + +
+ + )} +
+ ); +} diff --git a/src/components/UserPortal/ContactCard/ContactCard.module.css b/src/components/UserPortal/ContactCard/ContactCard.module.css new file mode 100644 index 0000000000..d722a3a302 --- /dev/null +++ b/src/components/UserPortal/ContactCard/ContactCard.module.css @@ -0,0 +1,33 @@ +.contact { + display: flex; + flex-direction: row; + padding: 10px 10px; + cursor: pointer; + border-radius: 10px; + margin-bottom: 10px; + border: 2px solid #f5f5f5; +} + +.contactImage { + width: 50px; + height: auto; + border-radius: 10px; +} + +.contactNameContainer { + display: flex; + flex-direction: column; + padding: 0px 10px; +} + +.grey { + color: grey; +} + +.bgGrey { + background-color: #f5f5f5; +} + +.bgWhite { + background-color: white; +} diff --git a/src/components/UserPortal/ContactCard/ContactCard.test.tsx b/src/components/UserPortal/ContactCard/ContactCard.test.tsx new file mode 100644 index 0000000000..cb7aaec882 --- /dev/null +++ b/src/components/UserPortal/ContactCard/ContactCard.test.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import { MockedProvider } from '@apollo/react-testing'; +import { I18nextProvider } from 'react-i18next'; + +import { BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import ContactCard from './ContactCard'; +import userEvent from '@testing-library/user-event'; + +const link = new StaticMockLink([], true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +let props = { + id: '1', + firstName: 'Noble', + lastName: 'Mittal', + email: 'noble@mittal.com', + image: '', + selectedContact: '', + setSelectedContact: jest.fn(), + setSelectedContactName: jest.fn(), +}; + +describe('Testing ContactCard Component [User Portal]', () => { + test('Component should be rendered properly if person image is undefined', async () => { + render( + + + + + + + + + + ); + + await wait(); + }); + + test('Component should be rendered properly if person image is not undefined', async () => { + props = { + ...props, + image: 'personImage', + }; + + render( + + + + + + + + + + ); + + await wait(); + }); + + test('Contact gets selectected when component is clicked', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('contactContainer')); + + await wait(); + }); + + test('Component is rendered with background color grey if the contact is selected', async () => { + props = { + ...props, + selectedContact: '1', + }; + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('contactContainer')); + + await wait(); + }); +}); diff --git a/src/components/UserPortal/ContactCard/ContactCard.tsx b/src/components/UserPortal/ContactCard/ContactCard.tsx new file mode 100644 index 0000000000..8dd5352b43 --- /dev/null +++ b/src/components/UserPortal/ContactCard/ContactCard.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import styles from './ContactCard.module.css'; + +interface InterfaceContactCardProps { + id: string; + firstName: string; + lastName: string; + email: string; + image: string; + selectedContact: string; + setSelectedContact: React.Dispatch>; + setSelectedContactName: React.Dispatch>; +} + +function contactCard(props: InterfaceContactCardProps): JSX.Element { + const contactName = `${props.firstName} ${props.lastName}`; + const imageUrl = props.image + ? props.image + : `https://api.dicebear.com/5.x/initials/svg?seed=${contactName}`; + + const handleSelectedContactChange = (): void => { + props.setSelectedContact(props.id); + props.setSelectedContactName(contactName); + }; + + const [isSelected, setIsSelected] = React.useState( + props.selectedContact === props.id + ); + + React.useEffect(() => { + setIsSelected(props.selectedContact === props.id); + }, [props.selectedContact]); + + return ( + <> +
+ {contactName} +
+ {contactName} + {props.email} +
+
+ + ); +} + +export default contactCard; diff --git a/src/screens/UserPortal/Chat/Chat.module.css b/src/screens/UserPortal/Chat/Chat.module.css new file mode 100644 index 0000000000..40add650f4 --- /dev/null +++ b/src/screens/UserPortal/Chat/Chat.module.css @@ -0,0 +1,63 @@ +.containerHeight { + height: calc(100vh - 66px); +} + +.mainContainer { + width: 50%; + flex-grow: 3; + padding: 20px; + max-height: 100%; + overflow: auto; + display: flex; + flex-direction: row; +} + +.chatContainer { + flex-grow: 4; + display: flex; + flex-direction: column; + background-color: white; + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; +} + +.contactContainer { + flex-grow: 1; + display: flex; + flex-direction: column; + background-color: white; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} + +.colorLight { + background-color: #f5f5f5; +} + +.addChatContainer { + gap: 5px; + padding: 10px; +} + +.contactListContainer { + flex-grow: 1; + padding: 15px 10px; +} + +.chatHeadingContainer { + padding: 10px; + color: white; + border-radius: 0px 10px 0px 0px; +} + +.borderNone { + border: none; +} + +.colorWhite { + color: white; +} + +.colorPrimary { + background: #31bb6b; +} diff --git a/src/screens/UserPortal/Chat/Chat.test.tsx b/src/screens/UserPortal/Chat/Chat.test.tsx new file mode 100644 index 0000000000..6476e9ee40 --- /dev/null +++ b/src/screens/UserPortal/Chat/Chat.test.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import { MockedProvider } from '@apollo/react-testing'; +import { I18nextProvider } from 'react-i18next'; + +import { ORGANIZATIONS_MEMBER_CONNECTION_LIST } from 'GraphQl/Queries/Queries'; +import { BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import Chat from './Chat'; +import * as getOrganizationId from 'utils/getOrganizationId'; +import userEvent from '@testing-library/user-event'; + +const MOCKS = [ + { + request: { + query: ORGANIZATIONS_MEMBER_CONNECTION_LIST, + variables: { + orgId: '', + firstName_contains: '', + }, + }, + result: { + data: { + organizationsMemberConnection: { + edges: [ + { + _id: '64001660a711c62d5b4076a2', + firstName: 'Noble', + lastName: 'Mittal', + image: null, + email: 'noble1@gmail.com', + createdAt: '2023-03-02T03:22:08.101Z', + }, + { + _id: '64001660a711c62d5b4076a3', + firstName: 'Noble', + lastName: 'Mittal', + image: 'mockImage', + email: 'noble@gmail.com', + createdAt: '2023-03-02T03:22:08.101Z', + }, + ], + }, + }, + }, + }, + { + request: { + query: ORGANIZATIONS_MEMBER_CONNECTION_LIST, + variables: { + orgId: '', + firstName_contains: 'j', + }, + }, + result: { + data: { + organizationsMemberConnection: { + edges: [ + { + _id: '64001660a711c62d5b4076a2', + firstName: 'John', + lastName: 'Cena', + image: null, + email: 'john@gmail.com', + createdAt: '2023-03-02T03:22:08.101Z', + }, + ], + }, + }, + }, + }, +]; + +const link = new StaticMockLink(MOCKS, true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +describe('Testing People Screen [User Portal]', () => { + jest.mock('utils/getOrganizationId'); + + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + + const getOrganizationIdSpy = jest + .spyOn(getOrganizationId, 'default') + .mockImplementation(() => { + return ''; + }); + + test('Screen should be rendered properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + + expect(getOrganizationIdSpy).toHaveBeenCalled(); + expect(screen.queryAllByText('Noble Mittal')).not.toBe([]); + }); + + test('User is able to select a contact', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByText('noble1@gmail.com')); + await wait(); + + expect(getOrganizationIdSpy).toHaveBeenCalled(); + expect(screen.queryAllByText('Noble Mittal')).not.toBe([]); + }); + + test('Search functionality works as expected', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type(screen.getByTestId('searchInput'), 'j'); + await wait(); + + expect(getOrganizationIdSpy).toHaveBeenCalled(); + expect(screen.queryByText('John Cena')).toBeInTheDocument(); + expect(screen.queryByText('Noble Mittal')).not.toBeInTheDocument(); + }); +}); diff --git a/src/screens/UserPortal/Chat/Chat.tsx b/src/screens/UserPortal/Chat/Chat.tsx new file mode 100644 index 0000000000..1ae9d5764d --- /dev/null +++ b/src/screens/UserPortal/Chat/Chat.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import OrganizationNavbar from 'components/UserPortal/OrganizationNavbar/OrganizationNavbar'; +import UserSidebar from 'components/UserPortal/UserSidebar/UserSidebar'; +import { ORGANIZATIONS_MEMBER_CONNECTION_LIST } from 'GraphQl/Queries/Queries'; +import { useQuery } from '@apollo/client'; +import styles from './Chat.module.css'; +import getOrganizationId from 'utils/getOrganizationId'; +import { useTranslation } from 'react-i18next'; +import { Form, InputGroup } from 'react-bootstrap'; +import { SearchOutlined } from '@mui/icons-material'; +import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; +import ContactCard from 'components/UserPortal/ContactCard/ContactCard'; +import ChatRoom from 'components/UserPortal/ChatRoom/ChatRoom'; + +interface InterfaceContactCardProps { + id: string; + firstName: string; + lastName: string; + email: string; + image: string; + selectedContact: string; + setSelectedContact: React.Dispatch>; + setSelectedContactName: React.Dispatch>; +} + +interface InterfaceChatRoomProps { + selectedContact: string; +} + +export default function chat(): JSX.Element { + const { t } = useTranslation('translation', { + keyPrefix: 'userChat', + }); + const organizationId = getOrganizationId(location.href); + + const [selectedContact, setSelectedContact] = React.useState(''); + const [selectedContactName, setSelectedContactName] = React.useState(''); + const [contacts, setContacts] = React.useState([]); + const [filterName, setFilterName] = React.useState(''); + + const navbarProps = { + currentPage: 'chat', + }; + + const chatRoomProps: InterfaceChatRoomProps = { + selectedContact, + }; + + const { + data: contactData, + loading: contactLoading, + refetch: contactRefetch, + } = useQuery(ORGANIZATIONS_MEMBER_CONNECTION_LIST, { + variables: { + orgId: organizationId, + firstName_contains: filterName, + }, + }); + + const handleSearch = ( + event: React.ChangeEvent + ): void => { + const newFilter = event.target.value; + setFilterName(newFilter); + + const filter = { + firstName_contains: newFilter, + }; + + contactRefetch(filter); + }; + + React.useEffect(() => { + if (contactData) { + setContacts(contactData.organizationsMemberConnection.edges); + } + }, [contactData]); + + return ( + <> + +
+ +
+
+
+

+ {t('contacts')} +

+ + + + + + +
+
+ {contactLoading ? ( +
+ Loading... +
+ ) : ( + contacts.map((contact: any, index: number) => { + const cardProps: InterfaceContactCardProps = { + id: contact._id, + firstName: contact.firstName, + lastName: contact.lastName, + email: contact.email, + image: contact.image, + setSelectedContactName, + selectedContact, + setSelectedContact, + }; + return ; + }) + )} +
+
+
+
+ {selectedContact ? selectedContactName : t('chat')} +
+ +
+
+
+ + ); +}