RelaySet.tsx raw
1 import { Button } from '@/components/ui/button'
2 import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
3 import {
4 DropdownMenu,
5 DropdownMenuContent,
6 DropdownMenuItem,
7 DropdownMenuTrigger
8 } from '@/components/ui/dropdown-menu'
9 import { Input } from '@/components/ui/input'
10 import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
11 import { useScreenSize } from '@/providers/ScreenSizeProvider'
12 import { TRelaySet } from '@/types'
13 import { useSortable } from '@dnd-kit/sortable'
14 import { CSS } from '@dnd-kit/utilities'
15 import {
16 Check,
17 ChevronDown,
18 Edit,
19 EllipsisVertical,
20 FolderClosed,
21 GripVertical,
22 Link,
23 Trash2
24 } from 'lucide-react'
25 import { useState } from 'react'
26 import { useTranslation } from 'react-i18next'
27 import DrawerMenuItem from '../DrawerMenuItem'
28 import RelayUrls from './RelayUrl'
29 import { useRelaySetsSettingComponent } from './provider'
30
31 export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) {
32 const { t } = useTranslation()
33 const { expandedRelaySetId } = useRelaySetsSettingComponent()
34 const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
35 id: relaySet.id
36 })
37
38 const style = {
39 transform: CSS.Transform.toString(transform),
40 transition,
41 opacity: isDragging ? 0.5 : 1
42 }
43
44 return (
45 <div ref={setNodeRef} style={style} className="relative group">
46 <div className="w-full border rounded-lg px-2 py-2.5">
47 <div className="flex justify-between items-center">
48 <div className="flex items-center">
49 <div
50 className="cursor-grab active:cursor-grabbing p-2 hover:bg-muted rounded touch-none"
51 {...attributes}
52 {...listeners}
53 >
54 <GripVertical className="size-4 text-muted-foreground" />
55 </div>
56 <div className="flex gap-2 items-center">
57 <div className="flex justify-center items-center w-6 h-6 shrink-0">
58 <FolderClosed className="size-4" />
59 </div>
60 <RelaySetName relaySet={relaySet} />
61 </div>
62 </div>
63 <div className="flex gap-1">
64 <RelayUrlsExpandToggle relaySetId={relaySet.id}>
65 {t('n relays', { n: relaySet.relayUrls.length })}
66 </RelayUrlsExpandToggle>
67 <RelaySetOptions relaySet={relaySet} />
68 </div>
69 </div>
70 {expandedRelaySetId === relaySet.id && <RelayUrls relaySetId={relaySet.id} />}
71 </div>
72 </div>
73 )
74 }
75
76 function RelaySetName({ relaySet }: { relaySet: TRelaySet }) {
77 const [newSetName, setNewSetName] = useState(relaySet.name)
78 const { updateRelaySet } = useFavoriteRelays()
79 const { renamingRelaySetId, setRenamingRelaySetId } = useRelaySetsSettingComponent()
80
81 const saveNewRelaySetName = () => {
82 if (relaySet.name === newSetName) {
83 return setRenamingRelaySetId(null)
84 }
85 updateRelaySet({ ...relaySet, name: newSetName })
86 setRenamingRelaySetId(null)
87 }
88
89 const handleRenameInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
90 setNewSetName(e.target.value)
91 }
92
93 const handleRenameInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
94 if (event.key === 'Enter') {
95 event.preventDefault()
96 saveNewRelaySetName()
97 }
98 }
99
100 return renamingRelaySetId === relaySet.id ? (
101 <div className="flex gap-1 items-center">
102 <Input
103 value={newSetName}
104 onChange={handleRenameInputChange}
105 onBlur={saveNewRelaySetName}
106 onKeyDown={handleRenameInputKeyDown}
107 className="font-semibold w-28"
108 />
109 <Button variant="ghost" size="icon" onClick={saveNewRelaySetName}>
110 <Check size={18} className="text-green-500" />
111 </Button>
112 </div>
113 ) : (
114 <div className="h-8 font-semibold flex items-center select-none">{relaySet.name}</div>
115 )
116 }
117
118 function RelayUrlsExpandToggle({
119 relaySetId,
120 children
121 }: {
122 relaySetId: string
123 children: React.ReactNode
124 }) {
125 const { expandedRelaySetId, setExpandedRelaySetId } = useRelaySetsSettingComponent()
126 return (
127 <div
128 className="text-sm text-muted-foreground flex items-center gap-1 cursor-pointer hover:text-foreground"
129 onClick={() => setExpandedRelaySetId((pre) => (pre === relaySetId ? null : relaySetId))}
130 >
131 <div className="select-none">{children}</div>
132 <ChevronDown
133 size={16}
134 className={`transition-transform duration-200 ${expandedRelaySetId === relaySetId ? 'rotate-180' : ''}`}
135 />
136 </div>
137 )
138 }
139
140 function RelaySetOptions({ relaySet }: { relaySet: TRelaySet }) {
141 const { t } = useTranslation()
142 const { isSmallScreen } = useScreenSize()
143 const { deleteRelaySet } = useFavoriteRelays()
144 const { setRenamingRelaySetId } = useRelaySetsSettingComponent()
145
146 const trigger = (
147 <Button variant="ghost" size="icon">
148 <EllipsisVertical />
149 </Button>
150 )
151
152 const rename = () => {
153 setRenamingRelaySetId(relaySet.id)
154 }
155
156 const copyShareLink = () => {
157 navigator.clipboard.writeText(
158 `https://smesh.mleku.dev/?${relaySet.relayUrls.map((url) => 'r=' + url).join('&')}`
159 )
160 }
161
162 if (isSmallScreen) {
163 return (
164 <Drawer>
165 <DrawerTrigger asChild>{trigger}</DrawerTrigger>
166 <DrawerContent>
167 <div className="py-2">
168 <DrawerMenuItem onClick={rename}>
169 <Edit />
170 {t('Rename')}
171 </DrawerMenuItem>
172 <DrawerMenuItem onClick={copyShareLink}>
173 <Link />
174 {t('Copy share link')}
175 </DrawerMenuItem>
176 <DrawerMenuItem
177 className="text-destructive focus:text-destructive"
178 onClick={() => deleteRelaySet(relaySet.id)}
179 >
180 <Trash2 />
181 {t('Delete')}
182 </DrawerMenuItem>
183 </div>
184 </DrawerContent>
185 </Drawer>
186 )
187 }
188
189 return (
190 <DropdownMenu>
191 <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
192 <DropdownMenuContent>
193 <DropdownMenuItem onClick={rename}>
194 <Edit />
195 {t('Rename')}
196 </DropdownMenuItem>
197 <DropdownMenuItem onClick={copyShareLink}>
198 <Link />
199 {t('Copy share link')}
200 </DropdownMenuItem>
201 <DropdownMenuItem
202 className="text-destructive focus:text-destructive"
203 onClick={() => deleteRelaySet(relaySet.id)}
204 >
205 <Trash2 />
206 {t('Delete')}
207 </DropdownMenuItem>
208 </DropdownMenuContent>
209 </DropdownMenu>
210 )
211 }
212