25 changed files with 418 additions and 201 deletions
@ -0,0 +1,209 @@
@@ -0,0 +1,209 @@
|
||||
import { Button } from '@/components/ui/button' |
||||
import { |
||||
DropdownMenu, |
||||
DropdownMenuCheckboxItem, |
||||
DropdownMenuContent, |
||||
DropdownMenuSeparator, |
||||
DropdownMenuTrigger |
||||
} from '@/components/ui/dropdown-menu' |
||||
import { isProtectedEvent } from '@/lib/event' |
||||
import { simplifyUrl } from '@/lib/url' |
||||
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' |
||||
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' |
||||
import client from '@/services/client.service' |
||||
import { NostrEvent } from 'nostr-tools' |
||||
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import RelayIcon from '../RelayIcon' |
||||
|
||||
type TPostTargetItem = |
||||
| { |
||||
type: 'writeRelays' |
||||
} |
||||
| { |
||||
type: 'relay' |
||||
url: string |
||||
} |
||||
| { |
||||
type: 'relaySet' |
||||
id: string |
||||
urls: string[] |
||||
} |
||||
|
||||
export default function PostRelaySelector({ |
||||
parentEvent, |
||||
openFrom, |
||||
setIsProtectedEvent, |
||||
setAdditionalRelayUrls |
||||
}: { |
||||
parentEvent?: NostrEvent |
||||
openFrom?: string[] |
||||
setIsProtectedEvent: Dispatch<SetStateAction<boolean>> |
||||
setAdditionalRelayUrls: Dispatch<SetStateAction<string[]>> |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const { relayUrls } = useCurrentRelays() |
||||
const { relaySets, favoriteRelays } = useFavoriteRelays() |
||||
const [postTargetItems, setPostTargetItems] = useState<TPostTargetItem[]>([]) |
||||
const parentEventSeenOnRelays = useMemo(() => { |
||||
if (!parentEvent || !isProtectedEvent(parentEvent)) { |
||||
return [] |
||||
} |
||||
return client.getSeenEventRelayUrls(parentEvent.id) |
||||
}, [parentEvent]) |
||||
const selectableRelays = useMemo(() => { |
||||
return Array.from(new Set(parentEventSeenOnRelays.concat(relayUrls).concat(favoriteRelays))) |
||||
}, [parentEventSeenOnRelays, relayUrls, favoriteRelays]) |
||||
const description = useMemo(() => { |
||||
if (postTargetItems.length === 0) { |
||||
return t('No relays selected') |
||||
} |
||||
if (postTargetItems.length === 1) { |
||||
const item = postTargetItems[0] |
||||
if (item.type === 'writeRelays') { |
||||
return t('Write relays') |
||||
} |
||||
if (item.type === 'relay') { |
||||
return simplifyUrl(item.url) |
||||
} |
||||
if (item.type === 'relaySet') { |
||||
return item.urls.length > 1 |
||||
? t('{{count}} relays', { count: item.urls.length }) |
||||
: simplifyUrl(item.urls[0]) |
||||
} |
||||
} |
||||
const hasWriteRelays = postTargetItems.some((item) => item.type === 'writeRelays') |
||||
const relayCount = postTargetItems.reduce((count, item) => { |
||||
if (item.type === 'relay') { |
||||
return count + 1 |
||||
} |
||||
if (item.type === 'relaySet') { |
||||
return count + item.urls.length |
||||
} |
||||
return count |
||||
}, 0) |
||||
if (hasWriteRelays) { |
||||
return t('Write relays and {{count}} other relays', { count: relayCount }) |
||||
} |
||||
return t('{{count}} relays', { count: relayCount }) |
||||
}, [postTargetItems]) |
||||
|
||||
useEffect(() => { |
||||
if (openFrom && openFrom.length) { |
||||
setPostTargetItems(Array.from(new Set(openFrom)).map((url) => ({ type: 'relay', url }))) |
||||
return |
||||
} |
||||
if (parentEventSeenOnRelays && parentEventSeenOnRelays.length) { |
||||
setPostTargetItems(parentEventSeenOnRelays.map((url) => ({ type: 'relay', url }))) |
||||
return |
||||
} |
||||
setPostTargetItems([{ type: 'writeRelays' }]) |
||||
}, [openFrom, parentEventSeenOnRelays]) |
||||
|
||||
useEffect(() => { |
||||
const isProtectedEvent = postTargetItems.every((item) => item.type !== 'writeRelays') |
||||
const relayUrls = postTargetItems.flatMap((item) => { |
||||
if (item.type === 'relay') { |
||||
return [item.url] |
||||
} |
||||
if (item.type === 'relaySet') { |
||||
return item.urls |
||||
} |
||||
return [] |
||||
}) |
||||
|
||||
setIsProtectedEvent(isProtectedEvent) |
||||
setAdditionalRelayUrls(relayUrls) |
||||
}, [postTargetItems]) |
||||
|
||||
const handleWriteRelaysCheckedChange = useCallback((checked: boolean) => { |
||||
if (checked) { |
||||
setPostTargetItems((prev) => [...prev, { type: 'writeRelays' }]) |
||||
} else { |
||||
setPostTargetItems((prev) => prev.filter((item) => item.type !== 'writeRelays')) |
||||
} |
||||
}, []) |
||||
|
||||
const handleRelayCheckedChange = useCallback((checked: boolean, url: string) => { |
||||
if (checked) { |
||||
setPostTargetItems((prev) => [...prev, { type: 'relay', url }]) |
||||
} else { |
||||
setPostTargetItems((prev) => |
||||
prev.filter((item) => !(item.type === 'relay' && item.url === url)) |
||||
) |
||||
} |
||||
}, []) |
||||
|
||||
const handleRelaySetCheckedChange = useCallback( |
||||
(checked: boolean, id: string, urls: string[]) => { |
||||
if (checked) { |
||||
setPostTargetItems((prev) => [...prev, { type: 'relaySet', id, urls }]) |
||||
} else { |
||||
setPostTargetItems((prev) => |
||||
prev.filter((item) => !(item.type === 'relaySet' && item.id === id)) |
||||
) |
||||
} |
||||
}, |
||||
[] |
||||
) |
||||
|
||||
return ( |
||||
<DropdownMenu> |
||||
<div className="flex items-center gap-2"> |
||||
{t('Post to')} |
||||
<DropdownMenuTrigger asChild> |
||||
<Button variant="outline" className="px-2"> |
||||
{description} |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
</div> |
||||
<DropdownMenuContent align="start" className="max-w-96"> |
||||
<DropdownMenuCheckboxItem |
||||
checked={postTargetItems.some((item) => item.type === 'writeRelays')} |
||||
onSelect={(e) => e.preventDefault()} |
||||
onCheckedChange={handleWriteRelaysCheckedChange} |
||||
> |
||||
{t('Write relays')} |
||||
</DropdownMenuCheckboxItem> |
||||
{relaySets.length > 0 && ( |
||||
<> |
||||
<DropdownMenuSeparator /> |
||||
{relaySets |
||||
.filter(({ relayUrls }) => relayUrls.length) |
||||
.map(({ id, name, relayUrls }) => ( |
||||
<DropdownMenuCheckboxItem |
||||
key={id} |
||||
checked={postTargetItems.some( |
||||
(item) => item.type === 'relaySet' && item.id === id |
||||
)} |
||||
onSelect={(e) => e.preventDefault()} |
||||
onCheckedChange={(checked) => handleRelaySetCheckedChange(checked, id, relayUrls)} |
||||
> |
||||
<div className="truncate"> |
||||
{name} ({relayUrls.length}) |
||||
</div> |
||||
</DropdownMenuCheckboxItem> |
||||
))} |
||||
</> |
||||
)} |
||||
{selectableRelays.length > 0 && ( |
||||
<> |
||||
<DropdownMenuSeparator /> |
||||
{selectableRelays.map((url) => ( |
||||
<DropdownMenuCheckboxItem |
||||
key={url} |
||||
checked={postTargetItems.some((item) => item.type === 'relay' && item.url === url)} |
||||
onSelect={(e) => e.preventDefault()} |
||||
onCheckedChange={(checked) => handleRelayCheckedChange(checked, url)} |
||||
className="flex items-center gap-2" |
||||
> |
||||
<RelayIcon url={url} /> |
||||
<div className="truncate">{simplifyUrl(url)}</div> |
||||
</DropdownMenuCheckboxItem> |
||||
))} |
||||
</> |
||||
)} |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
) |
||||
} |
||||
@ -1,79 +0,0 @@
@@ -1,79 +0,0 @@
|
||||
import { Label } from '@/components/ui/label' |
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' |
||||
import { Switch } from '@/components/ui/switch' |
||||
import { isProtectedEvent } from '@/lib/event' |
||||
import { simplifyUrl } from '@/lib/url' |
||||
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' |
||||
import client from '@/services/client.service' |
||||
import { Info } from 'lucide-react' |
||||
import { Event } from 'nostr-tools' |
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
|
||||
export default function SendOnlyToSwitch({ |
||||
parentEvent, |
||||
specifiedRelayUrls, |
||||
setSpecifiedRelayUrls, |
||||
openFrom |
||||
}: { |
||||
parentEvent?: Event |
||||
specifiedRelayUrls?: string[] |
||||
setSpecifiedRelayUrls: Dispatch<SetStateAction<string[] | undefined>> |
||||
openFrom?: string[] |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const { currentRelayUrls } = useCurrentRelays() |
||||
const [urls, setUrls] = useState<string[]>([]) |
||||
|
||||
useEffect(() => { |
||||
if (openFrom?.length) { |
||||
setUrls(openFrom) |
||||
setSpecifiedRelayUrls(openFrom) |
||||
return |
||||
} |
||||
if (!parentEvent) { |
||||
setUrls(currentRelayUrls) |
||||
return |
||||
} |
||||
const isProtected = isProtectedEvent(parentEvent) |
||||
const seenOn = client.getSeenEventRelayUrls(parentEvent.id) |
||||
if (isProtected && seenOn.length) { |
||||
setSpecifiedRelayUrls(seenOn) |
||||
setUrls(seenOn) |
||||
} else { |
||||
setUrls(currentRelayUrls) |
||||
} |
||||
}, [parentEvent, currentRelayUrls, openFrom]) |
||||
|
||||
if (!urls.length) return null |
||||
|
||||
return ( |
||||
<div className="flex items-center gap-2"> |
||||
<div className="flex items-center gap-1 truncate"> |
||||
<Label htmlFor="send-only-to-current-relays" className="truncate"> |
||||
{urls.length === 1 |
||||
? t('Send only to r', { r: simplifyUrl(urls[0]) }) |
||||
: t('Send only to these relays')} |
||||
</Label> |
||||
{urls.length > 1 && ( |
||||
<Popover> |
||||
<PopoverTrigger> |
||||
<Info size={14} /> |
||||
</PopoverTrigger> |
||||
<PopoverContent className="w-fit text-sm"> |
||||
{urls.map((url) => ( |
||||
<div key={url}>{simplifyUrl(url)}</div> |
||||
))} |
||||
</PopoverContent> |
||||
</Popover> |
||||
)} |
||||
</div> |
||||
<Switch |
||||
className="shrink-0" |
||||
id="send-only-to-current-relays" |
||||
checked={!!specifiedRelayUrls} |
||||
onCheckedChange={(checked) => setSpecifiedRelayUrls(checked ? urls : undefined)} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue