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