Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced chat search #2628

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions src/components/search-by-message/SearchByMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { useState, ChangeEvent, useEffect, FC } from 'react'
import {
useState,
ChangeEvent,
useEffect,
FC,
SetStateAction,
Dispatch
} from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'

Expand All @@ -7,19 +14,22 @@ import IconsWithCounter from '~/components/icons-with-counter/IconsWithCounter'
import { useDebounce } from '~/hooks/use-debounce'

import { styles } from '~/components/search-by-message/SearchByMessage.styles'
import { MessageInterface } from '~/types'

interface SearchByMessageProps {
messages: { text: string }[]
onFilteredMessagesChange: (filteredMessages: string[]) => void
onFilteredIndexChange: (filteredIndex: number) => void
isCloseSearch: () => void
allMessages: MessageInterface[]
setLimit: Dispatch<SetStateAction<number>>
}

const SearchByMessage: FC<SearchByMessageProps> = ({
messages,
onFilteredMessagesChange,
onFilteredIndexChange,
isCloseSearch
isCloseSearch,
allMessages,
setLimit
}) => {
const { t } = useTranslation()
const [search, setSearch] = useState<string>('')
Expand All @@ -36,9 +46,21 @@ const SearchByMessage: FC<SearchByMessageProps> = ({
}
useEffect(() => {
if (search) {
const filtered = messages.filter((message) =>
message.text.toLowerCase().includes(search.toLowerCase())
)
const possibleMessage: number[] = []
const filtered = allMessages.filter((message, index) => {
const isMatch = message.text
.toLowerCase()
.includes(search.toLowerCase())

if (isMatch) {
possibleMessage.push(index)
}
const maxNumber = Math.max(...possibleMessage)
setLimit(maxNumber + 1)

return isMatch
})

const filteredMessages = filtered.map((item) => item.text)

debouncedOnFilteredMessagesChange(filteredMessages)
Expand All @@ -48,7 +70,7 @@ const SearchByMessage: FC<SearchByMessageProps> = ({
}

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search, messages])
}, [search, allMessages])

const onClose = () => {
setSearch('')
Expand Down
58 changes: 54 additions & 4 deletions src/containers/chat/chat-header/ChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { FC, MouseEvent, useRef, useState } from 'react'
import {
FC,
MouseEvent,
SetStateAction,
useCallback,
useEffect,
useRef,
useState,
Dispatch
} from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
Expand All @@ -14,9 +23,17 @@ import SearchByMessage from '~/components/search-by-message/SearchByMessage'
import ChatMenu from '~/containers/layout/chat-menu/ChatMenu'

import { styles } from '~/containers/chat/chat-header/ChatHeader.styles'
import { ChatResponse, UserResponse } from '~/types'
import {
ChatResponse,
GetMessagesResponse,
MessageInterface,
UserResponse
} from '~/types'
import { useAppSelector } from '~/hooks/use-redux'
import { selectIsUserOnline } from '~/redux/selectors/socket-selectors'
import useAxios from '~/hooks/use-axios'
import { defaultResponses } from '~/constants'
import { messageService } from '~/services/message-service'

interface ChatHeaderProps {
onClick: (e?: MouseEvent<HTMLButtonElement>) => void
Expand All @@ -28,6 +45,7 @@ interface ChatHeaderProps {
messages: { text: string }[]
onFilteredMessagesChange: (filteredMessages: string[]) => void
onFilteredIndexChange: (filteredIndex: number) => void
setLimit: Dispatch<SetStateAction<number>>
}

const ChatHeader: FC<ChatHeaderProps> = ({
Expand All @@ -39,10 +57,12 @@ const ChatHeader: FC<ChatHeaderProps> = ({
currentChat,
messages,
onFilteredMessagesChange,
onFilteredIndexChange
onFilteredIndexChange,
setLimit
}) => {
const [isSearchOpen, setIsSearchOpen] = useState<boolean>(false)
const [menuAnchorEl, setMenuAnchorEl] = useState<HTMLElement | null>(null)
const [allMessages, setAllMessages] = useState<MessageInterface[]>([])
const anchorRef = useRef<HTMLDivElement | null>(null)
const { t } = useTranslation()
const { isMobile } = useBreakpoints()
Expand Down Expand Up @@ -76,6 +96,35 @@ const ChatHeader: FC<ChatHeaderProps> = ({
setIsSearchOpen(false)
}

const getAllMessages = useCallback(
() =>
messageService.getMessages({
chatId: currentChat._id,
limit: Number.MAX_SAFE_INTEGER,
skip: 0
}),
[currentChat._id]
)

const onAllMessagesResponse = useCallback(
dudchakk marked this conversation as resolved.
Show resolved Hide resolved
(response: GetMessagesResponse) => {
const items = response.items ?? []
setAllMessages(items)
},
[setAllMessages]
)

const { fetchData } = useAxios({
service: getAllMessages,
onResponse: onAllMessagesResponse,
defaultResponse: defaultResponses.itemsWithCount,
fetchOnMount: false
})

useEffect(() => {
void fetchData()
}, [fetchData])

const status = isOnline ? (
<>
<Typography sx={styles.statusBadge} />
Expand Down Expand Up @@ -111,10 +160,11 @@ const ChatHeader: FC<ChatHeaderProps> = ({
{isSearchOpen && (
<Box sx={styles.searchContainer}>
<SearchByMessage
allMessages={allMessages}
isCloseSearch={closeSearch}
messages={messages}
onFilteredIndexChange={onFilteredIndexChange}
onFilteredMessagesChange={onFilteredMessagesChange}
setLimit={setLimit}
/>
</Box>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/chat/Chat.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const styles = {
padding: '9px 16px 0'
}
}),
loader: { color: 'primary.700' },
loader: { color: 'primary.700', position: 'absolute', top: '250px' },
chip: { backgroundColor: 'basic.white' },
chipWrapper: {
display: 'flex',
Expand Down
29 changes: 18 additions & 11 deletions src/pages/chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const Chat = () => {
const [prevScrollTop, setPrevScrollTop] = useState(0)
const { setChatInfo, chatInfo } = useChatContext()
const { userId: myId } = useAppSelector((state) => state.appMain)
const [limit, setLimit] = useState<number>(15)
const dispatch = useAppDispatch()

useEffect(() => {
Expand All @@ -62,8 +63,6 @@ const Chat = () => {
}
}, [dispatch])

const limit = 15

const userToSpeak = useMemo<Member | undefined>(
() => selectedChat?.members.find((member) => member.user._id !== myId),
[selectedChat, myId]
Expand Down Expand Up @@ -139,7 +138,7 @@ const Chat = () => {
limit,
skip
}),
[selectedChat?._id, skip]
[selectedChat?._id, skip, limit]
)

const {
Expand Down Expand Up @@ -173,7 +172,7 @@ const Chat = () => {

useEffect(() => {
selectedChat && void fetchData()
}, [selectedChat, fetchData])
}, [selectedChat, fetchData, limit])

useEffect(() => {
const currentChatId = localStorage.getItem('currentChatId')
Expand All @@ -191,7 +190,7 @@ const Chat = () => {
}
}, [listOfChats, lastOpenedChat, selectedChat])

if (loading || (isMessagesLoading && !skip)) {
if (loading) {
return <Loader size={100} />
}

Expand Down Expand Up @@ -229,12 +228,18 @@ const Chat = () => {
? `${userToSpeak.user.firstName} ${userToSpeak.user.lastName}`
: t('chatPage.interlocutor')

return markedAsDeleted ? (
<AppChip labelSx={styles.warningLabel} sx={styles.warningChip}>
<WarningAmberRoundedIcon />
{t('chatPage.deletedChip', { userName: userName })}
</AppChip>
) : (
if (isMessagesLoading) return

if (markedAsDeleted) {
return (
<AppChip labelSx={styles.warningLabel} sx={styles.warningChip}>
<WarningAmberRoundedIcon />
{t('chatPage.deletedChip', { userName: userName })}
</AppChip>
)
}

return (
<ChatTextArea
label={t('chatPage.chat.inputLabel')}
onClick={() => void onMessageSend()}
Expand Down Expand Up @@ -301,6 +306,7 @@ const Chat = () => {
onFilteredIndexChange={hadleIndexMessage}
onFilteredMessagesChange={handleFilteredMessage}
onMenuClick={openChatsHandler}
setLimit={setLimit}
updateChats={handleUpdateChats}
updateMessages={fetchData}
user={userToSpeak?.user}
Expand All @@ -315,6 +321,7 @@ const Chat = () => {
messages={messages}
scrollHeight={!skip ? 0 : prevScrollHeight}
scrollTop={!skip ? 0 : prevScrollTop}
skip={skip}
userToSpeak={userToSpeak as Member}
/>
{renderChatTextArea()}
Expand Down
7 changes: 7 additions & 0 deletions src/pages/chat/MessagesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useAppSelector } from '~/hooks/use-redux'
import { selectIsTyping } from '~/redux/selectors/socket-selectors'
import { Member, MessageInterface } from '~/types'
import { getGroupedByDate, getIsNewDay } from '~/utils/helper-functions'
import Loader from '~/components/loader/Loader'

interface MessagesListProps {
messages: MessageInterface[]
Expand All @@ -24,6 +25,7 @@ interface MessagesListProps {
infiniteLoadCallback: (scrollTop: number, scrollHeight: number) => void
chatId: string
userToSpeak: Member
skip: number
}

const MessagesList = ({
Expand All @@ -34,6 +36,7 @@ const MessagesList = ({
infiniteLoadCallback,
scrollTop,
scrollHeight,
skip,
chatId,
userToSpeak
}: MessagesListProps) => {
Expand Down Expand Up @@ -70,6 +73,10 @@ const MessagesList = ({
[isMessagesLoading, infiniteLoadCallback]
)

if (isMessagesLoading && !skip) {
return <Loader size={100} sx={styles.loader} />
}

if (messages.length === 0) {
return (
<Box sx={styles.chipWrapper}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('SearchByMessage', () => {
const onFilteredMessagesChange = vi.fn()
const onFilteredIndexChange = vi.fn()
const isCloseSearch = vi.fn()
const setLimit = vi.fn()
const mockMessage = [
{
_id: '64ee0a2f6ae3b95ececb05b5',
Expand Down Expand Up @@ -43,6 +44,8 @@ describe('SearchByMessage', () => {
messages={mockMessage}
onFilteredIndexChange={onFilteredIndexChange}
onFilteredMessagesChange={onFilteredMessagesChange}
allMessages={mockMessage}
setLimit={setLimit}
/>
)
})
Expand Down
15 changes: 14 additions & 1 deletion tests/unit/pages/chat/messages-list/MessagesList.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ vi.mock('~/components/typing-block/TypingBlock', () => ({
default: vi.fn(() => <div>Typing animation</div>)
}))

vi.spyOn(window, "getComputedStyle").mockReturnValue(new CSSStyleDeclaration)
vi.spyOn(window, 'getComputedStyle').mockReturnValue(new CSSStyleDeclaration())

global.IntersectionObserver = vi.fn().mockImplementation((callback) => ({
observe: vi.fn(),
Expand Down Expand Up @@ -78,4 +78,17 @@ describe('MessagesList component', () => {
)
expect(screen.getByText(/2024/i)).toBeInTheDocument()
})

it('loader should be render', () => {
renderWithProviders(
<MessagesList
infiniteLoadCallback={vi.fn()}
messages={[]}
isMessagesLoading={true}
/>
)
const loader = screen.getByTestId('loader')

expect(loader).toBeInTheDocument()
})
})
Loading