import { DotsThreeIcon, FlagIcon, ProhibitInsetIcon, SignOutIcon, SpinnerGapIcon, } from '@phosphor-icons/react' import classNames from 'classnames' import React, { useCallback, useEffect, useId, useRef, useState } from 'react' import type { Channel as ChannelType, ChannelMemberResponse } from 'stream-chat' import { useChannelModerationActions } from '../../hooks/useChannelModerationActions' import ActionButton from '../ActionButton' export interface ChannelActionsMenuProps { channel: ChannelType participant: ChannelMemberResponse | undefined showDeleteConversation?: boolean /** * Show the Block/Unblock action. Defaults to true. * Set false to hide it (e.g. the Linktree official channel). */ showBlockParticipant?: boolean /** * Show the Report action. Defaults to true. * Set false to hide it (e.g. the Linktree official channel). */ showReportParticipant?: boolean onLeaveConversation?: (channel: ChannelType) => void onBlockParticipant?: (participantId?: string) => void onDeleteConversationClick?: () => void onBlockParticipantClick?: () => void onReportParticipantClick?: () => void customChannelActions?: React.ReactNode /** Classes applied to the trigger ("...") button so it matches the header. */ triggerClassName?: string } /** * Conversation overflow ("...") menu rendered as an anchored popover. * * Replaces the slide-in sidebar dialog for moderation actions: the trigger * opens a small popover that lists the available options (delete, block, * report) inline with their icons. */ export const ChannelActionsMenu: React.FC = ({ channel, participant, showDeleteConversation = true, showBlockParticipant = true, showReportParticipant = true, onLeaveConversation, onBlockParticipant, onDeleteConversationClick, onBlockParticipantClick, onReportParticipantClick, customChannelActions, triggerClassName, }) => { const [isOpen, setIsOpen] = useState(false) const containerRef = useRef(null) const menuId = useId() const close = useCallback(() => setIsOpen(false), []) const { isParticipantBlocked, isCheckingBlockedStatus, isLeaving, isUpdatingBlockStatus, handleLeaveConversation, handleBlockUser, handleUnblockUser, handleReportUser, } = useChannelModerationActions({ channel, participant, showBlockParticipant, enabled: isOpen, onLeaveConversation, onBlockParticipant, onDeleteConversationClick, onBlockParticipantClick, onReportParticipantClick, onActionComplete: close, logLabel: 'ChannelActionsMenu', }) // Close the popover on outside click or Escape. useEffect(() => { if (!isOpen) return const handlePointerDown = (event: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { setIsOpen(false) } } const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { setIsOpen(false) } } document.addEventListener('mousedown', handlePointerDown) document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('mousedown', handlePointerDown) document.removeEventListener('keydown', handleKeyDown) } }, [isOpen]) if (!participant) return null const hasActions = showDeleteConversation || showBlockParticipant || showReportParticipant || Boolean(customChannelActions) if (!hasActions) return null return (
{isOpen && (
    {showDeleteConversation && (
  • {isLeaving ? ( ) : ( )} Delete Conversation
  • )} {showBlockParticipant && (
  • {isCheckingBlockedStatus ? ( // Block status not yet resolved — show a disabled placeholder // so a premature click can't act on a stale block state. Block ) : isParticipantBlocked ? ( {isUpdatingBlockStatus ? ( ) : ( )} Unblock ) : ( {isUpdatingBlockStatus ? ( ) : ( )} Block )}
  • )} {showReportParticipant && (
  • Report
  • )} {customChannelActions}
)}
) }