import type React from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useUIKit } from '@tencentcloud/uikit-base-component-react'; import cs from 'classnames'; import { View, useGroupStore, ConversationType, GroupType } from 'tuikit-atomicx-react/chat'; import { ContextMenu } from '../../../../baseComp'; import { useMessageActions } from '../../../../hooks/useMessageActions'; import { useMessageListContext } from '../../MessageListContext'; import classes from './MessageActionDropdown.module.scss'; import type { MessageAction } from '../../../../types/message'; import type { MessageInfo } from 'tuikit-atomicx-react/chat'; interface MessageActionsProps { message: MessageInfo; messageActionList?: MessageAction[] | undefined; className?: string | undefined; children: React.ReactElement<{ ref?: React.Ref }>; } function MessageActionDropdown(props: MessageActionsProps) { const { message, messageActionList, className, children, } = props; const { t, direction } = useUIKit(); const defaultActionList = useMessageActions(); const { joinedGroupList } = useGroupStore(); const { onContextMenuOpenChange } = useMessageListContext(); const reportedContextMenuOpenRef = useRef(false); // `action.visible` callbacks may depend on the current time (e.g. `recall` // reads `Date.now()`), which React cannot track. This state acts as a manual // invalidation signal: it is refreshed every time the menu opens so `actions` // is re-evaluated instead of returning a stale memoized result. const [menuOpenedAt, setMenuOpenedAt] = useState(() => Date.now()); const handleContextMenuOpenChange = useCallback((open: boolean) => { if (open) { setMenuOpenedAt(Date.now()); } reportedContextMenuOpenRef.current = open; onContextMenuOpenChange?.(open); }, [onContextMenuOpenChange]); // Release the open state if unmounted while the menu is still open useEffect(() => () => { if (reportedContextMenuOpenRef.current) { onContextMenuOpenChange?.(false); } }, [onContextMenuOpenChange]); // Community groups do not support message quoting. This is a built-in, fixed // rule, so we resolve the current conversation's group type here and drop the // `quote` action for community groups. const isCommunityConversation = useMemo(() => { if (message.conversationType !== ConversationType.Group) { return false; } return joinedGroupList.some( group => group.groupID === message.to && group.groupType === GroupType.Community, ); }, [joinedGroupList, message.conversationType, message.to]); // Use default or custom action list const actions = useMemo(() => { const actionList = (messageActionList || defaultActionList).filter( action => !(isCommunityConversation && action.key === 'quote'), ); return actionList.map((action) => { // Check visibility conditions if (action.visible !== undefined) { const shouldShow = typeof action.visible === 'function' ? action.visible(message) : action.visible; if (!shouldShow) { return null; } } // If custom component is provided if (action.component) { const ActionComponent = action.component; return ( ); } // Render default action item return ( { action.onClick?.(message); }} icon={action.icon} > {t(`MessageList.${action.label?.toLowerCase()}`, { defaultValue: action.label })} ); }).filter(Boolean); // `menuOpenedAt` is intentionally listed: it forces re-evaluation of time-based `visible` callbacks // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultActionList, message, messageActionList, t, isCommunityConversation, menuOpenedAt]); if (!children) { return null; } // No visible actions: render the message bubble as-is without mounting the context menu if (actions.length === 0) { return children; } return ( {children} {actions} ); } export default MessageActionDropdown;