Files
smesh/src/components/StuffStats/RepostButton.tsx
woikos 08f75a902d Release v0.4.1
Auto-search after QR scan in search bar.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 20:38:28 +01:00

202 lines
6.1 KiB
TypeScript

import { Button } from '@/components/ui/button'
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import { useStuff } from '@/hooks/useStuff'
import { createRepostDraftEvent } from '@/lib/draft-event'
import { getNoteBech32Id } from '@/lib/event'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import stuffStatsService from '@/services/stuff-stats.service'
import { Loader, PencilLine, Repeat } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PostEditor from '../PostEditor'
import KeyboardShortcut from './KeyboardShortcut'
import { formatCount } from './utils'
export default function RepostButton({ stuff }: { stuff: Event | string }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const { publish, checkLogin, pubkey } = useNostr()
const { event, stuffKey } = useStuff(stuff)
const noteStats = useStuffStatsById(stuffKey)
const [reposting, setReposting] = useState(false)
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const { repostCount, hasReposted } = useMemo(() => {
// external content
if (!event) return { repostCount: 0, hasReposted: false }
return {
repostCount: hideUntrustedInteractions
? noteStats?.reposts?.filter((repost) => isUserTrusted(repost.pubkey)).length
: noteStats?.reposts?.length,
hasReposted: pubkey ? noteStats?.repostPubkeySet?.has(pubkey) : false
}
}, [noteStats, event, hideUntrustedInteractions])
const canRepost = !hasReposted && !reposting && !!event
const repost = async () => {
checkLogin(async () => {
if (!canRepost || !pubkey) return
setReposting(true)
const timer = setTimeout(() => setReposting(false), 5000)
try {
const hasReposted = noteStats?.repostPubkeySet?.has(pubkey)
if (hasReposted) return
if (!noteStats?.updatedAt) {
const noteStats = await stuffStatsService.fetchStuffStats(stuff, pubkey)
if (noteStats.repostPubkeySet?.has(pubkey)) {
return
}
}
const repost = createRepostDraftEvent(event)
const evt = await publish(repost)
stuffStatsService.updateStuffStatsByEvents([evt])
} catch (error) {
console.error('repost failed', error)
} finally {
setReposting(false)
clearTimeout(timer)
}
})
}
const trigger = (
<button
className={cn(
'flex gap-1 items-center px-3 h-full enabled:hover:text-lime-500 disabled:text-muted-foreground/40 group',
hasReposted ? 'text-lime-500' : 'text-muted-foreground'
)}
disabled={!event}
title={t('Repost (p) / Quote (q)')}
data-action="repost"
onClick={() => {
if (!event) return
if (isSmallScreen) {
setIsDrawerOpen(true)
}
}}
>
<span className="relative">
{reposting ? <Loader className="animate-spin" /> : <Repeat />}
<KeyboardShortcut shortcut="p" />
</span>
{!!repostCount && <div className="text-sm">{formatCount(repostCount)}</div>}
</button>
)
if (!event) {
return trigger
}
const postEditor = (
<PostEditor
open={isPostDialogOpen}
setOpen={setIsPostDialogOpen}
defaultContent={'\nnostr:' + getNoteBech32Id(event)}
/>
)
// Hidden button for keyboard shortcut (q for quote)
const quoteButton = (
<button
className="hidden"
data-action="quote"
onClick={(e) => {
e.stopPropagation()
checkLogin(() => {
setIsPostDialogOpen(true)
})
}}
/>
)
if (isSmallScreen) {
return (
<>
{trigger}
{quoteButton}
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
<DrawerOverlay onClick={() => setIsDrawerOpen(false)} />
<DrawerContent hideOverlay>
<div className="py-2">
<Button
onClick={(e) => {
e.stopPropagation()
setIsDrawerOpen(false)
repost()
}}
disabled={!canRepost}
className="w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5"
variant="ghost"
>
<Repeat /> {t('Repost')}
</Button>
<Button
onClick={(e) => {
e.stopPropagation()
setIsDrawerOpen(false)
checkLogin(() => {
setIsPostDialogOpen(true)
})
}}
className="w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5"
variant="ghost"
>
<PencilLine /> {t('Quote')}
</Button>
</div>
</DrawerContent>
</Drawer>
{postEditor}
</>
)
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
repost()
}}
disabled={!canRepost}
>
<Repeat /> {t('Repost')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
checkLogin(() => {
setIsPostDialogOpen(true)
})
}}
>
<PencilLine /> {t('Quote')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{quoteButton}
{postEditor}
</>
)
}