diff --git a/src/components/Message/MessageStatus.tsx b/src/components/Message/MessageStatus.tsx index df16a2d1d4..c67f345982 100644 --- a/src/components/Message/MessageStatus.tsx +++ b/src/components/Message/MessageStatus.tsx @@ -1,11 +1,13 @@ -import React from 'react'; +import React, { useState } from 'react'; +import clsx from 'clsx'; import { DeliveredCheckIcon, MessageDeliveredIcon } from './icons'; import { getReadByTooltipText, mapToUserNameOrId, TooltipUsernameMapper } from './utils'; import { AvatarProps, Avatar as DefaultAvatar } from '../Avatar'; import { LoadingIndicator } from '../Loading'; -import { Tooltip } from '../Tooltip'; +import { PopperTooltip, Tooltip } from '../Tooltip'; +import { useEnterLeaveHandlers } from '../Tooltip/hooks'; import { useChatContext } from '../../context/ChatContext'; import { useComponentContext } from '../../context/ComponentContext'; @@ -20,16 +22,6 @@ export type MessageStatusProps = { tooltipUserNameMapper?: TooltipUsernameMapper; }; -// TODO: remove after fully deprecating V1 theming -const TooltipContainer = ({ children }: React.PropsWithChildren>) => { - const { themeVersion } = useChatContext('TooltipContainer'); - return themeVersion === '2' ? ( -
{children}
- ) : ( - <>{children} - ); -}; - const UnMemoizedMessageStatus = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics >( @@ -41,6 +33,8 @@ const UnMemoizedMessageStatus = < tooltipUserNameMapper = mapToUserNameOrId, } = props; + const { handleEnter, handleLeave, popperVisible } = useEnterLeaveHandlers(); + const { client } = useChatContext('MessageStatus'); const { Avatar: contextAvatar } = useComponentContext('MessageStatus'); const { @@ -52,61 +46,100 @@ const UnMemoizedMessageStatus = < } = useMessageContext('MessageStatus'); const { t } = useTranslationContext('MessageStatus'); const { themeVersion } = useChatContext('MessageStatus'); + const [referenceElement, setReferenceElement] = useState(null); const Avatar = propAvatar || contextAvatar || DefaultAvatar; - if (!isMyMessage() || message.type === 'error') { - return null; - } + if (!isMyMessage() || message.type === 'error') return null; const justReadByMe = readBy?.length === 1 && readBy[0].id === client.user?.id; const rootClassName = `str-chat__message-${messageType}-status str-chat__message-status`; - if (message.status === 'sending') { - return ( - - {t('Sending...')} - - - ); - } - - if (readBy?.length && !threadList && !justReadByMe) { - const [lastReadUser] = readBy.filter((item) => item.id !== client.user?.id); - - return ( - - - {getReadByTooltipText(readBy, t, client, tooltipUserNameMapper)} + const sending = message.status === 'sending'; + const delivered = message.status === 'received' && message.id === lastReceivedId && !threadList; + const deliveredAndRead = !!(readBy?.length && !threadList && !justReadByMe); + + const [lastReadUser] = deliveredAndRead + ? readBy.filter((item) => item.id !== client.user?.id) + : []; + + return ( + + {sending && ( + <> + {themeVersion === '1' && {t('Sending...')}} + {themeVersion === '2' && ( + + {t('Sending...')} + + )} + + + )} + + {delivered && !deliveredAndRead && ( + <> + {themeVersion === '1' && {t('Delivered')}} + {themeVersion === '2' && ( + + {t('Delivered')} + + )} + {themeVersion === '2' ? : } + + )} + + {deliveredAndRead && ( + <> + {themeVersion === '1' && ( + {getReadByTooltipText(readBy, t, client, tooltipUserNameMapper)} + )} + {themeVersion === '2' && ( + + {getReadByTooltipText(readBy, t, client, tooltipUserNameMapper)} + + )} - - {readBy.length > 2 && ( - - {readBy.length - 1} - - )} - - ); - } - - if (message.status === 'received' && message.id === lastReceivedId && !threadList) { - return ( - - {t('Delivered')} - {themeVersion === '2' ? : } - - ); - } - - return null; + + {readBy.length > 2 && ( + + {readBy.length - 1} + + )} + + )} + + ); }; export const MessageStatus = React.memo(UnMemoizedMessageStatus) as typeof UnMemoizedMessageStatus; diff --git a/src/components/Reactions/ReactionsList.tsx b/src/components/Reactions/ReactionsList.tsx index bb4d84e17e..3871f1d9f2 100644 --- a/src/components/Reactions/ReactionsList.tsx +++ b/src/components/Reactions/ReactionsList.tsx @@ -1,8 +1,9 @@ -import React, { Suspense } from 'react'; +import React, { ComponentProps, Suspense, useState } from 'react'; import clsx from 'clsx'; import { useEmojiContext } from '../../context/EmojiContext'; import { useMessageContext } from '../../context/MessageContext'; +import { useChatContext } from '../../context/ChatContext'; import { useProcessReactions } from './hooks/useProcessReactions'; import type { NimbleEmojiProps } from 'emoji-mart'; @@ -13,6 +14,9 @@ import type { ReactEventHandler } from '../Message/types'; import type { DefaultStreamChatGenerics } from '../../types/types'; import type { ReactionEmoji } from '../Channel/emojiData'; +import { PopperTooltip } from '../Tooltip'; +import { useEnterLeaveHandlers } from '../Tooltip/hooks'; + export type ReactionsListProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics > = { @@ -32,6 +36,40 @@ export type ReactionsListProps< reverse?: boolean; }; +const ButtonWithTooltip = ({ + children, + onMouseEnter, + onMouseLeave, + ...rest +}: Omit, 'ref'>) => { + const [referenceElement, setReferenceElement] = useState(null); + + const { handleEnter, handleLeave, popperVisible } = useEnterLeaveHandlers({ + onMouseEnter, + onMouseLeave, + }); + + const { themeVersion } = useChatContext('ButtonWithTooltip'); + + return ( + <> + {themeVersion === '2' && ( + + {rest.title} + + )} + + + ); +}; + const UnMemoizedReactionsList = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics >( @@ -44,6 +82,7 @@ const UnMemoizedReactionsList = < const { additionalEmojiProps, + aggregatedNamesByType, emojiData, getEmojiByReactionType, iHaveReactedWithReaction, @@ -80,7 +119,11 @@ const UnMemoizedReactionsList = < })} key={emojiObject.id} > - + ) : null; })} diff --git a/src/components/Reactions/hooks/useProcessReactions.tsx b/src/components/Reactions/hooks/useProcessReactions.tsx index 260430a44a..49c95dc056 100644 --- a/src/components/Reactions/hooks/useProcessReactions.tsx +++ b/src/components/Reactions/hooks/useProcessReactions.tsx @@ -86,8 +86,21 @@ export const useProcessReactions = < [reactionCounts, supportedReactionsArePresent], ); + const aggregatedNamesByType = useMemo( + () => + latestReactions.reduce>>((typeMap, { type, user }) => { + typeMap[type] ??= []; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + typeMap[type].push(user?.name || user!.id); + return typeMap; + }, {}), + [latestReactions], + ); + return { additionalEmojiProps: reactionsAreCustom ? additionalEmojiProps : emojiSetDef, + aggregatedNamesByType, emojiData, getEmojiByReactionType, iHaveReactedWithReaction, diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index 89fd15bffc..33cb15d78a 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -1,9 +1,46 @@ -import React, { ComponentProps } from 'react'; +import React, { ComponentProps, useState } from 'react'; +import { PopperProps, usePopper } from 'react-popper'; + +export const Tooltip = ({ children, ...rest }: ComponentProps<'div'>) => ( +
+ {children} +
+); + +export const PopperTooltip = ({ + children, + offset = [0, 10], + referenceElement, + placement = 'top', + visible: visible = false, +}: React.PropsWithChildren<{ + referenceElement: T | null; + offset?: [number, number]; + placement?: PopperProps['placement']; + visible?: boolean; +}>) => { + const [popperElement, setPopperElement] = useState(null); + const { attributes, styles } = usePopper(referenceElement, popperElement, { + modifiers: [ + { + name: 'offset', + options: { + offset, + }, + }, + ], + placement, + }); + + if (!visible) return null; -export const Tooltip = (props: ComponentProps<'div'>) => { - const { children, ...rest } = props; return ( -
+
{children}
); diff --git a/src/components/Tooltip/hooks/index.ts b/src/components/Tooltip/hooks/index.ts new file mode 100644 index 0000000000..423bab9f07 --- /dev/null +++ b/src/components/Tooltip/hooks/index.ts @@ -0,0 +1 @@ +export * from './useEnterLeaveHandlers'; diff --git a/src/components/Tooltip/hooks/useEnterLeaveHandlers.ts b/src/components/Tooltip/hooks/useEnterLeaveHandlers.ts new file mode 100644 index 0000000000..f887044a38 --- /dev/null +++ b/src/components/Tooltip/hooks/useEnterLeaveHandlers.ts @@ -0,0 +1,26 @@ +import React, { useCallback, useState } from 'react'; + +export const useEnterLeaveHandlers = ({ + onMouseEnter, + onMouseLeave, +}: Partial>> = {}) => { + const [popperVisible, setPopperVisible] = useState(false); + + const handleEnter: React.MouseEventHandler = useCallback( + (e) => { + setPopperVisible(true); + onMouseEnter?.(e); + }, + [onMouseEnter], + ); + + const handleLeave: React.MouseEventHandler = useCallback( + (e) => { + setPopperVisible(false); + onMouseLeave?.(e); + }, + [onMouseLeave], + ); + + return { handleEnter, handleLeave, popperVisible }; +};