index.tsx raw

   1  import { Button } from '@/components/ui/button'
   2  import { createJoinDraftEvent, createLeaveDraftEvent } from '@/lib/draft-event'
   3  import { checkNip43Support } from '@/lib/relay'
   4  import { useNostr } from '@/providers/NostrProvider'
   5  import relayMembershipService from '@/services/relay-membership.service'
   6  import { TRelayInfo } from '@/types'
   7  import { LogIn, LogOut, Mail } from 'lucide-react'
   8  import { useEffect, useMemo, useState } from 'react'
   9  import { useTranslation } from 'react-i18next'
  10  import { toast } from 'sonner'
  11  import InviteCodeDialog from './InviteCodeDialog'
  12  import JoinDialog from './JoinDialog'
  13  
  14  interface RelayMembershipControlProps {
  15    relayInfo: TRelayInfo
  16    onMembershipStatusChange?: (status: boolean) => void
  17  }
  18  
  19  export default function RelayMembershipControl({
  20    relayInfo,
  21    onMembershipStatusChange
  22  }: RelayMembershipControlProps) {
  23    const { t } = useTranslation()
  24    const { pubkey, checkLogin, publish } = useNostr()
  25    const [isMember, setIsMember] = useState(false)
  26    const [isLoading, setIsLoading] = useState(false)
  27    const [isChecking, setIsChecking] = useState(false)
  28    const [showJoinDialog, setShowJoinDialog] = useState(false)
  29    const [showInviteCodeDialog, setShowInviteCodeDialog] = useState(false)
  30    const supportsNip43 = useMemo(() => checkNip43Support(relayInfo), [relayInfo])
  31  
  32    useEffect(() => {
  33      if (!supportsNip43 || !pubkey) {
  34        setIsMember(false)
  35        return
  36      }
  37  
  38      const checkMembership = async () => {
  39        try {
  40          setIsChecking(true)
  41          const status = await relayMembershipService.checkMembership(
  42            relayInfo.url,
  43            pubkey,
  44            relayInfo.pubkey
  45          )
  46          setIsMember(status)
  47        } finally {
  48          setIsChecking(false)
  49        }
  50      }
  51  
  52      checkMembership()
  53    }, [relayInfo.url, relayInfo.pubkey, pubkey, supportsNip43])
  54  
  55    useEffect(() => {
  56      if (onMembershipStatusChange) {
  57        onMembershipStatusChange(isMember)
  58      }
  59    }, [isMember, onMembershipStatusChange])
  60  
  61    if (!supportsNip43 || isChecking) {
  62      return null
  63    }
  64  
  65    const submitJoinRequest = async () => {
  66      setIsLoading(true)
  67      try {
  68        const draftEvent = createJoinDraftEvent('')
  69        const joinRequestEvent = await publish(draftEvent, {
  70          specifiedRelayUrls: [relayInfo.url]
  71        })
  72        toast.success(t('Join request sent successfully'))
  73        await relayMembershipService.addNewMember(relayInfo.url, joinRequestEvent.pubkey)
  74        onMembershipStatusChange?.(true)
  75      } catch {
  76        setShowJoinDialog(true)
  77      } finally {
  78        setIsLoading(false)
  79      }
  80    }
  81  
  82    const handleGetInviteCodeClick = () => {
  83      setShowInviteCodeDialog(true)
  84    }
  85  
  86    const handleLeaveClick = async () => {
  87      if (!confirm(t('Are you sure you want to leave this relay?'))) {
  88        return
  89      }
  90  
  91      setIsLoading(true)
  92      try {
  93        const draftEvent = createLeaveDraftEvent()
  94        const leaveRequestEvent = await publish(draftEvent, {
  95          specifiedRelayUrls: [relayInfo.url]
  96        })
  97        toast.success(t('Leave request sent successfully'))
  98        await relayMembershipService.removeMember(relayInfo.url, leaveRequestEvent.pubkey)
  99        setIsMember(false)
 100      } catch (error: any) {
 101        const errors = error instanceof AggregateError ? error.errors : [error]
 102        errors.forEach((err) => {
 103          toast.error(
 104            `${t('Failed to send leave request')}: ${err instanceof Error ? err.message : String(err)}`,
 105            { duration: 10_000 }
 106          )
 107          console.error(err)
 108        })
 109        return
 110      } finally {
 111        setIsLoading(false)
 112      }
 113    }
 114  
 115    return (
 116      <>
 117        {isMember ? (
 118          <div className="grid grid-cols-2 gap-2">
 119            <Button
 120              variant="secondary"
 121              className="w-full"
 122              onClick={handleGetInviteCodeClick}
 123              disabled={isLoading}
 124            >
 125              <Mail className="w-4 h-4 mr-2" />
 126              {t('Get Invite Code')}
 127            </Button>
 128            <Button
 129              variant="outline"
 130              className="w-full"
 131              onClick={handleLeaveClick}
 132              disabled={isLoading}
 133            >
 134              <LogOut className="w-4 h-4 mr-2" />
 135              {t('Leave')}
 136            </Button>
 137          </div>
 138        ) : (
 139          <Button
 140            variant="default"
 141            className="w-full"
 142            onClick={() => {
 143              checkLogin(() => submitJoinRequest())
 144            }}
 145            disabled={isLoading}
 146          >
 147            <LogIn className="w-4 h-4 mr-2" />
 148            {t('Request to Join Relay')}
 149          </Button>
 150        )}
 151  
 152        <JoinDialog
 153          relayInfo={relayInfo}
 154          showJoinDialog={showJoinDialog}
 155          setShowJoinDialog={setShowJoinDialog}
 156          onMembershipStatusChange={setIsMember}
 157        />
 158  
 159        <InviteCodeDialog
 160          relayInfo={relayInfo}
 161          showInviteCodeDialog={showInviteCodeDialog}
 162          setShowInviteCodeDialog={setShowInviteCodeDialog}
 163        />
 164      </>
 165    )
 166  }
 167