useMenuActions.tsx raw

   1  import { Pubkey } from '@/domain'
   2  import { getNoteBech32Id, isProtectedEvent } from '@/lib/event'
   3  import { toNjump } from '@/lib/link'
   4  import { simplifyUrl } from '@/lib/url'
   5  import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
   6  import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
   7  import { useMuteList } from '@/providers/MuteListProvider'
   8  import { useNostr } from '@/providers/NostrProvider'
   9  import { usePinList } from '@/providers/PinListProvider'
  10  import client from '@/services/client.service'
  11  import {
  12    Bell,
  13    BellOff,
  14    Code,
  15    Copy,
  16    Link,
  17    Pin,
  18    PinOff,
  19    SatelliteDish,
  20    Trash2,
  21    TriangleAlert
  22  } from 'lucide-react'
  23  import { Event, kinds } from 'nostr-tools'
  24  import { useMemo } from 'react'
  25  import { useTranslation } from 'react-i18next'
  26  import { toast } from 'sonner'
  27  import RelayIcon from '../RelayIcon'
  28  
  29  export interface SubMenuAction {
  30    label: React.ReactNode
  31    onClick: () => void
  32    className?: string
  33    separator?: boolean
  34  }
  35  
  36  export interface MenuAction {
  37    icon: React.ComponentType
  38    label: string
  39    onClick?: () => void
  40    className?: string
  41    separator?: boolean
  42    subMenu?: SubMenuAction[]
  43  }
  44  
  45  interface UseMenuActionsProps {
  46    event: Event
  47    closeDrawer: () => void
  48    showSubMenuActions: (subMenu: SubMenuAction[], title: string) => void
  49    setIsRawEventDialogOpen: (open: boolean) => void
  50    setIsReportDialogOpen: (open: boolean) => void
  51    isSmallScreen: boolean
  52  }
  53  
  54  export function useMenuActions({
  55    event,
  56    closeDrawer,
  57    showSubMenuActions,
  58    setIsRawEventDialogOpen,
  59    setIsReportDialogOpen,
  60    isSmallScreen
  61  }: UseMenuActionsProps) {
  62    const { t } = useTranslation()
  63    const { pubkey, attemptDelete } = useNostr()
  64    const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays()
  65    const { relaySets, favoriteRelays } = useFavoriteRelays()
  66    const relayUrls = useMemo(() => {
  67      return Array.from(new Set(currentBrowsingRelayUrls.concat(favoriteRelays)))
  68    }, [currentBrowsingRelayUrls, favoriteRelays])
  69    const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList()
  70    const { pinnedEventHexIdSet, pin, unpin } = usePinList()
  71    const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event])
  72  
  73    const broadcastSubMenu: SubMenuAction[] = useMemo(() => {
  74      const items = []
  75      if (pubkey && event.pubkey === pubkey) {
  76        items.push({
  77          label: <div className="text-left"> {t('Optimal relays')}</div>,
  78          onClick: async () => {
  79            closeDrawer()
  80            const promise = async () => {
  81              const relays = await client.determineTargetRelays(event)
  82              if (relays?.length) {
  83                await client.publishEvent(relays, event)
  84              }
  85            }
  86            toast.promise(promise, {
  87              loading: t('Republishing...'),
  88              success: () => {
  89                return t(
  90                  "Successfully republish to optimal relays (your write relays and mentioned users' read relays)"
  91                )
  92              },
  93              error: (err) => {
  94                return t('Failed to republish to optimal relays: {{error}}', {
  95                  error: err.message
  96                })
  97              }
  98            })
  99          }
 100        })
 101      }
 102  
 103      if (relaySets.length) {
 104        items.push(
 105          ...relaySets
 106            .filter((set) => set.relayUrls.length)
 107            .map((set, index) => ({
 108              label: <div className="text-left truncate">{set.name}</div>,
 109              onClick: async () => {
 110                closeDrawer()
 111                const promise = client.publishEvent(set.relayUrls, event)
 112                toast.promise(promise, {
 113                  loading: t('Republishing...'),
 114                  success: () => {
 115                    return t('Successfully republish to relay set: {{name}}', { name: set.name })
 116                  },
 117                  error: (err) => {
 118                    return t('Failed to republish to relay set: {{name}}. Error: {{error}}', {
 119                      name: set.name,
 120                      error: err.message
 121                    })
 122                  }
 123                })
 124              },
 125              separator: index === 0
 126            }))
 127        )
 128      }
 129  
 130      if (relayUrls.length) {
 131        items.push(
 132          ...relayUrls.map((relay, index) => ({
 133            label: (
 134              <div className="flex items-center gap-2 w-full">
 135                <RelayIcon url={relay} />
 136                <div className="flex-1 truncate text-left">{simplifyUrl(relay)}</div>
 137              </div>
 138            ),
 139            onClick: async () => {
 140              closeDrawer()
 141              const promise = client.publishEvent([relay], event)
 142              toast.promise(promise, {
 143                loading: t('Republishing...'),
 144                success: () => {
 145                  return t('Successfully republish to relay: {{url}}', { url: simplifyUrl(relay) })
 146                },
 147                error: (err) => {
 148                  return t('Failed to republish to relay: {{url}}. Error: {{error}}', {
 149                    url: simplifyUrl(relay),
 150                    error: err.message
 151                  })
 152                }
 153              })
 154            },
 155            separator: index === 0
 156          }))
 157        )
 158      }
 159  
 160      return items
 161    }, [pubkey, relayUrls, relaySets])
 162  
 163    const menuActions: MenuAction[] = useMemo(() => {
 164      const actions: MenuAction[] = [
 165        {
 166          icon: Copy,
 167          label: t('Copy event ID'),
 168          onClick: () => {
 169            navigator.clipboard.writeText(getNoteBech32Id(event))
 170            closeDrawer()
 171          }
 172        },
 173        {
 174          icon: Copy,
 175          label: t('Copy user ID'),
 176          onClick: () => {
 177            navigator.clipboard.writeText(Pubkey.tryFromString(event.pubkey)?.npub ?? '')
 178            closeDrawer()
 179          }
 180        },
 181        {
 182          icon: Link,
 183          label: t('Copy share link'),
 184          onClick: () => {
 185            navigator.clipboard.writeText(toNjump(getNoteBech32Id(event)))
 186            closeDrawer()
 187          }
 188        },
 189        {
 190          icon: Code,
 191          label: t('View raw event'),
 192          onClick: () => {
 193            closeDrawer()
 194            setIsRawEventDialogOpen(true)
 195          },
 196          separator: true
 197        }
 198      ]
 199  
 200      const isProtected = isProtectedEvent(event)
 201      if (!isProtected || event.pubkey === pubkey) {
 202        actions.push({
 203          icon: SatelliteDish,
 204          label: t('Republish to ...'),
 205          onClick: isSmallScreen
 206            ? () => showSubMenuActions(broadcastSubMenu, t('Republish to ...'))
 207            : undefined,
 208          subMenu: isSmallScreen ? undefined : broadcastSubMenu,
 209          separator: true
 210        })
 211      }
 212  
 213      if (event.pubkey === pubkey && event.kind === kinds.ShortTextNote) {
 214        const pinned = pinnedEventHexIdSet.has(event.id)
 215        actions.push({
 216          icon: pinned ? PinOff : Pin,
 217          label: pinned ? t('Unpin from profile') : t('Pin to profile'),
 218          onClick: async () => {
 219            closeDrawer()
 220            await (pinned ? unpin(event) : pin(event))
 221          }
 222        })
 223      }
 224  
 225      if (pubkey && event.pubkey !== pubkey) {
 226        actions.push({
 227          icon: TriangleAlert,
 228          label: t('Report'),
 229          className: 'text-destructive focus:text-destructive',
 230          onClick: () => {
 231            closeDrawer()
 232            setIsReportDialogOpen(true)
 233          },
 234          separator: true
 235        })
 236      }
 237  
 238      if (pubkey && event.pubkey !== pubkey) {
 239        if (isMuted) {
 240          actions.push({
 241            icon: Bell,
 242            label: t('Unmute user'),
 243            onClick: () => {
 244              closeDrawer()
 245              unmutePubkey(event.pubkey)
 246            },
 247            className: 'text-destructive focus:text-destructive',
 248            separator: true
 249          })
 250        } else {
 251          actions.push(
 252            {
 253              icon: BellOff,
 254              label: t('Mute user privately'),
 255              onClick: () => {
 256                closeDrawer()
 257                mutePubkeyPrivately(event.pubkey)
 258              },
 259              className: 'text-destructive focus:text-destructive',
 260              separator: true
 261            },
 262            {
 263              icon: BellOff,
 264              label: t('Mute user publicly'),
 265              onClick: () => {
 266                closeDrawer()
 267                mutePubkeyPublicly(event.pubkey)
 268              },
 269              className: 'text-destructive focus:text-destructive'
 270            }
 271          )
 272        }
 273      }
 274  
 275      if (pubkey && event.pubkey === pubkey) {
 276        actions.push({
 277          icon: Trash2,
 278          label: t('Try deleting this note'),
 279          onClick: () => {
 280            closeDrawer()
 281            attemptDelete(event)
 282          },
 283          className: 'text-destructive focus:text-destructive',
 284          separator: true
 285        })
 286      }
 287  
 288      return actions
 289    }, [
 290      t,
 291      event,
 292      pubkey,
 293      isMuted,
 294      isSmallScreen,
 295      broadcastSubMenu,
 296      pinnedEventHexIdSet,
 297      closeDrawer,
 298      showSubMenuActions,
 299      setIsRawEventDialogOpen,
 300      mutePubkeyPrivately,
 301      mutePubkeyPublicly,
 302      unmutePubkey
 303    ])
 304  
 305    return menuActions
 306  }
 307