feat: sync relay sets
This commit is contained in:
@@ -3,15 +3,16 @@ import { simplifyUrl } from '@/lib/url'
|
||||
import { SecondaryPageLink } from '@/PageManager'
|
||||
import { useFeed } from '@/providers/FeedProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
||||
import { Circle, CircleCheck } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RelaySetCard from '../RelaySetCard'
|
||||
|
||||
export default function FeedSwitcher({ close }: { close?: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const { feedType, setFeedType } = useFeed()
|
||||
const { feedType, switchFeed, activeRelaySetId, temporaryRelayUrls } = useFeed()
|
||||
const { pubkey } = useNostr()
|
||||
const { relayGroups, temporaryRelayUrls, switchRelayGroup } = useRelaySettings()
|
||||
const { relaySets } = useRelaySets()
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
@@ -20,14 +21,14 @@ export default function FeedSwitcher({ close }: { close?: () => void }) {
|
||||
itemName={t('Following')}
|
||||
isActive={feedType === 'following'}
|
||||
onClick={() => {
|
||||
setFeedType('following')
|
||||
switchFeed('following')
|
||||
close?.()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between px-2">
|
||||
<div className="text-muted-foreground text-sm font-semibold">{t('relay feeds')}</div>
|
||||
<div className="text-muted-foreground text-sm font-semibold">{t('relay sets')}</div>
|
||||
<SecondaryPageLink
|
||||
to={toRelaySettings()}
|
||||
className="text-highlight text-sm font-semibold"
|
||||
@@ -42,25 +43,25 @@ export default function FeedSwitcher({ close }: { close?: () => void }) {
|
||||
itemName={
|
||||
temporaryRelayUrls.length === 1 ? simplifyUrl(temporaryRelayUrls[0]) : t('Temporary')
|
||||
}
|
||||
isActive={feedType === 'relays'}
|
||||
isActive={feedType === 'temporary'}
|
||||
temporary
|
||||
onClick={() => {
|
||||
setFeedType('relays')
|
||||
switchFeed('temporary')
|
||||
close?.()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{relayGroups
|
||||
.filter((group) => group.relayUrls.length > 0)
|
||||
.map((group) => (
|
||||
<FeedSwitcherItem
|
||||
key={group.groupName}
|
||||
itemName={
|
||||
group.relayUrls.length === 1 ? simplifyUrl(group.relayUrls[0]) : group.groupName
|
||||
}
|
||||
isActive={feedType === 'relays' && group.isActive && temporaryRelayUrls.length === 0}
|
||||
onClick={() => {
|
||||
switchRelayGroup(group.groupName)
|
||||
{relaySets
|
||||
.filter((set) => set.relayUrls.length > 0)
|
||||
.map((set) => (
|
||||
<RelaySetCard
|
||||
key={set.id}
|
||||
relaySet={set}
|
||||
select={feedType === 'relays' && set.id === activeRelaySetId}
|
||||
showConnectionStatus={feedType === 'relays' && set.id === activeRelaySetId}
|
||||
onSelectChange={(select) => {
|
||||
if (!select) return
|
||||
switchFeed('relays', { activeRelaySetId: set.id })
|
||||
close?.()
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -22,12 +22,7 @@ export default function LoginDialog({
|
||||
if (isSmallScreen) {
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerContent
|
||||
className="max-h-[90vh]"
|
||||
style={{
|
||||
paddingBottom: 'env(safe-area-inset-bottom)'
|
||||
}}
|
||||
>
|
||||
<DrawerContent className="max-h-[90vh]">
|
||||
<div className="flex flex-col p-4 gap-4 overflow-auto">
|
||||
<AccountManager close={() => setOpen(false)} />
|
||||
</div>
|
||||
|
||||
124
src/components/RelaySetCard/index.tsx
Normal file
124
src/components/RelaySetCard/index.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import client from '@/services/client.service'
|
||||
import { TRelaySet } from '@/types'
|
||||
import { ChevronDown, Circle, CircleCheck } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function RelaySetCard({
|
||||
relaySet,
|
||||
select,
|
||||
onSelectChange,
|
||||
showConnectionStatus = false
|
||||
}: {
|
||||
relaySet: TRelaySet
|
||||
select: boolean
|
||||
onSelectChange: (select: boolean) => void
|
||||
showConnectionStatus?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [expand, setExpand] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full border rounded-lg p-4 ${select ? 'border-highlight bg-highlight/5' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div
|
||||
className="flex space-x-2 items-center cursor-pointer"
|
||||
onClick={() => onSelectChange(!select)}
|
||||
>
|
||||
<RelaySetActiveToggle select={select} />
|
||||
<div className="h-8 font-semibold flex items-center select-none">{relaySet.name}</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<RelayUrlsExpandToggle expand={expand} onExpandChange={setExpand}>
|
||||
{t('n relays', { n: relaySet.relayUrls.length })}
|
||||
</RelayUrlsExpandToggle>
|
||||
</div>
|
||||
</div>
|
||||
{expand && (
|
||||
<RelayUrls urls={relaySet.relayUrls} showConnectionStatus={showConnectionStatus} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RelaySetActiveToggle({ select }: { select: boolean }) {
|
||||
return select ? (
|
||||
<CircleCheck size={18} className="text-highlight shrink-0" />
|
||||
) : (
|
||||
<Circle size={18} className="shrink-0" />
|
||||
)
|
||||
}
|
||||
|
||||
function RelayUrlsExpandToggle({
|
||||
children,
|
||||
expand,
|
||||
onExpandChange
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
expand: boolean
|
||||
onExpandChange: (expand: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="text-sm text-muted-foreground flex items-center gap-1 cursor-pointer hover:text-foreground"
|
||||
onClick={() => onExpandChange(!expand)}
|
||||
>
|
||||
<div className="select-none">{children}</div>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={`transition-transform duration-200 ${expand ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RelayUrls({
|
||||
showConnectionStatus = false,
|
||||
urls
|
||||
}: {
|
||||
showConnectionStatus?: boolean
|
||||
urls: string[]
|
||||
}) {
|
||||
const [relays, setRelays] = useState<
|
||||
{
|
||||
url: string
|
||||
isConnected: boolean
|
||||
}[]
|
||||
>(urls.map((url) => ({ url, isConnected: false })) ?? [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showConnectionStatus || urls.length === 0) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const connectionStatusMap = client.listConnectionStatus()
|
||||
setRelays((pre) => {
|
||||
return pre.map((relay) => {
|
||||
const isConnected = connectionStatusMap.get(relay.url) || false
|
||||
return { ...relay, isConnected }
|
||||
})
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [showConnectionStatus, urls])
|
||||
|
||||
if (!urls) return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
{relays.map(({ url, isConnected: isConnected }, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
{showConnectionStatus &&
|
||||
(isConnected ? (
|
||||
<div className="text-green-500 text-xs">●</div>
|
||||
) : (
|
||||
<div className="text-red-500 text-xs">●</div>
|
||||
))}
|
||||
<div className="text-muted-foreground text-sm">{url}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
168
src/components/RelaySetsSetting/PullFromRelaysButton.tsx
Normal file
168
src/components/RelaySetsSetting/PullFromRelaysButton.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
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 { BIG_RELAY_URLS } from '@/constants'
|
||||
import { tagNameEquals } from '@/lib/tag'
|
||||
import { isWebsocketUrl, simplifyUrl } from '@/lib/url'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { TRelaySet } from '@/types'
|
||||
import { CloudDownload } from 'lucide-react'
|
||||
import { kinds } from 'nostr-tools'
|
||||
import { useEffect, useState } from 'react'
|
||||
import RelaySetCard from '../RelaySetCard'
|
||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
||||
|
||||
export default function PullFromRelaysButton() {
|
||||
const { pubkey } = useNostr()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const trigger = (
|
||||
<Button variant="secondary" className="w-full" disabled={!pubkey}>
|
||||
<CloudDownload />
|
||||
Pull from 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>Select the relay sets you want to pull</DrawerTitle>
|
||||
<DrawerDescription className="hidden" />
|
||||
</DrawerHeader>
|
||||
<RemoteRelaySets close={() => setOpen(false)} />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select the relay sets you want to pull</DialogTitle>
|
||||
<DialogDescription className="hidden" />
|
||||
</DialogHeader>
|
||||
<RemoteRelaySets close={() => setOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function RemoteRelaySets({ close }: { close?: () => void }) {
|
||||
const { pubkey, relayList } = useNostr()
|
||||
const { mergeRelaySets } = useRelaySets()
|
||||
const [initialed, setInitialed] = useState(false)
|
||||
const [relaySets, setRelaySets] = useState<TRelaySet[]>([])
|
||||
const [selectedRelaySetIds, setSelectedRelaySetIds] = useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!pubkey) return
|
||||
|
||||
const init = async () => {
|
||||
setInitialed(false)
|
||||
const events = await client.fetchEvents(
|
||||
(relayList?.write ?? []).concat(BIG_RELAY_URLS).slice(0, 4),
|
||||
{
|
||||
kinds: [kinds.Relaysets],
|
||||
authors: [pubkey],
|
||||
limit: 50
|
||||
}
|
||||
)
|
||||
setRelaySets(
|
||||
events
|
||||
.map((evt) => {
|
||||
const id = evt.tags.find(tagNameEquals('d'))?.[1]
|
||||
if (!id) return null
|
||||
|
||||
const relayUrls = evt.tags
|
||||
.filter(tagNameEquals('relay'))
|
||||
.map((tag) => tag[1])
|
||||
.filter((url) => url && isWebsocketUrl(url))
|
||||
if (!relayUrls.length) return null
|
||||
|
||||
let title = evt.tags.find(tagNameEquals('title'))?.[1]
|
||||
if (!title) {
|
||||
title = relayUrls.length === 1 ? simplifyUrl(relayUrls[0]) : id
|
||||
}
|
||||
return { id, name: title, relayUrls }
|
||||
})
|
||||
.filter(Boolean) as TRelaySet[]
|
||||
)
|
||||
setInitialed(true)
|
||||
}
|
||||
init()
|
||||
}, [pubkey])
|
||||
|
||||
if (!pubkey) return null
|
||||
if (!initialed) return <div className="text-center text-muted-foreground">Loading...</div>
|
||||
if (!relaySets.length) {
|
||||
return <div className="text-center text-muted-foreground">No relay sets found</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
{relaySets.map((relaySet) => (
|
||||
<RelaySetCard
|
||||
key={relaySet.id}
|
||||
relaySet={relaySet}
|
||||
select={selectedRelaySetIds.includes(relaySet.id)}
|
||||
onSelectChange={(select) => {
|
||||
if (select) {
|
||||
setSelectedRelaySetIds([...selectedRelaySetIds, relaySet.id])
|
||||
} else {
|
||||
setSelectedRelaySetIds(selectedRelaySetIds.filter((id) => id !== relaySet.id))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
className="w-20 shrink-0"
|
||||
variant="secondary"
|
||||
onClick={() => setSelectedRelaySetIds(relaySets.map((r) => r.id))}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={!selectedRelaySetIds.length}
|
||||
onClick={() => {
|
||||
if (selectedRelaySetIds.length > 0) {
|
||||
mergeRelaySets(relaySets.filter((set) => selectedRelaySetIds.includes(set.id)))
|
||||
close?.()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedRelaySetIds.length > 0
|
||||
? `Pull ${selectedRelaySetIds.length} relay sets`
|
||||
: 'Pull'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
43
src/components/RelaySetsSetting/PushToRelaysButton.tsx
Normal file
43
src/components/RelaySetsSetting/PushToRelaysButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useToast } from '@/hooks'
|
||||
import { createRelaySetDraftEvent } from '@/lib/draft-event'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
||||
import { CloudUpload, Loader } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useRelaySetsSettingComponent } from './provider'
|
||||
|
||||
export default function PushToRelaysButton() {
|
||||
const { toast } = useToast()
|
||||
const { pubkey, publish } = useNostr()
|
||||
const { relaySets } = useRelaySets()
|
||||
const { selectedRelaySetIds } = useRelaySetsSettingComponent()
|
||||
const [pushing, setPushing] = useState(false)
|
||||
|
||||
const push = async () => {
|
||||
const selectedRelaySets = relaySets.filter((r) => selectedRelaySetIds.includes(r.id))
|
||||
if (!selectedRelaySets.length) return
|
||||
|
||||
setPushing(true)
|
||||
const draftEvents = selectedRelaySets.map((relaySet) => createRelaySetDraftEvent(relaySet))
|
||||
await Promise.allSettled(draftEvents.map((event) => publish(event)))
|
||||
toast({
|
||||
title: 'Push Successful',
|
||||
description: 'Successfully pushed relay sets to relays'
|
||||
})
|
||||
setPushing(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
disabled={!pubkey || pushing || selectedRelaySetIds.length === 0}
|
||||
onClick={push}
|
||||
>
|
||||
<CloudUpload />
|
||||
Push to relays
|
||||
{pushing && <Loader className="animate-spin" />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
176
src/components/RelaySetsSetting/RelaySet.tsx
Normal file
176
src/components/RelaySetsSetting/RelaySet.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
||||
import { TRelaySet } from '@/types'
|
||||
import { Check, ChevronDown, Circle, CircleCheck, EllipsisVertical } from 'lucide-react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RelayUrls from './RelayUrl'
|
||||
import { useRelaySetsSettingComponent } from './provider'
|
||||
|
||||
export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) {
|
||||
const { t } = useTranslation()
|
||||
const { expandedRelaySetId, selectedRelaySetIds } = useRelaySetsSettingComponent()
|
||||
const isSelected = useMemo(
|
||||
() => selectedRelaySetIds.includes(relaySet.id),
|
||||
[selectedRelaySetIds, relaySet.id]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full border rounded-lg p-4 ${isSelected ? 'border-highlight bg-highlight/5' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex space-x-2 items-center">
|
||||
<RelaySetActiveToggle relaySetId={relaySet.id} />
|
||||
<RelaySetName relaySet={relaySet} />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<RelayUrlsExpandToggle relaySetId={relaySet.id}>
|
||||
{t('n relays', { n: relaySet.relayUrls.length })}
|
||||
</RelayUrlsExpandToggle>
|
||||
<RelaySetOptions relaySet={relaySet} />
|
||||
</div>
|
||||
</div>
|
||||
{expandedRelaySetId === relaySet.id && <RelayUrls relaySetId={relaySet.id} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RelaySetActiveToggle({ relaySetId }: { relaySetId: string }) {
|
||||
const { selectedRelaySetIds, toggleSelectedRelaySetId } = useRelaySetsSettingComponent()
|
||||
const isSelected = useMemo(
|
||||
() => selectedRelaySetIds.includes(relaySetId),
|
||||
[selectedRelaySetIds, relaySetId]
|
||||
)
|
||||
|
||||
const handleClick = () => {
|
||||
toggleSelectedRelaySetId(relaySetId)
|
||||
}
|
||||
|
||||
return isSelected ? (
|
||||
<CircleCheck
|
||||
size={18}
|
||||
className="text-highlight shrink-0 cursor-pointer"
|
||||
onClick={handleClick}
|
||||
/>
|
||||
) : (
|
||||
<Circle
|
||||
size={18}
|
||||
className="text-muted-foreground shrink-0 cursor-pointer hover:text-foreground"
|
||||
onClick={handleClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RelaySetName({ relaySet }: { relaySet: TRelaySet }) {
|
||||
const [newSetName, setNewSetName] = useState(relaySet.name)
|
||||
const { updateRelaySet } = useRelaySets()
|
||||
const { renamingRelaySetId, setRenamingRelaySetId, toggleSelectedRelaySetId } =
|
||||
useRelaySetsSettingComponent()
|
||||
|
||||
const saveNewRelaySetName = () => {
|
||||
if (relaySet.name === newSetName) {
|
||||
return setRenamingRelaySetId(null)
|
||||
}
|
||||
updateRelaySet({ ...relaySet, name: newSetName })
|
||||
setRenamingRelaySetId(null)
|
||||
}
|
||||
|
||||
const handleRenameInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewSetName(e.target.value)
|
||||
}
|
||||
|
||||
const handleRenameInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveNewRelaySetName()
|
||||
}
|
||||
}
|
||||
|
||||
return renamingRelaySetId === relaySet.id ? (
|
||||
<div className="flex gap-1 items-center">
|
||||
<Input
|
||||
value={newSetName}
|
||||
onChange={handleRenameInputChange}
|
||||
onBlur={saveNewRelaySetName}
|
||||
onKeyDown={handleRenameInputKeyDown}
|
||||
className="font-semibold w-28"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={saveNewRelaySetName}>
|
||||
<Check size={18} className="text-green-500" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="h-8 font-semibold flex items-center cursor-pointer select-none"
|
||||
onClick={() => toggleSelectedRelaySetId(relaySet.id)}
|
||||
>
|
||||
{relaySet.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RelayUrlsExpandToggle({
|
||||
relaySetId,
|
||||
children
|
||||
}: {
|
||||
relaySetId: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const { expandedRelaySetId, setExpandedRelaySetId } = useRelaySetsSettingComponent()
|
||||
return (
|
||||
<div
|
||||
className="text-sm text-muted-foreground flex items-center gap-1 cursor-pointer hover:text-foreground"
|
||||
onClick={() => setExpandedRelaySetId((pre) => (pre === relaySetId ? null : relaySetId))}
|
||||
>
|
||||
<div className="select-none">{children}</div>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={`transition-transform duration-200 ${expandedRelaySetId === relaySetId ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RelaySetOptions({ relaySet }: { relaySet: TRelaySet }) {
|
||||
const { t } = useTranslation()
|
||||
const { deleteRelaySet } = useRelaySets()
|
||||
const { setRenamingRelaySetId } = useRelaySetsSettingComponent()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<EllipsisVertical />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => setRenamingRelaySetId(relaySet.id)}>
|
||||
{t('Rename')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`https://jumble.social/?${relaySet.relayUrls.map((url) => 'r=' + url).join('&')}`
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('Copy share link')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => deleteRelaySet(relaySet.id)}
|
||||
>
|
||||
{t('Delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -2,31 +2,30 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useFetchRelayInfos } from '@/hooks'
|
||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { useFeed } from '@/providers/FeedProvider'
|
||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { CircleX, SearchCheck } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function RelayUrls({ groupName }: { groupName: string }) {
|
||||
export default function RelayUrls({ relaySetId }: { relaySetId: string }) {
|
||||
const { t } = useTranslation()
|
||||
const { relayGroups, updateRelayGroupRelayUrls } = useRelaySettings()
|
||||
const isActive = useMemo(
|
||||
() => relayGroups.find((group) => group.groupName === groupName)?.isActive ?? false,
|
||||
[relayGroups, groupName]
|
||||
)
|
||||
const { relaySets, updateRelaySet } = useRelaySets()
|
||||
const { activeRelaySetId } = useFeed()
|
||||
const [newRelayUrl, setNewRelayUrl] = useState('')
|
||||
const [newRelayUrlError, setNewRelayUrlError] = useState<string | null>(null)
|
||||
const relaySet = useMemo(
|
||||
() => relaySets.find((r) => r.id === relaySetId),
|
||||
[relaySets, relaySetId]
|
||||
)
|
||||
const [relays, setRelays] = useState<
|
||||
{
|
||||
url: string
|
||||
isConnected: boolean
|
||||
}[]
|
||||
>(
|
||||
relayGroups
|
||||
.find((group) => group.groupName === groupName)
|
||||
?.relayUrls.map((url) => ({ url, isConnected: false })) ?? []
|
||||
)
|
||||
>(relaySet?.relayUrls.map((url) => ({ url, isConnected: false })) ?? [])
|
||||
const isActive = relaySet?.id === activeRelaySetId
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
@@ -42,12 +41,14 @@ export default function RelayUrls({ groupName }: { groupName: string }) {
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (!relaySet) return null
|
||||
|
||||
const removeRelayUrl = (url: string) => {
|
||||
setRelays((relays) => relays.filter((relay) => relay.url !== url))
|
||||
updateRelayGroupRelayUrls(
|
||||
groupName,
|
||||
relays.map(({ url }) => url).filter((u) => u !== url)
|
||||
)
|
||||
updateRelaySet({
|
||||
...relaySet,
|
||||
relayUrls: relays.map(({ url }) => url).filter((u) => u !== url)
|
||||
})
|
||||
}
|
||||
|
||||
const saveNewRelayUrl = () => {
|
||||
@@ -61,7 +62,7 @@ export default function RelayUrls({ groupName }: { groupName: string }) {
|
||||
}
|
||||
setRelays((pre) => [...pre, { url: normalizedUrl, isConnected: false }])
|
||||
const newRelayUrls = [...relays.map(({ url }) => url), normalizedUrl]
|
||||
updateRelayGroupRelayUrls(groupName, newRelayUrls)
|
||||
updateRelaySet({ ...relaySet, relayUrls: newRelayUrls })
|
||||
setNewRelayUrl('')
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useFetchRelayInfos } from '@/hooks'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { simplifyUrl } from '@/lib/url'
|
||||
import { useFeed } from '@/providers/FeedProvider'
|
||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
||||
import client from '@/services/client.service'
|
||||
import { Save, SearchCheck } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function TemporaryRelayGroup() {
|
||||
export default function TemporaryRelaySet() {
|
||||
const { t } = useTranslation()
|
||||
const { temporaryRelayUrls, relayGroups, addRelayGroup, switchRelayGroup } = useRelaySettings()
|
||||
const { temporaryRelayUrls, switchFeed } = useFeed()
|
||||
const { addRelaySet } = useRelaySets()
|
||||
const [relays, setRelays] = useState<
|
||||
{
|
||||
url: string
|
||||
@@ -40,15 +43,10 @@ export default function TemporaryRelayGroup() {
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const existingTemporaryIndexes = relayGroups
|
||||
.filter((group) => /^Temporary \d+$/.test(group.groupName))
|
||||
.map((group) => group.groupName.split(' ')[1])
|
||||
.map(Number)
|
||||
.filter((index) => !isNaN(index))
|
||||
const nextIndex = Math.max(...existingTemporaryIndexes, 0) + 1
|
||||
const groupName = `Temporary ${nextIndex}`
|
||||
addRelayGroup(groupName, temporaryRelayUrls)
|
||||
switchRelayGroup(groupName)
|
||||
const relaySetName =
|
||||
temporaryRelayUrls.length === 1 ? simplifyUrl(temporaryRelayUrls[0]) : 'Temporary'
|
||||
const id = addRelaySet(relaySetName, temporaryRelayUrls)
|
||||
switchFeed('relays', { activeRelaySetId: id })
|
||||
}
|
||||
|
||||
return (
|
||||
76
src/components/RelaySetsSetting/index.tsx
Normal file
76
src/components/RelaySetsSetting/index.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RelaySetsSettingComponentProvider } from './provider'
|
||||
import RelaySet from './RelaySet'
|
||||
import TemporaryRelaySet from './TemporaryRelaySet'
|
||||
import PushToRelaysButton from './PushToRelaysButton'
|
||||
import PullFromRelaysButton from './PullFromRelaysButton'
|
||||
|
||||
export default function RelaySetsSetting() {
|
||||
const { t } = useTranslation()
|
||||
const { relaySets, addRelaySet } = useRelaySets()
|
||||
const [newRelaySetName, setNewRelaySetName] = useState('')
|
||||
const dummyRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (dummyRef.current) {
|
||||
dummyRef.current.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const saveRelaySet = () => {
|
||||
if (!newRelaySetName) return
|
||||
addRelaySet(newRelaySetName)
|
||||
}
|
||||
|
||||
const handleNewRelaySetNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewRelaySetName(e.target.value)
|
||||
}
|
||||
|
||||
const handleNewRelaySetNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveRelaySet()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RelaySetsSettingComponentProvider>
|
||||
<div ref={dummyRef} tabIndex={-1} style={{ position: 'absolute', opacity: 0 }}></div>
|
||||
<div className="flex gap-4">
|
||||
<PushToRelaysButton />
|
||||
<PullFromRelaysButton />
|
||||
</div>
|
||||
<div className="space-y-2 mt-4">
|
||||
<TemporaryRelaySet />
|
||||
{relaySets.map((relaySet) => (
|
||||
<RelaySet key={relaySet.id} relaySet={relaySet} />
|
||||
))}
|
||||
</div>
|
||||
{relaySets.length < 10 && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
<div className="w-full border rounded-lg p-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="font-semibold">{t('Add a new relay set')}</div>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<Input
|
||||
placeholder={t('Relay set name')}
|
||||
value={newRelaySetName}
|
||||
onChange={handleNewRelaySetNameChange}
|
||||
onKeyDown={handleNewRelaySetNameKeyDown}
|
||||
onBlur={saveRelaySet}
|
||||
/>
|
||||
<Button onClick={saveRelaySet}>{t('Add')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</RelaySetsSettingComponentProvider>
|
||||
)
|
||||
}
|
||||
52
src/components/RelaySetsSetting/provider.tsx
Normal file
52
src/components/RelaySetsSetting/provider.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
|
||||
type TRelaySetsSettingComponentContext = {
|
||||
renamingRelaySetId: string | null
|
||||
setRenamingRelaySetId: React.Dispatch<React.SetStateAction<string | null>>
|
||||
expandedRelaySetId: string | null
|
||||
setExpandedRelaySetId: React.Dispatch<React.SetStateAction<string | null>>
|
||||
selectedRelaySetIds: string[]
|
||||
toggleSelectedRelaySetId: (relaySetId: string) => void
|
||||
}
|
||||
|
||||
export const RelaySetsSettingComponentContext = createContext<
|
||||
TRelaySetsSettingComponentContext | undefined
|
||||
>(undefined)
|
||||
|
||||
export const useRelaySetsSettingComponent = () => {
|
||||
const context = useContext(RelaySetsSettingComponentContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useRelaySetsSettingComponent must be used within a RelaySetsSettingComponentProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function RelaySetsSettingComponentProvider({ children }: { children: React.ReactNode }) {
|
||||
const [renamingRelaySetId, setRenamingRelaySetId] = useState<string | null>(null)
|
||||
const [expandedRelaySetId, setExpandedRelaySetId] = useState<string | null>(null)
|
||||
const [selectedRelaySetIds, setSelectedRelaySetIds] = useState<string[]>([])
|
||||
|
||||
return (
|
||||
<RelaySetsSettingComponentContext.Provider
|
||||
value={{
|
||||
renamingRelaySetId,
|
||||
setRenamingRelaySetId,
|
||||
expandedRelaySetId,
|
||||
setExpandedRelaySetId,
|
||||
selectedRelaySetIds,
|
||||
toggleSelectedRelaySetId: (relaySetId) => {
|
||||
setSelectedRelaySetIds((pre) => {
|
||||
if (pre.includes(relaySetId)) {
|
||||
return pre.filter((id) => id !== relaySetId)
|
||||
}
|
||||
return [...pre, relaySetId]
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RelaySetsSettingComponentContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { Check, ChevronDown, Circle, CircleCheck, EllipsisVertical } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import RelayUrls from './RelayUrl'
|
||||
import { useRelaySettingsComponent } from './provider'
|
||||
import { TRelayGroup } from './types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function RelayGroup({ group }: { group: TRelayGroup }) {
|
||||
const { t } = useTranslation()
|
||||
const { expandedRelayGroup } = useRelaySettingsComponent()
|
||||
const { temporaryRelayUrls } = useRelaySettings()
|
||||
const { groupName, relayUrls } = group
|
||||
const isActive = temporaryRelayUrls.length === 0 && group.isActive
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full border rounded-lg p-4 ${isActive ? 'border-highlight bg-highlight/5' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex space-x-2 items-center">
|
||||
<RelayGroupActiveToggle
|
||||
groupName={groupName}
|
||||
isActive={isActive}
|
||||
canActive={relayUrls.length > 0}
|
||||
/>
|
||||
<RelayGroupName groupName={groupName} />
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<RelayUrlsExpandToggle groupName={groupName}>
|
||||
{t('n relays', { n: relayUrls.length })}
|
||||
</RelayUrlsExpandToggle>
|
||||
<RelayGroupOptions group={group} />
|
||||
</div>
|
||||
</div>
|
||||
{expandedRelayGroup === groupName && <RelayUrls groupName={groupName} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RelayGroupActiveToggle({
|
||||
groupName,
|
||||
isActive,
|
||||
canActive
|
||||
}: {
|
||||
groupName: string
|
||||
isActive: boolean
|
||||
canActive: boolean
|
||||
}) {
|
||||
const { switchRelayGroup } = useRelaySettings()
|
||||
|
||||
return isActive ? (
|
||||
<CircleCheck size={18} className="text-highlight shrink-0" />
|
||||
) : (
|
||||
<Circle
|
||||
size={18}
|
||||
className={`text-muted-foreground shrink-0 ${canActive ? 'cursor-pointer hover:text-foreground ' : ''}`}
|
||||
onClick={() => {
|
||||
if (canActive) {
|
||||
switchRelayGroup(groupName)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RelayGroupName({ groupName }: { groupName: string }) {
|
||||
const { t } = useTranslation()
|
||||
const [newGroupName, setNewGroupName] = useState(groupName)
|
||||
const [newNameError, setNewNameError] = useState<string | null>(null)
|
||||
const { relayGroups, switchRelayGroup, renameRelayGroup } = useRelaySettings()
|
||||
const { renamingGroup, setRenamingGroup } = useRelaySettingsComponent()
|
||||
|
||||
const hasRelayUrls = relayGroups.find((group) => group.groupName === groupName)?.relayUrls.length
|
||||
|
||||
const saveNewGroupName = () => {
|
||||
if (groupName === newGroupName) {
|
||||
return setRenamingGroup(null)
|
||||
}
|
||||
if (relayGroups.find((group) => group.groupName === newGroupName)) {
|
||||
return setNewNameError(t('relay collection name already exists'))
|
||||
}
|
||||
const errMsg = renameRelayGroup(groupName, newGroupName)
|
||||
if (errMsg) {
|
||||
setNewNameError(errMsg)
|
||||
return
|
||||
}
|
||||
setRenamingGroup(null)
|
||||
}
|
||||
|
||||
const handleRenameInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewGroupName(e.target.value)
|
||||
setNewNameError(null)
|
||||
}
|
||||
|
||||
const handleRenameInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveNewGroupName()
|
||||
}
|
||||
}
|
||||
|
||||
return renamingGroup === groupName ? (
|
||||
<div className="flex gap-1 items-center">
|
||||
<Input
|
||||
value={newGroupName}
|
||||
onChange={handleRenameInputChange}
|
||||
onBlur={saveNewGroupName}
|
||||
onKeyDown={handleRenameInputKeyDown}
|
||||
className={`font-semibold w-28 ${newNameError ? 'border-destructive' : ''}`}
|
||||
/>
|
||||
<Button variant="ghost" size="icon" onClick={saveNewGroupName}>
|
||||
<Check size={18} className="text-green-500" />
|
||||
</Button>
|
||||
{newNameError && <div className="text-xs text-destructive">{newNameError}</div>}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`h-8 font-semibold flex items-center ${hasRelayUrls ? 'cursor-pointer' : 'text-muted-foreground'}`}
|
||||
onClick={() => {
|
||||
if (hasRelayUrls) {
|
||||
switchRelayGroup(groupName)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{groupName}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RelayUrlsExpandToggle({
|
||||
groupName,
|
||||
children
|
||||
}: {
|
||||
groupName: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const { expandedRelayGroup, setExpandedRelayGroup } = useRelaySettingsComponent()
|
||||
return (
|
||||
<div
|
||||
className="text-sm text-muted-foreground flex items-center gap-1 cursor-pointer hover:text-foreground"
|
||||
onClick={() => setExpandedRelayGroup((pre) => (pre === groupName ? null : groupName))}
|
||||
>
|
||||
<div className="select-none">{children}</div>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={`transition-transform duration-200 ${expandedRelayGroup === groupName ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RelayGroupOptions({ group }: { group: TRelayGroup }) {
|
||||
const { t } = useTranslation()
|
||||
const { deleteRelayGroup } = useRelaySettings()
|
||||
const { setRenamingGroup } = useRelaySettingsComponent()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<EllipsisVertical />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={() => setRenamingGroup(group.groupName)}>
|
||||
{t('Rename')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`https://jumble.social/?${group.relayUrls.map((url) => 'r=' + url).join('&')}`
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('Copy share link')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
onClick={() => deleteRelayGroup(group.groupName)}
|
||||
>
|
||||
{t('Delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { RelaySettingsComponentProvider } from './provider'
|
||||
import RelayGroup from './RelayGroup'
|
||||
import TemporaryRelayGroup from './TemporaryRelayGroup'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function RelaySettings({ hideTitle = false }: { hideTitle?: boolean }) {
|
||||
const { t } = useTranslation()
|
||||
const { relayGroups, addRelayGroup } = useRelaySettings()
|
||||
const [newGroupName, setNewGroupName] = useState('')
|
||||
const [newNameError, setNewNameError] = useState<string | null>(null)
|
||||
const dummyRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (dummyRef.current) {
|
||||
dummyRef.current.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const saveRelayGroup = () => {
|
||||
if (relayGroups.find((group) => group.groupName === newGroupName)) {
|
||||
return setNewNameError(t('relay collection name already exists'))
|
||||
}
|
||||
const errMsg = addRelayGroup(newGroupName)
|
||||
if (errMsg) {
|
||||
return setNewNameError(errMsg)
|
||||
}
|
||||
setNewGroupName('')
|
||||
}
|
||||
|
||||
const handleNewGroupNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewGroupName(e.target.value)
|
||||
setNewNameError(null)
|
||||
}
|
||||
|
||||
const handleNewGroupNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveRelayGroup()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RelaySettingsComponentProvider>
|
||||
<div ref={dummyRef} tabIndex={-1} style={{ position: 'absolute', opacity: 0 }}></div>
|
||||
{!hideTitle && <div className="text-lg font-semibold mb-4">{t('Relay Settings')}</div>}
|
||||
<div className="space-y-2">
|
||||
<TemporaryRelayGroup />
|
||||
{relayGroups.map((group, index) => (
|
||||
<RelayGroup key={index} group={group} />
|
||||
))}
|
||||
</div>
|
||||
{relayGroups.length < 10 && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
<div className="w-full border rounded-lg p-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="font-semibold">{t('Add a new relay collection')}</div>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<Input
|
||||
className={newNameError ? 'border-destructive' : ''}
|
||||
placeholder={t('Relay collection name')}
|
||||
value={newGroupName}
|
||||
onChange={handleNewGroupNameChange}
|
||||
onKeyDown={handleNewGroupNameKeyDown}
|
||||
onBlur={saveRelayGroup}
|
||||
/>
|
||||
<Button onClick={saveRelayGroup}>{t('Add')}</Button>
|
||||
</div>
|
||||
{newNameError && <div className="text-xs text-destructive mt-1">{newNameError}</div>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</RelaySettingsComponentProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
|
||||
type TRelaySettingsComponentContext = {
|
||||
renamingGroup: string | null
|
||||
setRenamingGroup: React.Dispatch<React.SetStateAction<string | null>>
|
||||
expandedRelayGroup: string | null
|
||||
setExpandedRelayGroup: React.Dispatch<React.SetStateAction<string | null>>
|
||||
}
|
||||
|
||||
export const RelaySettingsComponentContext = createContext<
|
||||
TRelaySettingsComponentContext | undefined
|
||||
>(undefined)
|
||||
|
||||
export const useRelaySettingsComponent = () => {
|
||||
const context = useContext(RelaySettingsComponentContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useRelaySettingsComponent must be used within a RelaySettingsComponentProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function RelaySettingsComponentProvider({ children }: { children: React.ReactNode }) {
|
||||
const [renamingGroup, setRenamingGroup] = useState<string | null>(null)
|
||||
const [expandedRelayGroup, setExpandedRelayGroup] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<RelaySettingsComponentContext.Provider
|
||||
value={{
|
||||
renamingGroup,
|
||||
setRenamingGroup,
|
||||
expandedRelayGroup,
|
||||
setExpandedRelayGroup
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RelaySettingsComponentContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export type TRelayGroup = {
|
||||
groupName: string
|
||||
relayUrls: string[]
|
||||
isActive: boolean
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { CommandDialog, CommandInput, CommandItem, CommandList } from '@/compone
|
||||
import { useSearchProfiles } from '@/hooks'
|
||||
import { toNote, toNoteList, toProfile, toProfileList } from '@/lib/link'
|
||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||
import { useRelaySettings } from '@/providers/RelaySettingsProvider'
|
||||
import { TProfile } from '@/types'
|
||||
import { Hash, Notebook, UserRound } from 'lucide-react'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
@@ -83,12 +82,6 @@ export function SearchDialog({ open, setOpen }: { open: boolean; setOpen: Dispat
|
||||
}
|
||||
|
||||
function NormalItem({ search, onClick }: { search: string; onClick?: () => void }) {
|
||||
const { searchableRelayUrls } = useRelaySettings()
|
||||
|
||||
if (searchableRelayUrls.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SecondaryPageLink to={toNoteList({ search })} onClick={onClick}>
|
||||
<CommandItem>
|
||||
|
||||
Reference in New Issue
Block a user