Skip to content

Commit

Permalink
Search by messages in chat (#1116)
Browse files Browse the repository at this point in the history
* fixed conflicts

* fixed some tests

* fix conflicts

* fix some bugs in search

* fixed tests
  • Loading branch information
ArturBekhDEV authored and Mav-Ivan committed Sep 19, 2023
1 parent afb2160 commit 928ac82
Show file tree
Hide file tree
Showing 12 changed files with 244 additions and 37 deletions.
37 changes: 30 additions & 7 deletions src/components/icons-with-counter/IconsWithCounter.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -11,20 +11,41 @@ import { styles } from '~/components/icons-with-counter/IconsWithCounter.style'

interface IconsWithCounterProps {
maxValue: number
onFilteredIndexChange: (index: number) => void
}

const IconsWithCounter: FC<IconsWithCounterProps> = ({ maxValue }) => {
const IconsWithCounter: FC<IconsWithCounterProps> = ({
maxValue,
onFilteredIndexChange
}) => {
const [possibleValue, setPossibleValue] = useState<number>(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 (
Expand All @@ -33,7 +54,9 @@ const IconsWithCounter: FC<IconsWithCounterProps> = ({ maxValue }) => {
<KeyboardArrowUpIcon />
</IconButton>
<Typography sx={styles.typography}>
{possibleValue} {t('common.of')} {maxValue}
{maxValue
? `${possibleValue + 1} ${t('common.of')} ${maxValue}`
: `${possibleValue} ${t('common.of')} ${maxValue}`}
</Typography>
<IconButton data-testid='IconDown' onClick={handleDecrement}>
<KeyboardArrowDownIcon />
Expand Down
15 changes: 13 additions & 2 deletions src/components/message/Message.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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'
Expand Down
47 changes: 38 additions & 9 deletions src/components/message/Message.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -23,18 +23,24 @@ interface MessageProps {
sx?: {
avatar?: SxProps
}
filteredMessages?: string[]
filteredIndex: number
}

const Message: FC<MessageProps> = ({ message, prevMessage, sx = {} }) => {
const Message: FC<MessageProps> = ({
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
Expand All @@ -46,11 +52,27 @@ const Message: FC<MessageProps> = ({ message, prevMessage, sx = {} }) => {
e.stopPropagation()
}

const messageRef = useRef<HTMLDivElement>(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 && (
<Link onClick={handleLinkClick} to={pathToProfile}>
<Avatar
Expand All @@ -59,13 +81,20 @@ const Message: FC<MessageProps> = ({ message, prevMessage, sx = {} }) => {
/>
</Link>
)

return (
<Box sx={styles.root(isMyMessage, isAvatarVisible)}>
<Box ref={messageRef} sx={styles.root(isMyMessage, isAvatarVisible)}>
{avatar}
<AppCard sx={styles.messageCard(isMyMessage)}>
<AppCard
sx={
isTextFiltered
? styles.findMessageCard
: styles.messageCard(isMyMessage)
}
>
{text}
<Typography sx={styles.date(isMyMessage)}>{date}</Typography>
<Typography sx={styles.date(isMyMessage, isTextFiltered)}>
{date}
</Typography>
</AppCard>
</Box>
)
Expand Down
2 changes: 1 addition & 1 deletion src/components/search-by-message/SearchByMessage.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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%'
Expand Down
51 changes: 43 additions & 8 deletions src/components/search-by-message/SearchByMessage.tsx
Original file line number Diff line number Diff line change
@@ -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<SearchByMessageProps> = ({ maxValue = 10 }) => {
const SearchByMessage: FC<SearchByMessageProps> = ({
messages,
onFilteredMessagesChange,
onFilteredIndexChange,
isCloseSearch
}) => {
const { t } = useTranslation()
const [search, setSearch] = useState<string>('')
const [findMessage, setFindMessage] = useState<string[]>([])

const debouncedOnFilteredMessagesChange = useDebounce(
(filteredMessages: string[]) => {
onFilteredMessagesChange(filteredMessages)
},
500
)
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
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 (
<Box sx={styles.container}>
<IconsWithCounter maxValue={maxValue} />
<Box onClick={(e) => e.stopPropagation()} sx={styles.container}>
<IconsWithCounter
maxValue={findMessage.length}
onFilteredIndexChange={onFilteredIndexChange}
/>
<InputWithIcon
onChange={onChange}
onClear={onClear}
onClear={onClose}
placeholder={`${t('common.search')}...`}
sx={styles.input}
value={search}
Expand Down
12 changes: 11 additions & 1 deletion src/containers/chat/chat-header/ChatHeader.styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TypographyVariantEnum } from '~/types'
import { mainShadow } from '~/styles/app-theme/custom-shadows'

export const styles = {
container: { position: 'relative', alignItems: 'center', p: '14px 24px' },
Expand Down Expand Up @@ -26,5 +27,14 @@ export const styles = {
columnGap: '5px',
right: '24px'
},
icon: { color: 'primary.700' }
icon: { color: 'primary.700' },
searchContainer: {
width: '100%',
position: 'absolute',
zIndex: '1',
top: '75px',
left: '0px',
pt: '10px',
boxShadow: mainShadow
}
}
36 changes: 33 additions & 3 deletions src/containers/chat/chat-header/ChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, MouseEvent } from 'react'
import { FC, MouseEvent, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
Expand All @@ -10,6 +10,7 @@ import MoreVertIcon from '@mui/icons-material/MoreVert'
import useBreakpoints from '~/hooks/use-breakpoints'
import AppCard from '~/components/app-card/AppCard'
import TitleWithDescription from '~/components/title-with-description/TitleWithDescription'
import SearchByMessage from '~/components/search-by-message/SearchByMessage'

import { styles } from '~/containers/chat/chat-header/ChatHeader.styles'
import { UserResponse } from '~/types'
Expand All @@ -18,18 +19,33 @@ interface ChatHeaderProps {
onClick: () => void
onMenuClick: (e: MouseEvent<HTMLButtonElement>) => void
user: Pick<UserResponse, '_id' | 'firstName' | 'lastName' | 'photo'>
messages: { text: string }[]
onFilteredMessagesChange: (filteredMessages: string[]) => void
onFilteredIndexChange: (filteredIndex: number) => void
}

const ChatHeader: FC<ChatHeaderProps> = ({ onClick, onMenuClick, user }) => {
const ChatHeader: FC<ChatHeaderProps> = ({
onClick,
user,
onMenuClick,
messages,
onFilteredMessagesChange,
onFilteredIndexChange
}) => {
const [isSearchOpen, setIsSearchOpen] = useState<boolean>(false)
const { t } = useTranslation()
const { isMobile } = useBreakpoints()

const handleOnClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
}
const handleSearch = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
setIsSearchOpen(!isSearchOpen)
}

const iconButtons = [
{ _id: 1, icon: <SearchIcon />, handleOnClick },
{ _id: 1, icon: <SearchIcon />, handleOnClick: handleSearch },
{ _id: 2, icon: <MoreVertIcon />, handleOnClick }
]

Expand All @@ -39,6 +55,10 @@ const ChatHeader: FC<ChatHeaderProps> = ({ onClick, onMenuClick, user }) => {
</IconButton>
))

const closeSearch = () => {
setIsSearchOpen(false)
}

const status = (
<>
<Typography sx={styles.statusBadge} />
Expand All @@ -59,6 +79,16 @@ const ChatHeader: FC<ChatHeaderProps> = ({ onClick, onMenuClick, user }) => {
title={`${user.firstName} ${user.lastName}`}
/>
<Box sx={styles.actions}>{icons}</Box>
{isSearchOpen && (
<Box sx={styles.searchContainer}>
<SearchByMessage
isCloseSearch={closeSearch}
messages={messages}
onFilteredIndexChange={onFilteredIndexChange}
onFilteredMessagesChange={onFilteredMessagesChange}
/>
</Box>
)}
</AppCard>
)
}
Expand Down
Loading

0 comments on commit 928ac82

Please sign in to comment.