You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

200 lines
6.5 KiB

import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Ellipsis } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePaymentAttestationStatus } from '@/hooks/usePaymentAttestationStatus'
import { hexPubkeysEqual } from '@/lib/pubkey'
import { isAttestableSuperchatPayment } from '@/lib/superchat'
import { useNostr } from '@/providers/NostrProvider'
import { DesktopMenu } from './DesktopMenu'
import EditOrCloneEventDialog, { type TEditOrCloneMode } from './EditOrCloneEventDialog'
import { MobileMenu } from './MobileMenu'
import NoteOptionsMetaHeader from './NoteOptionsMetaHeader'
import RawEventDialog from './RawEventDialog'
import ReportDialog from './ReportDialog'
import { SubMenuAction, useMenuActions, type ShowSubMenuOptions } from './useMenuActions'
import PostEditor from '../PostEditor'
import type { HighlightData } from '../PostEditor/HighlightEditor'
export default function NoteOptions({
event,
className,
initialHighlightData,
highlightDefaultContent,
isPostEditorOpen,
onPostEditorClose,
onOpenPublicMessage,
initialPublicMessageTo,
onOpenCallInvite,
initialDefaultContent,
pinned = false,
seenOnAllowlist
}: {
event: Event
className?: string
/** Note is shown in a pinned section (profile pins, etc.). */
pinned?: boolean
/** When set (home favorites feed), relay list in the menu matches the feed allowlist. */
seenOnAllowlist?: readonly string[]
initialHighlightData?: HighlightData
highlightDefaultContent?: string
isPostEditorOpen?: boolean
onPostEditorClose?: () => void
/** Opens the post editor in public message mode with the given pubkey in the mention list. */
onOpenPublicMessage?: (pubkey: string) => void
/** When set, the post editor is opened in public message mode with this pubkey pre-filled. */
initialPublicMessageTo?: string | null
/** Opens the post editor with the given content (e.g. call invite URL). */
onOpenCallInvite?: (url: string) => void
/** Default content when opening the editor (e.g. call invite URL). */
initialDefaultContent?: string | null
}) {
const { t } = useTranslation()
const { pubkey } = useNostr()
const { isSmallScreen } = useScreenSize()
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
const [isAttestationDialogOpen, setIsAttestationDialogOpen] = useState(false)
const [isReportDialogOpen, setIsReportDialogOpen] = useState(false)
const [editCloneOpen, setEditCloneOpen] = useState(false)
const [editCloneMode, setEditCloneMode] = useState<TEditOrCloneMode>('clone')
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [showSubMenu, setShowSubMenu] = useState(false)
const [activeSubMenu, setActiveSubMenu] = useState<SubMenuAction[]>([])
const [subMenuTitle, setSubMenuTitle] = useState('')
const [subMenuSearchable, setSubMenuSearchable] = useState(false)
const closeDrawer = () => {
setIsDrawerOpen(false)
setShowSubMenu(false)
setSubMenuSearchable(false)
}
const goBackToMainMenu = () => {
setShowSubMenu(false)
setSubMenuSearchable(false)
}
const showSubMenuActions = (
subMenu: SubMenuAction[],
title: string,
options?: ShowSubMenuOptions
) => {
setActiveSubMenu(subMenu)
setSubMenuTitle(title)
setSubMenuSearchable(Boolean(options?.subMenuSearchable))
setShowSubMenu(true)
}
const attestableEvent = isAttestableSuperchatPayment(event) ? event : undefined
const { attested, attestationEvent, recipientPubkey } = usePaymentAttestationStatus(attestableEvent)
const canViewAttestation =
attested &&
attestationEvent != null &&
pubkey != null &&
recipientPubkey != null &&
hexPubkeysEqual(pubkey, recipientPubkey)
const menuActions = useMenuActions({
event,
closeDrawer,
showSubMenuActions,
setIsRawEventDialogOpen,
setIsReportDialogOpen,
isSmallScreen,
onOpenPublicMessage,
onOpenCallInvite,
onOpenEditOrClone: (mode) => {
setEditCloneMode(mode)
setEditCloneOpen(true)
},
pinned,
onViewAttestation: canViewAttestation
? () => {
queueMicrotask(() => setIsAttestationDialogOpen(true))
}
: undefined
})
const trigger = useMemo(
() => (
<button
className="flex items-center text-muted-foreground hover:text-foreground pl-2 h-full"
onClick={() => setIsDrawerOpen(true)}
>
<Ellipsis />
</button>
),
[]
)
const menuHeader = useMemo(
() => (
<NoteOptionsMetaHeader
event={event}
allowedRelays={seenOnAllowlist}
onNavigate={closeDrawer}
inDropdown={!isSmallScreen}
/>
),
[event, seenOnAllowlist, isSmallScreen]
)
return (
<div className={className} onClick={(e) => e.stopPropagation()}>
{isSmallScreen ? (
<MobileMenu
menuActions={menuActions}
trigger={trigger}
header={menuHeader}
isDrawerOpen={isDrawerOpen}
setIsDrawerOpen={setIsDrawerOpen}
showSubMenu={showSubMenu}
activeSubMenu={activeSubMenu}
subMenuTitle={subMenuTitle}
subMenuSearchable={subMenuSearchable}
closeDrawer={closeDrawer}
goBackToMainMenu={goBackToMainMenu}
/>
) : (
<DesktopMenu menuActions={menuActions} trigger={trigger} header={menuHeader} />
)}
<RawEventDialog
event={event}
isOpen={isRawEventDialogOpen}
onClose={() => setIsRawEventDialogOpen(false)}
/>
{attestationEvent ? (
<RawEventDialog
event={attestationEvent}
isOpen={isAttestationDialogOpen}
onClose={() => setIsAttestationDialogOpen(false)}
title={t('Payment attestation')}
/>
) : null}
<ReportDialog
event={event}
isOpen={isReportDialogOpen}
closeDialog={() => setIsReportDialogOpen(false)}
/>
<EditOrCloneEventDialog
open={editCloneOpen}
onOpenChange={setEditCloneOpen}
sourceEvent={event}
mode={editCloneMode}
/>
{onPostEditorClose != null && (
<PostEditor
open={isPostEditorOpen ?? false}
setOpen={(open) => {
if (!open) onPostEditorClose()
}}
defaultContent={initialDefaultContent ?? highlightDefaultContent ?? ''}
initialHighlightData={initialHighlightData}
initialPublicMessageTo={initialPublicMessageTo ?? undefined}
/>
)}
</div>
)
}