RepostButton.tsx raw
1 import { Button } from '@/components/ui/button'
2 import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
3 import {
4 DropdownMenu,
5 DropdownMenuContent,
6 DropdownMenuItem,
7 DropdownMenuTrigger
8 } from '@/components/ui/dropdown-menu'
9 import { useStuffStatsById } from '@/hooks/useStuffStatsById'
10 import { useStuff } from '@/hooks/useStuff'
11 import { createRepostDraftEvent } from '@/lib/draft-event'
12 import { getNoteBech32Id } from '@/lib/event'
13 import { cn } from '@/lib/utils'
14 import { useNostr } from '@/providers/NostrProvider'
15 import { useScreenSize } from '@/providers/ScreenSizeProvider'
16 import { useUserTrust } from '@/providers/UserTrustProvider'
17 import stuffStatsService from '@/services/stuff-stats.service'
18 import { Loader, PencilLine, Repeat } from 'lucide-react'
19 import { Event } from 'nostr-tools'
20 import { useMemo, useState } from 'react'
21 import { useTranslation } from 'react-i18next'
22 import PostEditor from '../PostEditor'
23 import KeyboardShortcut from './KeyboardShortcut'
24 import { formatCount } from './utils'
25
26 export default function RepostButton({ stuff }: { stuff: Event | string }) {
27 const { t } = useTranslation()
28 const { isSmallScreen } = useScreenSize()
29 const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
30 const { publish, checkLogin, pubkey } = useNostr()
31 const { event, stuffKey } = useStuff(stuff)
32 const noteStats = useStuffStatsById(stuffKey)
33 const [reposting, setReposting] = useState(false)
34 const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
35 const [isDrawerOpen, setIsDrawerOpen] = useState(false)
36 const { repostCount, hasReposted } = useMemo(() => {
37 // external content
38 if (!event) return { repostCount: 0, hasReposted: false }
39
40 return {
41 repostCount: hideUntrustedInteractions
42 ? noteStats?.reposts?.filter((repost) => isUserTrusted(repost.pubkey)).length
43 : noteStats?.reposts?.length,
44 hasReposted: pubkey ? noteStats?.repostPubkeySet?.has(pubkey) : false
45 }
46 }, [noteStats, event, hideUntrustedInteractions])
47 const canRepost = !hasReposted && !reposting && !!event
48
49 const repost = async () => {
50 checkLogin(async () => {
51 if (!canRepost || !pubkey) return
52
53 setReposting(true)
54 const timer = setTimeout(() => setReposting(false), 5000)
55
56 try {
57 const hasReposted = noteStats?.repostPubkeySet?.has(pubkey)
58 if (hasReposted) return
59 if (!noteStats?.updatedAt) {
60 const noteStats = await stuffStatsService.fetchStuffStats(stuff, pubkey)
61 if (noteStats.repostPubkeySet?.has(pubkey)) {
62 return
63 }
64 }
65
66 const repost = createRepostDraftEvent(event)
67 const evt = await publish(repost)
68 stuffStatsService.updateStuffStatsByEvents([evt])
69 } catch (error) {
70 console.error('repost failed', error)
71 } finally {
72 setReposting(false)
73 clearTimeout(timer)
74 }
75 })
76 }
77
78 const trigger = (
79 <button
80 className={cn(
81 'flex gap-1 items-center px-3 h-full enabled:hover:text-lime-500 disabled:text-muted-foreground/40 group',
82 hasReposted ? 'text-lime-500' : 'text-muted-foreground'
83 )}
84 disabled={!event}
85 title={t('Repost (p) / Quote (q)')}
86 data-action="repost"
87 onClick={() => {
88 if (!event) return
89
90 if (isSmallScreen) {
91 setIsDrawerOpen(true)
92 }
93 }}
94 >
95 <span className="relative">
96 {reposting ? <Loader className="animate-spin" /> : <Repeat />}
97 <KeyboardShortcut shortcut="p" />
98 </span>
99 {!!repostCount && <div className="text-sm">{formatCount(repostCount)}</div>}
100 </button>
101 )
102
103 if (!event) {
104 return trigger
105 }
106
107 const postEditor = (
108 <PostEditor
109 open={isPostDialogOpen}
110 setOpen={setIsPostDialogOpen}
111 defaultContent={'\nnostr:' + getNoteBech32Id(event)}
112 />
113 )
114
115 // Hidden button for keyboard shortcut (q for quote)
116 const quoteButton = (
117 <button
118 className="hidden"
119 data-action="quote"
120 onClick={(e) => {
121 e.stopPropagation()
122 checkLogin(() => {
123 setIsPostDialogOpen(true)
124 })
125 }}
126 />
127 )
128
129 if (isSmallScreen) {
130 return (
131 <>
132 {trigger}
133 {quoteButton}
134 <Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
135 <DrawerOverlay onClick={() => setIsDrawerOpen(false)} />
136 <DrawerContent hideOverlay>
137 <div className="py-2">
138 <Button
139 onClick={(e) => {
140 e.stopPropagation()
141 setIsDrawerOpen(false)
142 repost()
143 }}
144 disabled={!canRepost}
145 className="w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5"
146 variant="ghost"
147 >
148 <Repeat /> {t('Repost')}
149 </Button>
150 <Button
151 onClick={(e) => {
152 e.stopPropagation()
153 setIsDrawerOpen(false)
154 checkLogin(() => {
155 setIsPostDialogOpen(true)
156 })
157 }}
158 className="w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5"
159 variant="ghost"
160 >
161 <PencilLine /> {t('Quote')}
162 </Button>
163 </div>
164 </DrawerContent>
165 </Drawer>
166 {postEditor}
167 </>
168 )
169 }
170
171 return (
172 <>
173 <DropdownMenu>
174 <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
175 <DropdownMenuContent>
176 <DropdownMenuItem
177 onClick={(e) => {
178 e.stopPropagation()
179 repost()
180 }}
181 disabled={!canRepost}
182 >
183 <Repeat /> {t('Repost')}
184 </DropdownMenuItem>
185 <DropdownMenuItem
186 onClick={(e) => {
187 e.stopPropagation()
188 checkLogin(() => {
189 setIsPostDialogOpen(true)
190 })
191 }}
192 >
193 <PencilLine /> {t('Quote')}
194 </DropdownMenuItem>
195 </DropdownMenuContent>
196 </DropdownMenu>
197 {quoteButton}
198 {postEditor}
199 </>
200 )
201 }
202