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