diff --git a/src/components/icons-with-counter/IconsWithCounter.tsx b/src/components/icons-with-counter/IconsWithCounter.tsx index 5ed7843633..25ac653440 100644 --- a/src/components/icons-with-counter/IconsWithCounter.tsx +++ b/src/components/icons-with-counter/IconsWithCounter.tsx @@ -1,4 +1,4 @@ -import { useState, FC } from 'react' +import { useState, FC, useEffect } from 'react' import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' @@ -11,20 +11,41 @@ import { styles } from '~/components/icons-with-counter/IconsWithCounter.style' interface IconsWithCounterProps { maxValue: number + onFilteredIndexChange: (index: number) => void } -const IconsWithCounter: FC = ({ maxValue }) => { +const IconsWithCounter: FC = ({ + maxValue, + onFilteredIndexChange +}) => { const [possibleValue, setPossibleValue] = useState(0) + const { t } = useTranslation() + useEffect(() => { + if (!maxValue) { + setPossibleValue(0) + } + + onFilteredIndexChange(possibleValue) + }, [onFilteredIndexChange, maxValue, possibleValue]) + const handleIncrement = () => { - setPossibleValue((prev) => (prev % maxValue) + 1) + if (maxValue !== 0) { + setPossibleValue((prev) => { + const newValue = (prev + 1) % maxValue + return newValue + }) + } } const handleDecrement = () => { - possibleValue > 1 - ? setPossibleValue((prev) => prev - 1) - : setPossibleValue(maxValue) + if (maxValue !== 0) { + setPossibleValue((prev) => { + const newValue = prev > 0 ? prev - 1 : maxValue - 1 + return newValue + }) + } } return ( @@ -33,7 +54,9 @@ const IconsWithCounter: FC = ({ maxValue }) => { - {possibleValue} {t('common.of')} {maxValue} + {maxValue + ? `${possibleValue + 1} ${t('common.of')} ${maxValue}` + : `${possibleValue} ${t('common.of')} ${maxValue}`} diff --git a/src/components/message/Message.styles.ts b/src/components/message/Message.styles.ts index b02ed0066b..5a8755b8f3 100644 --- a/src/components/message/Message.styles.ts +++ b/src/components/message/Message.styles.ts @@ -13,6 +13,15 @@ export const styles = { height: '44px', '&:hover': { transform: 'scale(1.1)' } }, + + findMessageCard: { + backgroundColor: 'basic.turquoiseChat', + borderRadius: '10px', + display: 'inline', + color: 'primary.900', + typography: TypographyVariantEnum.Body1, + p: '8px 16px' + }, messageCard: (isMyMessage: boolean) => ({ boxSizing: 'border-box', maxWidth: '520px', @@ -24,9 +33,11 @@ export const styles = { typography: TypographyVariantEnum.Body1, p: '8px 16px' }), - date: (isMyMessage: boolean) => ({ + date: (isMyMessage: boolean, isTextFiltered: boolean) => ({ typography: TypographyVariantEnum.Caption, - color: `primary.${isMyMessage ? 100 : 500}`, + color: isTextFiltered + ? 'primary.500' + : `primary.${isMyMessage ? 100 : 500}`, float: 'right', userSelect: 'none', m: '4px 0 0 8px' diff --git a/src/components/message/Message.tsx b/src/components/message/Message.tsx index 6e1d213a0d..868c3c5fcb 100644 --- a/src/components/message/Message.tsx +++ b/src/components/message/Message.tsx @@ -1,4 +1,4 @@ -import { FC, MouseEvent } from 'react' +import { FC, MouseEvent, useRef, useEffect } from 'react' import { Link } from 'react-router-dom' import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' @@ -23,18 +23,24 @@ interface MessageProps { sx?: { avatar?: SxProps } + filteredMessages?: string[] + filteredIndex: number } -const Message: FC = ({ message, prevMessage, sx = {} }) => { +const Message: FC = ({ + message, + prevMessage, + sx = {}, + filteredMessages = [], + filteredIndex +}) => { const { userId: myId } = useAppSelector((state) => state.appMain) - const { author, text, authorRole, createdAt } = message const { _id, photo } = author const { path } = authRoutes.userProfile const isMyMessage = myId === _id const isSameAuthor = prevMessage?.author._id === _id const pathToProfile = createUrlPath(path, _id, { authorRole }) - const timeDifference = prevMessage ? new Date(createdAt).getTime() - new Date(prevMessage.createdAt).getTime() : Infinity @@ -46,11 +52,27 @@ const Message: FC = ({ message, prevMessage, sx = {} }) => { e.stopPropagation() } + const messageRef = useRef(null) + const isTextFiltered = filteredMessages.some( + (filteredMessage) => filteredMessage.toLowerCase() === text.toLowerCase() + ) + useEffect(() => { + if (filteredIndex >= 0 && filteredIndex < filteredMessages.length) { + const messageToScrollTo = filteredMessages[filteredIndex] + + if (messageToScrollTo === text) { + messageRef.current?.scrollIntoView({ + behavior: 'smooth', + block: 'center' + }) + } + } + }, [filteredIndex, filteredMessages, text]) + const date = getFormattedDate({ date: createdAt, options: { hour: '2-digit', minute: '2-digit' } }) - const avatar = !isMyMessage && isAvatarVisible && ( = ({ message, prevMessage, sx = {} }) => { /> ) - return ( - + {avatar} - + {text} - {date} + + {date} + ) diff --git a/src/components/search-by-message/SearchByMessage.styles.ts b/src/components/search-by-message/SearchByMessage.styles.ts index 88c2333abf..ea0c1ab0ac 100644 --- a/src/components/search-by-message/SearchByMessage.styles.ts +++ b/src/components/search-by-message/SearchByMessage.styles.ts @@ -6,7 +6,7 @@ export const styles = { justifyContent: 'space-between', py: '8px', backgroundColor: 'basic.white', - borderRadius: ' 0 0 6px 6px' + borderRadius: ' 0 0 12px 12px' }, input: { width: '90%' diff --git a/src/components/search-by-message/SearchByMessage.tsx b/src/components/search-by-message/SearchByMessage.tsx index 136f25a820..6cd029cc77 100644 --- a/src/components/search-by-message/SearchByMessage.tsx +++ b/src/components/search-by-message/SearchByMessage.tsx @@ -1,34 +1,69 @@ -import { useState, ChangeEvent, FC } from 'react' +import { useState, ChangeEvent, useEffect, FC } from 'react' import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' import InputWithIcon from '~/components/input-with-icon/InputWithIcon' import IconsWithCounter from '~/components/icons-with-counter/IconsWithCounter' +import { useDebounce } from '~/hooks/use-debounce' import { styles } from '~/components/search-by-message/SearchByMessage.styles' interface SearchByMessageProps { - maxValue: number + messages: { text: string }[] + onFilteredMessagesChange: (filteredMessages: string[]) => void + onFilteredIndexChange: (filteredIndex: number) => void + isCloseSearch: () => void } -const SearchByMessage: FC = ({ maxValue = 10 }) => { +const SearchByMessage: FC = ({ + messages, + onFilteredMessagesChange, + onFilteredIndexChange, + isCloseSearch +}) => { const { t } = useTranslation() const [search, setSearch] = useState('') + const [findMessage, setFindMessage] = useState([]) + const debouncedOnFilteredMessagesChange = useDebounce( + (filteredMessages: string[]) => { + onFilteredMessagesChange(filteredMessages) + }, + 500 + ) const onChange = (event: ChangeEvent) => { setSearch(event.target.value) } + useEffect(() => { + if (search) { + const filtered = messages.filter((message) => + message.text.toLowerCase().includes(search.toLowerCase()) + ) + const filteredMessages = filtered.map((item) => item.text) + + debouncedOnFilteredMessagesChange(filteredMessages) + setFindMessage(filteredMessages) + } else { + onFilteredMessagesChange([]) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [search, messages]) - const onClear = () => { + const onClose = () => { setSearch('') + isCloseSearch() + onFilteredMessagesChange([]) } - return ( - - + e.stopPropagation()} sx={styles.container}> + void onMenuClick: (e: MouseEvent) => void user: Pick + messages: { text: string }[] + onFilteredMessagesChange: (filteredMessages: string[]) => void + onFilteredIndexChange: (filteredIndex: number) => void } -const ChatHeader: FC = ({ onClick, onMenuClick, user }) => { +const ChatHeader: FC = ({ + onClick, + user, + onMenuClick, + messages, + onFilteredMessagesChange, + onFilteredIndexChange +}) => { + const [isSearchOpen, setIsSearchOpen] = useState(false) const { t } = useTranslation() const { isMobile } = useBreakpoints() const handleOnClick = (e: MouseEvent) => { e.stopPropagation() } + const handleSearch = (e: MouseEvent) => { + e.stopPropagation() + setIsSearchOpen(!isSearchOpen) + } const iconButtons = [ - { _id: 1, icon: , handleOnClick }, + { _id: 1, icon: , handleOnClick: handleSearch }, { _id: 2, icon: , handleOnClick } ] @@ -39,6 +55,10 @@ const ChatHeader: FC = ({ onClick, onMenuClick, user }) => { )) + const closeSearch = () => { + setIsSearchOpen(false) + } + const status = ( <> @@ -59,6 +79,16 @@ const ChatHeader: FC = ({ onClick, onMenuClick, user }) => { title={`${user.firstName} ${user.lastName}`} /> {icons} + {isSearchOpen && ( + + + + )} ) } diff --git a/src/pages/chat/Chat.tsx b/src/pages/chat/Chat.tsx index dc3bb8e5b0..40e3c4d7b6 100644 --- a/src/pages/chat/Chat.tsx +++ b/src/pages/chat/Chat.tsx @@ -46,6 +46,8 @@ const Chat = () => { const [selectedChat, setSelectedChat] = useState(null) const [messages, setMessages] = useState([]) const [textAreaValue, setTextAreaValue] = useState('') + const [filteredMessages, setFilteredMessages] = useState([]) + const [filteredIndex, setFilteredIndex] = useState(0) const scrollRef = useRef(null) const groupedMessages = getGroupedByDate(messages, getIsNewDay) @@ -124,6 +126,8 @@ const Chat = () => { {group.items.map((item, index) => ( { ) + const handleFilteredMessage = (filteredMessages: string[]) => { + setFilteredMessages(filteredMessages.reverse()) + } + + const hadleIndexMessage = (filteredIndex: number) => { + setFilteredIndex(filteredIndex) + } + return ( {isMobile && ( @@ -197,7 +209,10 @@ const Chat = () => { ) : ( <> onSidebarHandler(true)} + onFilteredIndexChange={hadleIndexMessage} + onFilteredMessagesChange={handleFilteredMessage} onMenuClick={openChatsHandler} user={selectedChat.members[0].user} /> diff --git a/src/styles/app-theme/app.pallete.ts b/src/styles/app-theme/app.pallete.ts index f44b327bb1..4c9fd639ee 100644 --- a/src/styles/app-theme/app.pallete.ts +++ b/src/styles/app-theme/app.pallete.ts @@ -18,7 +18,8 @@ const palette = { orientalHerbs: '#12A03A', lime: '#99CC00', turquoise: '#489DA0', - turquoiseDark: '#3B8587' + turquoiseDark: '#3B8587', + turquoiseChat: '#A0F0F2' }, companyBlue: 'rgba(0, 167, 167, 0.2)', error: { diff --git a/tests/unit/components/icons-with-counter/IconsWithCounter.spec.jsx b/tests/unit/components/icons-with-counter/IconsWithCounter.spec.jsx index 0a8272e0a8..f78c57247a 100644 --- a/tests/unit/components/icons-with-counter/IconsWithCounter.spec.jsx +++ b/tests/unit/components/icons-with-counter/IconsWithCounter.spec.jsx @@ -1,18 +1,26 @@ +import { vi } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react' import IconsWithCounter from '~/components/icons-with-counter/IconsWithCounter' describe('IconWithCounter test', () => { const testValue = 10 + const mockOnFilteredIndexChange = vi.fn() beforeEach(() => { - render() + render( + + ) }) it('should increment data', () => { const buttonIncrement = screen.getByTestId('IconUp') fireEvent.click(buttonIncrement) - expect(screen.getByText('1 common.of 10')).toBeTruthy() + expect(screen.getByText('2 common.of 10')).toBeTruthy() + expect(mockOnFilteredIndexChange).toHaveBeenCalledWith(1) }) it('should decrement data', () => { diff --git a/tests/unit/components/message/Message.spec.jsx b/tests/unit/components/message/Message.spec.jsx index db190e9883..4988126d34 100644 --- a/tests/unit/components/message/Message.spec.jsx +++ b/tests/unit/components/message/Message.spec.jsx @@ -7,10 +7,16 @@ import { getFormattedDate } from '~/utils/helper-functions' import { messagesMock } from '~tests/unit/containers/chat/list-of-users-with-search/MockChat.spec.constants' const messageMock = messagesMock[0] - +const filteredMessageMock = ['Some text'] describe('Message component', () => { beforeEach(() => { - renderWithProviders() + renderWithProviders( + + ) }) it('should render the author name and message content', () => { diff --git a/tests/unit/components/search-by-message/SearchByMessage.spec.jsx b/tests/unit/components/search-by-message/SearchByMessage.spec.jsx index 3eafca8e67..600a117842 100644 --- a/tests/unit/components/search-by-message/SearchByMessage.spec.jsx +++ b/tests/unit/components/search-by-message/SearchByMessage.spec.jsx @@ -1,11 +1,50 @@ +import { vi } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react' import SearchByMessage from '~/components/search-by-message/SearchByMessage' describe('SearchByMessage', () => { + const onFilteredMessagesChange = vi.fn() + const onFilteredIndexChange = vi.fn() + const isCloseSearch = vi.fn() + const mockMessage = [ + { + _id: '64ee0a2f6ae3b95ececb05b5', + author: { + _id: '6421d9833cdf38b706756dff' + }, + authorRole: 'student', + text: 'Hello from there!', + isRead: false, + chat: '64a543b5afb24d9c76bfdef1', + createdAt: '2023-07-03T08:55:53.812Z', + updatedAt: '2023-07-03T08:55:53.812Z' + }, + { + _id: '64ee0de96ae3b95ececb05bb', + author: { + _id: '6494128829631adbaf5cf615', + photo: '1687425744398-ITA wallpapers-19.png' + }, + authorRole: 'tutor', + text: 'Hello from tutor!', + isRead: false, + chat: '64a543b5afb24d9c76bfdef1', + createdAt: '2023-07-03T08:55:53.812Z', + updatedAt: '2023-07-03T08:55:53.812Z' + } + ] + const testValue = 'test data' beforeEach(() => { - render() + render( + + ) }) it('should change and clear input value', () => {