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.
 
 
 
 

206 lines
6.5 KiB

import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import {
Drawer,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerTitle
} from '@/components/ui/drawer'
import { Textarea } from '@/components/ui/textarea'
import { ExtendedKind } from '@/constants'
import { createPublicMessageDraftEvent } from '@/lib/draft-event'
import { createFakeEvent } from '@/lib/event'
import { showSimplePublishSuccess } from '@/lib/publishing-feedback'
import { LoginRequiredError } from '@/lib/nostr-errors'
import { pubkeyToNpub } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import MarkdownArticle from '../Note/MarkdownArticle/MarkdownArticle'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
const TIP_NOTICE_DEFAULT_KEY = 'I just sent you a tip!'
function defaultTipNoticeMessage(recipientPubkey: string, tipText: string): string {
const npub = pubkeyToNpub(recipientPubkey)
return `nostr:${npub} ${tipText}`
}
export default function TipPublicMessagePrompt({
open,
onOpenChange,
recipientPubkey
}: {
open: boolean
onOpenChange: (open: boolean) => void
recipientPubkey: string | null
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { publish, checkLogin, pubkey: selfPubkey } = useNostr()
const [sending, setSending] = useState(false)
const [message, setMessage] = useState('')
const cancelRef = useRef<HTMLButtonElement>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const tipText = t(TIP_NOTICE_DEFAULT_KEY)
useEffect(() => {
if (!open || !recipientPubkey) return
setMessage(defaultTipNoticeMessage(recipientPubkey, tipText))
}, [open, recipientPubkey, tipText])
useEffect(() => {
if (!open) return
const id = requestAnimationFrame(() => {
textareaRef.current?.focus()
textareaRef.current?.setSelectionRange(
textareaRef.current.value.length,
textareaRef.current.value.length
)
})
return () => cancelAnimationFrame(id)
}, [open])
const previewEvent = useMemo(() => {
if (!recipientPubkey) return null
return createFakeEvent({
kind: ExtendedKind.PUBLIC_MESSAGE,
pubkey: selfPubkey ?? '',
content: message,
tags: [['p', recipientPubkey]]
})
}, [message, recipientPubkey, selfPubkey])
const handleSend = () => {
if (!recipientPubkey) return
const trimmed = message.trim()
if (!trimmed) return
checkLogin(async () => {
if (selfPubkey === recipientPubkey) {
onOpenChange(false)
return
}
setSending(true)
try {
const draft = await createPublicMessageDraftEvent(trimmed, [recipientPubkey], {
addClientTag: true
})
await publish(draft, { disableFallbacks: true })
showSimplePublishSuccess(t('Tip notice sent'))
onOpenChange(false)
} catch (error) {
if (error instanceof LoginRequiredError) return
toast.error(
t('Failed to send tip notice', {
error: error instanceof Error ? error.message : String(error)
})
)
} finally {
setSending(false)
}
})
}
const body = (
<div className="min-w-0">
<p className="text-sm font-medium text-foreground">{t('Tip notice success only note')}</p>
<Textarea
ref={textareaRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
disabled={sending}
rows={6}
className="mt-3 min-h-[10rem] resize-y text-sm leading-relaxed"
aria-label={t('Tip notice prompt description')}
/>
{previewEvent ? (
<div className="mt-4 min-w-0">
<p className="text-xs font-medium text-muted-foreground">{t('Preview')}</p>
<div
className={cn(
'mt-1.5 max-h-56 min-w-0 overflow-y-auto overflow-x-hidden rounded-md border border-border',
'bg-muted/25 px-3 py-2'
)}
>
<MarkdownArticle event={previewEvent} hideMetadata lazyMedia={false} className="text-sm" />
</div>
</div>
) : null}
</div>
)
const actions = (
<>
<Button
ref={cancelRef}
type="button"
variant="default"
onClick={() => onOpenChange(false)}
disabled={sending}
>
{t('Cancel')}
</Button>
<Button
type="button"
variant="outline"
onClick={handleSend}
disabled={sending || !recipientPubkey || !message.trim()}
>
{t('Send')}
</Button>
</>
)
if (!recipientPubkey) return null
if (isSmallScreen) {
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<DrawerContent className="min-w-0 overflow-hidden px-4 pb-6" onOpenAutoFocus={(e) => e.preventDefault()}>
<DrawerHeader>
<DrawerTitle className="flex min-w-0 items-center gap-2">
<span className="shrink-0">{t('Tip notice prompt title')}</span>
<UserAvatar size="small" userId={recipientPubkey} className="shrink-0" />
<Username userId={recipientPubkey} className="min-w-0 flex-1 truncate" />
</DrawerTitle>
</DrawerHeader>
<div className="px-0 pb-4">{body}</div>
<DrawerFooter className="flex-row justify-end gap-2 pt-2">{actions}</DrawerFooter>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="w-[calc(100vw-2rem)] max-w-lg min-w-0 overflow-hidden sm:max-w-lg"
onOpenAutoFocus={(e) => e.preventDefault()}
>
<DialogHeader className="min-w-0">
<DialogTitle className="flex min-w-0 items-center gap-2">
<span className="shrink-0">{t('Tip notice prompt title')}</span>
<UserAvatar size="small" userId={recipientPubkey} className="shrink-0" />
<Username userId={recipientPubkey} className="min-w-0 flex-1 truncate" />
</DialogTitle>
<DialogDescription>{t('Tip notice prompt description')}</DialogDescription>
</DialogHeader>
{body}
<DialogFooter className="gap-2 sm:gap-2">{actions}</DialogFooter>
</DialogContent>
</Dialog>
)
}