PostRelaySelector.tsx raw
1 import { Button } from '@/components/ui/button'
2 import {
3 DropdownMenu,
4 DropdownMenuCheckboxItem,
5 DropdownMenuContent,
6 DropdownMenuSeparator,
7 DropdownMenuTrigger
8 } from '@/components/ui/dropdown-menu'
9 import { isProtectedEvent } from '@/lib/event'
10 import { simplifyUrl } from '@/lib/url'
11 import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
12 import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
13 import client from '@/services/client.service'
14 import { NostrEvent } from 'nostr-tools'
15 import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react'
16 import { useTranslation } from 'react-i18next'
17 import RelayIcon from '../RelayIcon'
18
19 type TPostTargetItem =
20 | {
21 type: 'writeRelays'
22 }
23 | {
24 type: 'relay'
25 url: string
26 }
27 | {
28 type: 'relaySet'
29 id: string
30 urls: string[]
31 }
32
33 export default function PostRelaySelector({
34 parentEvent,
35 openFrom,
36 setIsProtectedEvent,
37 setAdditionalRelayUrls
38 }: {
39 parentEvent?: NostrEvent
40 openFrom?: string[]
41 setIsProtectedEvent: Dispatch<SetStateAction<boolean>>
42 setAdditionalRelayUrls: Dispatch<SetStateAction<string[]>>
43 }) {
44 const { t } = useTranslation()
45 const { relayUrls } = useCurrentRelays()
46 const { relaySets, favoriteRelays } = useFavoriteRelays()
47 const [postTargetItems, setPostTargetItems] = useState<TPostTargetItem[]>([])
48 const parentEventSeenOnRelays = useMemo(() => {
49 if (!parentEvent || !isProtectedEvent(parentEvent)) {
50 return []
51 }
52 return client.getSeenEventRelayUrls(parentEvent.id)
53 }, [parentEvent])
54 const selectableRelays = useMemo(() => {
55 return Array.from(new Set(parentEventSeenOnRelays.concat(relayUrls).concat(favoriteRelays)))
56 }, [parentEventSeenOnRelays, relayUrls, favoriteRelays])
57 const description = useMemo(() => {
58 if (postTargetItems.length === 0) {
59 return t('No relays selected')
60 }
61 if (postTargetItems.length === 1) {
62 const item = postTargetItems[0]
63 if (item.type === 'writeRelays') {
64 return t('Write relays')
65 }
66 if (item.type === 'relay') {
67 return simplifyUrl(item.url)
68 }
69 if (item.type === 'relaySet') {
70 return item.urls.length > 1
71 ? t('{{count}} relays', { count: item.urls.length })
72 : simplifyUrl(item.urls[0])
73 }
74 }
75 const hasWriteRelays = postTargetItems.some((item) => item.type === 'writeRelays')
76 const relayCount = postTargetItems.reduce((count, item) => {
77 if (item.type === 'relay') {
78 return count + 1
79 }
80 if (item.type === 'relaySet') {
81 return count + item.urls.length
82 }
83 return count
84 }, 0)
85 if (hasWriteRelays) {
86 return t('Write relays and {{count}} other relays', { count: relayCount })
87 }
88 return t('{{count}} relays', { count: relayCount })
89 }, [postTargetItems])
90
91 useEffect(() => {
92 if (openFrom && openFrom.length) {
93 setPostTargetItems(Array.from(new Set(openFrom)).map((url) => ({ type: 'relay', url })))
94 return
95 }
96 if (parentEventSeenOnRelays && parentEventSeenOnRelays.length) {
97 setPostTargetItems(parentEventSeenOnRelays.map((url) => ({ type: 'relay', url })))
98 return
99 }
100 setPostTargetItems([{ type: 'writeRelays' }])
101 }, [openFrom, parentEventSeenOnRelays])
102
103 useEffect(() => {
104 const isProtectedEvent = postTargetItems.every((item) => item.type !== 'writeRelays')
105 const relayUrls = postTargetItems.flatMap((item) => {
106 if (item.type === 'relay') {
107 return [item.url]
108 }
109 if (item.type === 'relaySet') {
110 return item.urls
111 }
112 return []
113 })
114
115 setIsProtectedEvent(isProtectedEvent)
116 setAdditionalRelayUrls(relayUrls)
117 }, [postTargetItems])
118
119 const handleWriteRelaysCheckedChange = useCallback((checked: boolean) => {
120 if (checked) {
121 setPostTargetItems((prev) => [...prev, { type: 'writeRelays' }])
122 } else {
123 setPostTargetItems((prev) => prev.filter((item) => item.type !== 'writeRelays'))
124 }
125 }, [])
126
127 const handleRelayCheckedChange = useCallback((checked: boolean, url: string) => {
128 if (checked) {
129 setPostTargetItems((prev) => [...prev, { type: 'relay', url }])
130 } else {
131 setPostTargetItems((prev) =>
132 prev.filter((item) => !(item.type === 'relay' && item.url === url))
133 )
134 }
135 }, [])
136
137 const handleRelaySetCheckedChange = useCallback(
138 (checked: boolean, id: string, urls: string[]) => {
139 if (checked) {
140 setPostTargetItems((prev) => [...prev, { type: 'relaySet', id, urls }])
141 } else {
142 setPostTargetItems((prev) =>
143 prev.filter((item) => !(item.type === 'relaySet' && item.id === id))
144 )
145 }
146 },
147 []
148 )
149
150 return (
151 <DropdownMenu>
152 <div className="flex items-center gap-2">
153 {t('Post to')}
154 <DropdownMenuTrigger asChild>
155 <Button variant="outline" className="px-2">
156 {description}
157 </Button>
158 </DropdownMenuTrigger>
159 </div>
160 <DropdownMenuContent align="start" className="max-w-96">
161 <DropdownMenuCheckboxItem
162 checked={postTargetItems.some((item) => item.type === 'writeRelays')}
163 onSelect={(e) => e.preventDefault()}
164 onCheckedChange={handleWriteRelaysCheckedChange}
165 >
166 {t('Write relays')}
167 </DropdownMenuCheckboxItem>
168 {relaySets.length > 0 && (
169 <>
170 <DropdownMenuSeparator />
171 {relaySets
172 .filter(({ relayUrls }) => relayUrls.length)
173 .map(({ id, name, relayUrls }) => (
174 <DropdownMenuCheckboxItem
175 key={id}
176 checked={postTargetItems.some(
177 (item) => item.type === 'relaySet' && item.id === id
178 )}
179 onSelect={(e) => e.preventDefault()}
180 onCheckedChange={(checked) => handleRelaySetCheckedChange(checked, id, relayUrls)}
181 >
182 <div className="truncate">
183 {name} ({relayUrls.length})
184 </div>
185 </DropdownMenuCheckboxItem>
186 ))}
187 </>
188 )}
189 {selectableRelays.length > 0 && (
190 <>
191 <DropdownMenuSeparator />
192 {selectableRelays.map((url) => (
193 <DropdownMenuCheckboxItem
194 key={url}
195 checked={postTargetItems.some((item) => item.type === 'relay' && item.url === url)}
196 onSelect={(e) => e.preventDefault()}
197 onCheckedChange={(checked) => handleRelayCheckedChange(checked, url)}
198 className="flex items-center gap-2"
199 >
200 <RelayIcon url={url} />
201 <div className="truncate">{simplifyUrl(url)}</div>
202 </DropdownMenuCheckboxItem>
203 ))}
204 </>
205 )}
206 </DropdownMenuContent>
207 </DropdownMenu>
208 )
209 }
210