5 changed files with 325 additions and 3 deletions
@ -0,0 +1,248 @@
@@ -0,0 +1,248 @@
|
||||
import { Button } from '@/components/ui/button' |
||||
import { |
||||
Dialog, |
||||
DialogContent, |
||||
DialogDescription, |
||||
DialogHeader, |
||||
DialogTitle, |
||||
DialogTrigger |
||||
} from '@/components/ui/dialog' |
||||
import { |
||||
Drawer, |
||||
DrawerContent, |
||||
DrawerDescription, |
||||
DrawerHeader, |
||||
DrawerTitle, |
||||
DrawerTrigger |
||||
} from '@/components/ui/drawer' |
||||
import { toProfile } from '@/lib/link' |
||||
import { useSecondaryPage } from '@/PageManager' |
||||
import { useNostr } from '@/providers/NostrProvider' |
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
||||
import client from '@/services/client.service' |
||||
import { TMailboxRelay } from '@/types' |
||||
import { ChevronDown, Circle, CircleCheck, ScanSearch } from 'lucide-react' |
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import RelayIcon from '../RelayIcon' |
||||
import { SimpleUserAvatar } from '../UserAvatar' |
||||
import { SimpleUsername } from '../Username' |
||||
|
||||
export default function CalculateOptimalReadRelaysButton({ |
||||
mergeRelays |
||||
}: { |
||||
mergeRelays: (newRelays: TMailboxRelay[]) => void |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const { isSmallScreen } = useScreenSize() |
||||
const { pubkey } = useNostr() |
||||
const [open, setOpen] = useState(false) |
||||
|
||||
const trigger = ( |
||||
<Button variant="secondary" className="w-full" disabled={!pubkey}> |
||||
<ScanSearch /> |
||||
{t('Calculate optimal read relays')} |
||||
</Button> |
||||
) |
||||
|
||||
if (isSmallScreen) { |
||||
return ( |
||||
<Drawer open={open} onOpenChange={setOpen}> |
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger> |
||||
<DrawerContent className="max-h-[90vh]"> |
||||
<div className="flex flex-col p-4 gap-4 overflow-auto"> |
||||
<DrawerHeader> |
||||
<DrawerTitle>{t('Select relays to append')}</DrawerTitle> |
||||
<DrawerDescription className="hidden" /> |
||||
</DrawerHeader> |
||||
<OptimalReadRelays close={() => setOpen(false)} mergeRelays={mergeRelays} /> |
||||
</div> |
||||
</DrawerContent> |
||||
</Drawer> |
||||
) |
||||
} |
||||
|
||||
return ( |
||||
<Dialog open={open} onOpenChange={setOpen}> |
||||
<DialogTrigger asChild>{trigger}</DialogTrigger> |
||||
<DialogContent className="max-h-[90vh] overflow-auto"> |
||||
<DialogHeader> |
||||
<DialogTitle>{t('Select relays to append')}</DialogTitle> |
||||
<DialogDescription className="hidden" /> |
||||
</DialogHeader> |
||||
<OptimalReadRelays close={() => setOpen(false)} mergeRelays={mergeRelays} /> |
||||
</DialogContent> |
||||
</Dialog> |
||||
) |
||||
} |
||||
|
||||
function OptimalReadRelays({ |
||||
close, |
||||
mergeRelays |
||||
}: { |
||||
close: () => void |
||||
mergeRelays: (newRelays: TMailboxRelay[]) => void |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const { pubkey } = useNostr() |
||||
const [isCalculating, setIsCalculating] = useState(false) |
||||
const [optimalReadRelays, setOptimalReadRelays] = useState<{ url: string; pubkeys: string[] }[]>( |
||||
[] |
||||
) |
||||
const [selectedRelayUrls, setSelectedRelayUrls] = useState<string[]>([]) |
||||
|
||||
useEffect(() => { |
||||
if (!pubkey) return |
||||
|
||||
const init = async () => { |
||||
setIsCalculating(true) |
||||
const relays = await client.calculateOptimalReadRelays(pubkey) |
||||
console.log(relays) |
||||
setOptimalReadRelays(relays) |
||||
setIsCalculating(false) |
||||
} |
||||
init() |
||||
}, []) |
||||
|
||||
if (isCalculating) { |
||||
return <div className="text-center text-sm text-muted-foreground">{t('calculating...')}</div> |
||||
} |
||||
|
||||
return ( |
||||
<div className="space-y-4"> |
||||
<div className="space-y-2"> |
||||
{optimalReadRelays.map((relay) => ( |
||||
<RelayItem |
||||
key={relay.url} |
||||
relay={relay} |
||||
close={close} |
||||
selectedRelayUrls={selectedRelayUrls} |
||||
setSelectedRelayUrls={setSelectedRelayUrls} |
||||
/> |
||||
))} |
||||
</div> |
||||
<Button |
||||
disabled={selectedRelayUrls.length === 0} |
||||
className="w-full" |
||||
onClick={() => { |
||||
mergeRelays( |
||||
selectedRelayUrls.map((url) => ({ |
||||
url, |
||||
scope: 'read' |
||||
})) |
||||
) |
||||
close() |
||||
}} |
||||
> |
||||
{selectedRelayUrls.length === 0 |
||||
? t('Append') |
||||
: t('Append n relays', { n: selectedRelayUrls.length })} |
||||
</Button> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
function RelayItem({ |
||||
relay, |
||||
close, |
||||
selectedRelayUrls, |
||||
setSelectedRelayUrls |
||||
}: { |
||||
relay: { url: string; pubkeys: string[] } |
||||
close: () => void |
||||
selectedRelayUrls: string[] |
||||
setSelectedRelayUrls: Dispatch<SetStateAction<string[]>> |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const { push } = useSecondaryPage() |
||||
const [expanded, setExpanded] = useState(false) |
||||
|
||||
const selected = selectedRelayUrls.includes(relay.url) |
||||
|
||||
return ( |
||||
<div |
||||
className={`rounded-lg p-4 border select-none cursor-pointer ${selected ? 'border-highlight bg-highlight/5' : ''}`} |
||||
onClick={() => |
||||
setSelectedRelayUrls((pre) => |
||||
pre.includes(relay.url) ? pre.filter((url) => url !== relay.url) : [...pre, relay.url] |
||||
) |
||||
} |
||||
> |
||||
<div className="flex items-center justify-between"> |
||||
<div className="flex items-center gap-2 flex-1 w-0"> |
||||
<SelectToggle |
||||
select={selectedRelayUrls.includes(relay.url)} |
||||
setSelect={(select) => { |
||||
setSelectedRelayUrls((prev) => |
||||
select ? [...prev, relay.url] : prev.filter((url) => url !== relay.url) |
||||
) |
||||
}} |
||||
/> |
||||
<RelayIcon url={relay.url} className="h-8 w-8" /> |
||||
<div className="font-semibold truncate text-lg">{relay.url}</div> |
||||
</div> |
||||
<div |
||||
className="flex items-center cursor-pointer gap-1 text-muted-foreground hover:text-foreground text-sm" |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
setExpanded((prev) => !prev) |
||||
}} |
||||
> |
||||
<div> |
||||
{relay.pubkeys.length} {t('followings')} |
||||
</div> |
||||
<ChevronDown |
||||
size={16} |
||||
className={`transition-transform duration-200 ${expanded ? 'rotate-180' : ''}`} |
||||
/> |
||||
</div> |
||||
</div> |
||||
{expanded && ( |
||||
<div className="space-y-2 pt-2 pl-7"> |
||||
{relay.pubkeys.map((pubkey) => ( |
||||
<div |
||||
key={pubkey} |
||||
className="flex cursor-pointer items-center gap-2" |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
close() |
||||
push(toProfile(pubkey)) |
||||
}} |
||||
> |
||||
<SimpleUserAvatar userId={pubkey} size="small" /> |
||||
<SimpleUsername userId={pubkey} className="font-semibold truncate" /> |
||||
</div> |
||||
))} |
||||
</div> |
||||
)} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
function SelectToggle({ |
||||
select, |
||||
setSelect |
||||
}: { |
||||
select: boolean |
||||
setSelect: (select: boolean) => void |
||||
}) { |
||||
return select ? ( |
||||
<CircleCheck |
||||
size={18} |
||||
className="text-highlight shrink-0 cursor-pointer" |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
setSelect(false) |
||||
}} |
||||
/> |
||||
) : ( |
||||
<Circle |
||||
size={18} |
||||
className="shrink-0 cursor-pointer" |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
setSelect(true) |
||||
}} |
||||
/> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue