import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { usePrimaryPage } from '@/contexts/primary-page-context' import { buildHiveTalkJoinUrl, roomIdForPubkeys } from '@/lib/hivetalk' import { getNostrArchivesProfileUrl, openExternalUrl } from '@/lib/link' import { pubkeyToNpub } from '@/lib/pubkey' import { useMuteList } from '@/contexts/mute-list-context' import { muteSetHas } from '@/lib/mute-set' import { normalizeAnyRelayUrl } from '@/lib/url' import { useNostr } from '@/providers/NostrProvider' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' import { encodeProfileInteractionsSpellId } from '@/pages/primary/SpellsPage/fauxSpellConfig' import client, { replaceableEventService } from '@/services/client.service' import { nip66Service } from '@/services/nip66.service' import RawEventDialog from '@/components/NoteOptions/RawEventDialog' import { Bell, BellOff, Code, Copy, Ellipsis, ExternalLink, Flag, ThumbsUp, MessageCircle, Network, Send, SatelliteDish, Video } from 'lucide-react' import { useMemo, useState, useEffect } from 'react' import { createReactionDraftEvent } from '@/lib/draft-event' import PostEditor from '@/components/PostEditor' import { showSimplePublishSuccess, toastPublishPromise } from '@/lib/publishing-feedback' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Event, kinds } from 'nostr-tools' export default function ProfileOptions({ pubkey, profileEvent, onSendPublicMessage, onSendCallInvite, onSeeReports }: { pubkey: string /** Optional profile event (kind 0): reply / like, republish to relays, view JSON */ profileEvent?: Event /** Opens the post editor in public message mode with this profile's pubkey in the mention list. */ onSendPublicMessage?: () => void /** Opens the post editor to send the call invite URL as a public message to this profile. */ onSendCallInvite?: (url: string) => void /** Opens the profile reports modal. */ onSeeReports?: () => void }) { const { t } = useTranslation() const { navigate } = usePrimaryPage() const { pubkey: accountPubkey, publish, checkLogin, canManageIdentity } = useNostr() const { mutePubkeySet, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList() const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays() const { relaySets, favoriteRelays } = useFavoriteRelays() const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false) const [openReply, setOpenReply] = useState(false) const [reacting, setReacting] = useState(false) const [monitoringListRelayCount, setMonitoringListRelayCount] = useState(null) const [localProfileEvent, setLocalProfileEvent] = useState(profileEvent) // Fetch profile event if not provided useEffect(() => { if (profileEvent) { setLocalProfileEvent(profileEvent) return } // If profileEvent is not provided, try to fetch it using comprehensive search const fetchEvent = async () => { try { // Use fetchProfileEvent which includes comprehensive relay search const event = await replaceableEventService.fetchProfileEvent(pubkey, false, { allowWideRelayFallback: true }) if (event) { setLocalProfileEvent(event) } } catch { // Silently fail: reply/like stay hidden until the event loads } } fetchEvent() }, [pubkey, profileEvent]) const isMuted = useMemo(() => muteSetHas(mutePubkeySet, pubkey), [mutePubkeySet, pubkey]) const nostrArchivesProfileUrl = useMemo(() => getNostrArchivesProfileUrl(pubkey), [pubkey]) /** All available relays: current feed, favorites, relay sets, defaults (FAST_READ, FAST_WRITE). */ const allAvailableRelayUrls = useMemo(() => { const urls = [ ...currentBrowsingRelayUrls.map((url) => normalizeAnyRelayUrl(url) || url), ...favoriteRelays.map((url) => normalizeAnyRelayUrl(url) || url), ...relaySets.flatMap((set) => set.relayUrls.map((url) => normalizeAnyRelayUrl(url) || url)), ...FAST_READ_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url), ...FAST_WRITE_RELAY_URLS.map((url) => normalizeAnyRelayUrl(url) || url) ].filter(Boolean) as string[] return Array.from(new Set(urls)) }, [currentBrowsingRelayUrls, favoriteRelays, relaySets]) useEffect(() => { void nip66Service.getPublicLivelyRelayUrls().then((urls) => { setMonitoringListRelayCount(urls?.length ?? 0) }) }, []) const eventToUse = localProfileEvent || profileEvent /** Kind 0 only; coerce `kind` in case deserialization yields a string. */ const kind0ForRelay = eventToUse != null && Number(eventToUse.kind) === kinds.Metadata ? eventToUse : undefined const handleRepublishToAllAvailable = async () => { if (!kind0ForRelay) { toast.error(t('Profile event not available')) return } const promise = client.publishEvent(allAvailableRelayUrls, kind0ForRelay, { skipOutboxRetry: true }).then((result) => { if (result.successCount < 1) { throw new Error(t('No relay accepted the event')) } return result }) toastPublishPromise(promise, { loading: t('Republishing...'), success: () => t('Successfully republish to all available relays'), error: (err) => t('Failed to republish to all available relays: {{error}}', { error: err.message }) }) } const handleRepublishToAllActive = async () => { if (!kind0ForRelay) { toast.error(t('Profile event not available')) return } const promise = (async () => { let relays = await nip66Service.getPublicLivelyRelayUrls() const usedMonitoringList = !!relays?.length if (!relays?.length) { relays = allAvailableRelayUrls } if (!relays?.length) { throw new Error(t('No relays available')) } const result = await client.publishEvent(relays, kind0ForRelay, { skipOutboxRetry: true }) const minRequired = usedMonitoringList ? 5 : 1 if (result.successCount < minRequired) { throw new Error( usedMonitoringList ? t('Only {{count}} relay(s) accepted the event; at least 5 required for "all active relays".', { count: result.successCount }) : t('No relay accepted the event') ) } return result })() toastPublishPromise(promise, { loading: t('Republishing...'), success: () => t('Successfully republish to all active relays'), error: (err) => t('Failed to republish to all active relays: {{error}}', { error: err.message }) }) } const handleLike = () => { if (!eventToUse) return checkLogin(async () => { if (reacting) return setReacting(true) try { const reaction = createReactionDraftEvent(eventToUse, '+') const evt = await publish(reaction) if (evt) { showSimplePublishSuccess(t('Reaction published')) } } finally { setReacting(false) } }) } if (pubkey === accountPubkey) return null const callInviteUrl = accountPubkey && buildHiveTalkJoinUrl({ room: roomIdForPubkeys(accountPubkey, pubkey) }) return ( {eventToUse && ( <> setOpenReply(true)}> {t('Reply')} {reacting ? t('Publishing...') : t('Like')} )} {onSendPublicMessage && ( {t('Send public message')} )} {callInviteUrl && ( <> window.open(callInviteUrl, '_blank', 'noopener,noreferrer')} > { navigator.clipboard.writeText(callInviteUrl) toast.success(t('Copied to clipboard')) }} > {t('Copy call invite link')} {onSendCallInvite && ( onSendCallInvite(callInviteUrl)}> {t('Send call invite')} )} )} navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))} > {t('Copy user ID')} navigate('spells', { spell: encodeProfileInteractionsSpellId(pubkey) })}> {t('Interactions map')} {onSeeReports && ( {t('See reports')} )} {nostrArchivesProfileUrl && ( openExternalUrl(nostrArchivesProfileUrl)}> {t('View on Nostr.Archives')} )} {kind0ForRelay && ( <> {t('Republish to all available relays')} ({allAvailableRelayUrls.length}) {t('Republish to all active relays')} {monitoringListRelayCount !== null && ` (${monitoringListRelayCount > 0 ? monitoringListRelayCount : allAvailableRelayUrls.length})`} setIsRawEventDialogOpen(true)}> {t('View JSON')} )} {canManageIdentity && (isMuted ? ( unmutePubkey(pubkey)} className="text-destructive focus:text-destructive" > {t('Unmute user')} ) : ( <> mutePubkeyPrivately(pubkey)} className="text-destructive focus:text-destructive" > {t('Mute user privately')} mutePubkeyPublicly(pubkey)} className="text-destructive focus:text-destructive" > {t('Mute user publicly')} ))} {eventToUse && ( )} {kind0ForRelay && ( setIsRawEventDialogOpen(false)} /> )} ) }