index.tsx raw
1 import { Button } from '@/components/ui/button'
2 import {
3 Drawer,
4 DrawerContent,
5 DrawerHeader,
6 DrawerOverlay,
7 DrawerTitle
8 } from '@/components/ui/drawer'
9 import {
10 DropdownMenu,
11 DropdownMenuContent,
12 DropdownMenuItem,
13 DropdownMenuLabel,
14 DropdownMenuSeparator,
15 DropdownMenuTrigger
16 } from '@/components/ui/dropdown-menu'
17 import { Separator } from '@/components/ui/separator'
18 import { normalizeRelayUrl } from '@/domain/relay/adapters'
19 import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
20 import { useNostr } from '@/providers/NostrProvider'
21 import { useScreenSize } from '@/providers/ScreenSizeProvider'
22 import { TRelaySet } from '@/types'
23 import { Check, FolderPlus, Plus, Star } from 'lucide-react'
24 import { useMemo, useState } from 'react'
25 import { useTranslation } from 'react-i18next'
26 import DrawerMenuItem from '../DrawerMenuItem'
27
28 export default function SaveRelayDropdownMenu({
29 urls,
30 bigButton = false
31 }: {
32 urls: string[]
33 bigButton?: boolean
34 }) {
35 const { t } = useTranslation()
36 const { isSmallScreen } = useScreenSize()
37 const { favoriteRelays, relaySets } = useFavoriteRelays()
38 const normalizedUrls = useMemo(() => urls.map((url) => normalizeRelayUrl(url)).filter((url): url is string => url !== null), [urls])
39 const alreadySaved = useMemo(() => {
40 return (
41 normalizedUrls.every((url) => favoriteRelays.includes(url)) ||
42 relaySets.some((set) => normalizedUrls.every((url) => set.relayUrls.includes(url)))
43 )
44 }, [relaySets, normalizedUrls])
45 const [isDrawerOpen, setIsDrawerOpen] = useState(false)
46
47 const trigger = bigButton ? (
48 <Button variant="ghost" size="titlebar-icon" onClick={() => setIsDrawerOpen(true)}>
49 <Star className={alreadySaved ? 'fill-primary stroke-primary' : ''} />
50 </Button>
51 ) : (
52 <button
53 className="enabled:hover:text-primary [&_svg]:size-5 pr-0 pt-0.5"
54 onClick={(e) => {
55 e.stopPropagation()
56 setIsDrawerOpen(true)
57 }}
58 >
59 <Star className={alreadySaved ? 'fill-primary stroke-primary' : ''} />
60 </button>
61 )
62
63 if (isSmallScreen) {
64 return (
65 <div>
66 {trigger}
67 <div onClick={(e) => e.stopPropagation()}>
68 <Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
69 <DrawerOverlay onClick={() => setIsDrawerOpen(false)} />
70 <DrawerContent hideOverlay>
71 <DrawerHeader>
72 <DrawerTitle>{t('Save to')} ...</DrawerTitle>
73 </DrawerHeader>
74 <div className="py-2">
75 <RelayItem urls={normalizedUrls} />
76 {relaySets.map((set) => (
77 <RelaySetItem key={set.id} set={set} urls={normalizedUrls} />
78 ))}
79 <Separator />
80 <SaveToNewSet urls={normalizedUrls} />
81 </div>
82 </DrawerContent>
83 </Drawer>
84 </div>
85 </div>
86 )
87 }
88
89 return (
90 <DropdownMenu>
91 <DropdownMenuTrigger asChild className="px-2">
92 {trigger}
93 </DropdownMenuTrigger>
94 <DropdownMenuContent onClick={(e) => e.stopPropagation()}>
95 <DropdownMenuLabel>{t('Save to')} ...</DropdownMenuLabel>
96 <DropdownMenuSeparator />
97 <RelayItem urls={normalizedUrls} />
98 {relaySets.map((set) => (
99 <RelaySetItem key={set.id} set={set} urls={normalizedUrls} />
100 ))}
101 <DropdownMenuSeparator />
102 <SaveToNewSet urls={normalizedUrls} />
103 </DropdownMenuContent>
104 </DropdownMenu>
105 )
106 }
107
108 function RelayItem({ urls }: { urls: string[] }) {
109 const { t } = useTranslation()
110 const { isSmallScreen } = useScreenSize()
111 const { favoriteRelays, addFavoriteRelays, deleteFavoriteRelays } = useFavoriteRelays()
112 const saved = useMemo(
113 () => urls.every((url) => favoriteRelays.includes(url)),
114 [favoriteRelays, urls]
115 )
116
117 const handleClick = async () => {
118 if (saved) {
119 await deleteFavoriteRelays(urls)
120 } else {
121 await addFavoriteRelays(urls)
122 }
123 }
124
125 if (isSmallScreen) {
126 return (
127 <DrawerMenuItem onClick={handleClick}>
128 {saved ? <Check /> : <Plus />}
129 {saved ? t('Unfavorite') : t('Favorite')}
130 </DrawerMenuItem>
131 )
132 }
133
134 return (
135 <DropdownMenuItem className="flex gap-2" onClick={handleClick}>
136 {saved ? <Check /> : <Plus />}
137 {saved ? t('Unfavorite') : t('Favorite')}
138 </DropdownMenuItem>
139 )
140 }
141
142 function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) {
143 const { isSmallScreen } = useScreenSize()
144 const { pubkey, startLogin } = useNostr()
145 const { updateRelaySet } = useFavoriteRelays()
146 const saved = urls.every((url) => set.relayUrls.includes(url))
147
148 const handleClick = () => {
149 if (!pubkey) {
150 startLogin()
151 return
152 }
153 if (saved) {
154 updateRelaySet({
155 ...set,
156 relayUrls: set.relayUrls.filter((u) => !urls.includes(u))
157 })
158 } else {
159 updateRelaySet({
160 ...set,
161 relayUrls: Array.from(new Set([...set.relayUrls, ...urls]))
162 })
163 }
164 }
165
166 if (isSmallScreen) {
167 return (
168 <DrawerMenuItem onClick={handleClick}>
169 {saved ? <Check /> : <Plus />}
170 {set.name}
171 </DrawerMenuItem>
172 )
173 }
174
175 return (
176 <DropdownMenuItem key={set.id} className="flex gap-2" onClick={handleClick}>
177 {saved ? <Check /> : <Plus />}
178 {set.name}
179 </DropdownMenuItem>
180 )
181 }
182
183 function SaveToNewSet({ urls }: { urls: string[] }) {
184 const { t } = useTranslation()
185 const { isSmallScreen } = useScreenSize()
186 const { pubkey, startLogin } = useNostr()
187 const { createRelaySet } = useFavoriteRelays()
188
189 const handleSave = () => {
190 if (!pubkey) {
191 startLogin()
192 return
193 }
194 const newSetName = prompt(t('Enter a name for the new relay set'))
195 if (newSetName) {
196 createRelaySet(newSetName, urls)
197 }
198 }
199
200 if (isSmallScreen) {
201 return (
202 <DrawerMenuItem onClick={handleSave}>
203 <FolderPlus />
204 {t('Save to a new relay set')}
205 </DrawerMenuItem>
206 )
207 }
208
209 return (
210 <DropdownMenuItem onClick={handleSave}>
211 <FolderPlus />
212 {t('Save to a new relay set')}
213 </DropdownMenuItem>
214 )
215 }
216