PostRelaySelector.tsx raw

   1  import { Button } from '@/components/ui/button'
   2  import {
   3    DropdownMenu,
   4    DropdownMenuCheckboxItem,
   5    DropdownMenuContent,
   6    DropdownMenuSeparator,
   7    DropdownMenuTrigger
   8  } from '@/components/ui/dropdown-menu'
   9  import { isProtectedEvent } from '@/lib/event'
  10  import { simplifyUrl } from '@/lib/url'
  11  import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
  12  import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
  13  import client from '@/services/client.service'
  14  import { NostrEvent } from 'nostr-tools'
  15  import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'
  16  import { useTranslation } from 'react-i18next'
  17  import RelayIcon from '../RelayIcon'
  18  
  19  type TPostTargetItem =
  20    | {
  21        type: 'writeRelays'
  22      }
  23    | {
  24        type: 'relay'
  25        url: string
  26      }
  27    | {
  28        type: 'relaySet'
  29        id: string
  30        urls: string[]
  31      }
  32  
  33  export default function PostRelaySelector({
  34    parentEvent,
  35    openFrom,
  36    setIsProtectedEvent,
  37    setAdditionalRelayUrls
  38  }: {
  39    parentEvent?: NostrEvent
  40    openFrom?: string[]
  41    setIsProtectedEvent: Dispatch<SetStateAction<boolean>>
  42    setAdditionalRelayUrls: Dispatch<SetStateAction<string[]>>
  43  }) {
  44    const { t } = useTranslation()
  45    const { relayUrls } = useCurrentRelays()
  46    const { relaySets, favoriteRelays } = useFavoriteRelays()
  47    const [postTargetItems, setPostTargetItems] = useState<TPostTargetItem[]>([])
  48    const parentEventSeenOnRelays = useMemo(() => {
  49      if (!parentEvent || !isProtectedEvent(parentEvent)) {
  50        return []
  51      }
  52      return client.getSeenEventRelayUrls(parentEvent.id)
  53    }, [parentEvent])
  54    const selectableRelays = useMemo(() => {
  55      return Array.from(new Set(parentEventSeenOnRelays.concat(relayUrls).concat(favoriteRelays)))
  56    }, [parentEventSeenOnRelays, relayUrls, favoriteRelays])
  57    const description = useMemo(() => {
  58      if (postTargetItems.length === 0) {
  59        return t('No relays selected')
  60      }
  61      if (postTargetItems.length === 1) {
  62        const item = postTargetItems[0]
  63        if (item.type === 'writeRelays') {
  64          return t('Write relays')
  65        }
  66        if (item.type === 'relay') {
  67          return simplifyUrl(item.url)
  68        }
  69        if (item.type === 'relaySet') {
  70          return item.urls.length > 1
  71            ? t('{{count}} relays', { count: item.urls.length })
  72            : simplifyUrl(item.urls[0])
  73        }
  74      }
  75      const hasWriteRelays = postTargetItems.some((item) => item.type === 'writeRelays')
  76      const relayCount = postTargetItems.reduce((count, item) => {
  77        if (item.type === 'relay') {
  78          return count + 1
  79        }
  80        if (item.type === 'relaySet') {
  81          return count + item.urls.length
  82        }
  83        return count
  84      }, 0)
  85      if (hasWriteRelays) {
  86        return t('Write relays and {{count}} other relays', { count: relayCount })
  87      }
  88      return t('{{count}} relays', { count: relayCount })
  89    }, [postTargetItems])
  90  
  91    useEffect(() => {
  92      if (openFrom && openFrom.length) {
  93        setPostTargetItems(Array.from(new Set(openFrom)).map((url) => ({ type: 'relay', url })))
  94        return
  95      }
  96      if (parentEventSeenOnRelays && parentEventSeenOnRelays.length) {
  97        setPostTargetItems(parentEventSeenOnRelays.map((url) => ({ type: 'relay', url })))
  98        return
  99      }
 100      setPostTargetItems([{ type: 'writeRelays' }])
 101    }, [openFrom, parentEventSeenOnRelays])
 102  
 103    useEffect(() => {
 104      const isProtectedEvent = postTargetItems.every((item) => item.type !== 'writeRelays')
 105      const relayUrls = postTargetItems.flatMap((item) => {
 106        if (item.type === 'relay') {
 107          return [item.url]
 108        }
 109        if (item.type === 'relaySet') {
 110          return item.urls
 111        }
 112        return []
 113      })
 114  
 115      setIsProtectedEvent(isProtectedEvent)
 116      setAdditionalRelayUrls(relayUrls)
 117    }, [postTargetItems])
 118  
 119    const handleWriteRelaysCheckedChange = useCallback((checked: boolean) => {
 120      if (checked) {
 121        setPostTargetItems((prev) => [...prev, { type: 'writeRelays' }])
 122      } else {
 123        setPostTargetItems((prev) => prev.filter((item) => item.type !== 'writeRelays'))
 124      }
 125    }, [])
 126  
 127    const handleRelayCheckedChange = useCallback((checked: boolean, url: string) => {
 128      if (checked) {
 129        setPostTargetItems((prev) => [...prev, { type: 'relay', url }])
 130      } else {
 131        setPostTargetItems((prev) =>
 132          prev.filter((item) => !(item.type === 'relay' && item.url === url))
 133        )
 134      }
 135    }, [])
 136  
 137    const handleRelaySetCheckedChange = useCallback(
 138      (checked: boolean, id: string, urls: string[]) => {
 139        if (checked) {
 140          setPostTargetItems((prev) => [...prev, { type: 'relaySet', id, urls }])
 141        } else {
 142          setPostTargetItems((prev) =>
 143            prev.filter((item) => !(item.type === 'relaySet' && item.id === id))
 144          )
 145        }
 146      },
 147      []
 148    )
 149  
 150    return (
 151      <DropdownMenu>
 152        <div className="flex items-center gap-2">
 153          {t('Post to')}
 154          <DropdownMenuTrigger asChild>
 155            <Button variant="outline" className="px-2">
 156              {description}
 157            </Button>
 158          </DropdownMenuTrigger>
 159        </div>
 160        <DropdownMenuContent align="start" className="max-w-96">
 161          <DropdownMenuCheckboxItem
 162            checked={postTargetItems.some((item) => item.type === 'writeRelays')}
 163            onSelect={(e) => e.preventDefault()}
 164            onCheckedChange={handleWriteRelaysCheckedChange}
 165          >
 166            {t('Write relays')}
 167          </DropdownMenuCheckboxItem>
 168          {relaySets.length > 0 && (
 169            <>
 170              <DropdownMenuSeparator />
 171              {relaySets
 172                .filter(({ relayUrls }) => relayUrls.length)
 173                .map(({ id, name, relayUrls }) => (
 174                  <DropdownMenuCheckboxItem
 175                    key={id}
 176                    checked={postTargetItems.some(
 177                      (item) => item.type === 'relaySet' && item.id === id
 178                    )}
 179                    onSelect={(e) => e.preventDefault()}
 180                    onCheckedChange={(checked) => handleRelaySetCheckedChange(checked, id, relayUrls)}
 181                  >
 182                    <div className="truncate">
 183                      {name} ({relayUrls.length})
 184                    </div>
 185                  </DropdownMenuCheckboxItem>
 186                ))}
 187            </>
 188          )}
 189          {selectableRelays.length > 0 && (
 190            <>
 191              <DropdownMenuSeparator />
 192              {selectableRelays.map((url) => (
 193                <DropdownMenuCheckboxItem
 194                  key={url}
 195                  checked={postTargetItems.some((item) => item.type === 'relay' && item.url === url)}
 196                  onSelect={(e) => e.preventDefault()}
 197                  onCheckedChange={(checked) => handleRelayCheckedChange(checked, url)}
 198                  className="flex items-center gap-2"
 199                >
 200                  <RelayIcon url={url} />
 201                  <div className="truncate">{simplifyUrl(url)}</div>
 202                </DropdownMenuCheckboxItem>
 203              ))}
 204            </>
 205          )}
 206        </DropdownMenuContent>
 207      </DropdownMenu>
 208    )
 209  }
 210