index.tsx raw

   1  import { Badge } from '@/components/ui/badge'
   2  import { Button } from '@/components/ui/button'
   3  import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
   4  import { useFetchRelayInfo } from '@/hooks'
   5  import { checkNip43Support } from '@/lib/relay'
   6  import { normalizeHttpUrl } from '@/lib/url'
   7  import { cn } from '@/lib/utils'
   8  import { useNostr } from '@/providers/NostrProvider'
   9  import { Check, Copy, GitBranch, Link, Mail, SquareCode } from 'lucide-react'
  10  import { useMemo, useState } from 'react'
  11  import { useTranslation } from 'react-i18next'
  12  import { toast } from 'sonner'
  13  import PostEditor from '../PostEditor'
  14  import RelayIcon from '../RelayIcon'
  15  import RelayMembershipControl from '../RelayMembershipControl'
  16  import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu'
  17  import UserAvatar from '../UserAvatar'
  18  import Username from '../Username'
  19  import RelayReviewsPreview from './RelayReviewsPreview'
  20  
  21  export default function RelayInfo({ url, className }: { url: string; className?: string }) {
  22    const { t } = useTranslation()
  23    const { checkLogin } = useNostr()
  24    const { relayInfo, isFetching } = useFetchRelayInfo(url)
  25    const [open, setOpen] = useState(false)
  26    const [isMember, setIsMember] = useState(false)
  27    const supportsNip43 = useMemo(() => checkNip43Support(relayInfo), [relayInfo])
  28    const shouldShowPostButton = useMemo(() => !supportsNip43 || isMember, [supportsNip43, isMember])
  29  
  30    if (isFetching || !relayInfo) {
  31      return null
  32    }
  33  
  34    return (
  35      <div className={cn('space-y-4 mb-2', className)}>
  36        <div className="px-4 space-y-4">
  37          <div className="space-y-2">
  38            <div className="flex items-center gap-2 justify-between">
  39              <div className="flex gap-2 items-center flex-1">
  40                <RelayIcon url={url} className="w-8 h-8" />
  41                <div className="text-2xl font-semibold truncate select-text flex-1 w-0">
  42                  {relayInfo.name || relayInfo.shortUrl}
  43                </div>
  44              </div>
  45              <RelayControls url={relayInfo.url} />
  46            </div>
  47            {!!relayInfo.tags?.length && (
  48              <div className="flex gap-2">
  49                {relayInfo.tags.map((tag) => (
  50                  <Badge variant="secondary">{tag}</Badge>
  51                ))}
  52              </div>
  53            )}
  54            {relayInfo.description && (
  55              <div className="text-wrap break-words whitespace-pre-wrap mt-2 select-text">
  56                {relayInfo.description}
  57              </div>
  58            )}
  59          </div>
  60  
  61          <div className="space-y-2">
  62            <div className="text-sm font-semibold text-muted-foreground">{t('Homepage')}</div>
  63            <a
  64              href={normalizeHttpUrl(relayInfo.url)}
  65              target="_blank"
  66              className="hover:underline text-primary select-text truncate block w-fit max-w-full"
  67            >
  68              {normalizeHttpUrl(relayInfo.url)}
  69            </a>
  70          </div>
  71  
  72          <ScrollArea className="overflow-x-auto">
  73            <div className="flex gap-8 pb-2">
  74              {relayInfo.pubkey && (
  75                <div className="space-y-2 w-fit">
  76                  <div className="text-sm font-semibold text-muted-foreground">{t('Operator')}</div>
  77                  <div className="flex gap-2 items-center">
  78                    <UserAvatar userId={relayInfo.pubkey} size="small" />
  79                    <Username userId={relayInfo.pubkey} className="font-semibold text-nowrap" />
  80                  </div>
  81                </div>
  82              )}
  83              {relayInfo.contact && (
  84                <div className="space-y-2 w-fit">
  85                  <div className="text-sm font-semibold text-muted-foreground">{t('Contact')}</div>
  86                  <div className="flex gap-2 items-center font-semibold select-text text-nowrap">
  87                    <Mail />
  88                    {relayInfo.contact}
  89                  </div>
  90                </div>
  91              )}
  92              {relayInfo.software && (
  93                <div className="space-y-2 w-fit">
  94                  <div className="text-sm font-semibold text-muted-foreground">{t('Software')}</div>
  95                  <div className="flex gap-2 items-center font-semibold select-text text-nowrap">
  96                    <SquareCode />
  97                    {formatSoftware(relayInfo.software)}
  98                  </div>
  99                </div>
 100              )}
 101              {relayInfo.version && (
 102                <div className="space-y-2 w-fit">
 103                  <div className="text-sm font-semibold text-muted-foreground">{t('Version')}</div>
 104                  <div className="flex gap-2 items-center font-semibold select-text text-nowrap">
 105                    <GitBranch />
 106                    {relayInfo.version}
 107                  </div>
 108                </div>
 109              )}
 110            </div>
 111            <ScrollBar orientation="horizontal" />
 112          </ScrollArea>
 113          <RelayMembershipControl relayInfo={relayInfo} onMembershipStatusChange={setIsMember} />
 114          {shouldShowPostButton && (
 115            <>
 116              <Button
 117                variant="secondary"
 118                className="w-full"
 119                onClick={() => checkLogin(() => setOpen(true))}
 120              >
 121                {t('Share something on this Relay')}
 122              </Button>
 123              <PostEditor open={open} setOpen={setOpen} />
 124            </>
 125          )}
 126        </div>
 127        <RelayReviewsPreview relayUrl={url} />
 128      </div>
 129    )
 130  }
 131  
 132  function formatSoftware(software: string) {
 133    const parts = software.split('/')
 134    return parts[parts.length - 1]
 135  }
 136  
 137  function RelayControls({ url }: { url: string }) {
 138    const [copiedUrl, setCopiedUrl] = useState(false)
 139    const [copiedShareableUrl, setCopiedShareableUrl] = useState(false)
 140  
 141    const handleCopyUrl = () => {
 142      navigator.clipboard.writeText(url)
 143      setCopiedUrl(true)
 144      setTimeout(() => setCopiedUrl(false), 2000)
 145    }
 146  
 147    const handleCopyShareableUrl = () => {
 148      navigator.clipboard.writeText(`https://smesh.mleku.dev/?r=${url}`)
 149      setCopiedShareableUrl(true)
 150      toast.success('Shareable URL copied to clipboard')
 151      setTimeout(() => setCopiedShareableUrl(false), 2000)
 152    }
 153  
 154    return (
 155      <div className="flex items-center gap-1">
 156        <Button variant="ghost" size="titlebar-icon" onClick={handleCopyShareableUrl}>
 157          {copiedShareableUrl ? <Check /> : <Link />}
 158        </Button>
 159        <Button variant="ghost" size="titlebar-icon" onClick={handleCopyUrl}>
 160          {copiedUrl ? <Check /> : <Copy />}
 161        </Button>
 162        <SaveRelayDropdownMenu urls={[url]} bigButton />
 163      </div>
 164    )
 165  }
 166