feat: add mailbox configuration
This commit is contained in:
62
src/components/MailboxSetting/MailboxRelay.tsx
Normal file
62
src/components/MailboxSetting/MailboxRelay.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
import { CircleX, Server } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TMailboxRelay, TMailboxRelayScope } from './types'
|
||||
|
||||
export default function MailboxRelay({
|
||||
mailboxRelay,
|
||||
changeMailboxRelayScope,
|
||||
removeMailboxRelay
|
||||
}: {
|
||||
mailboxRelay: TMailboxRelay
|
||||
changeMailboxRelayScope: (url: string, scope: TMailboxRelayScope) => void
|
||||
removeMailboxRelay: (url: string) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const relayIcon = useMemo(() => {
|
||||
const url = new URL(mailboxRelay.url)
|
||||
return `${url.protocol === 'wss:' ? 'https:' : 'http:'}//${url.host}/favicon.ico`
|
||||
}, [mailboxRelay.url])
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 flex-1 w-0">
|
||||
<Avatar className="w-6 h-6">
|
||||
<AvatarImage src={relayIcon} />
|
||||
<AvatarFallback>
|
||||
<Server size={14} />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="truncate">{mailboxRelay.url}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Select
|
||||
value={mailboxRelay.scope}
|
||||
onValueChange={(v: TMailboxRelayScope) => changeMailboxRelayScope(mailboxRelay.url, v)}
|
||||
>
|
||||
<SelectTrigger className="w-24 shrink-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="both">{t('R & W')}</SelectItem>
|
||||
<SelectItem value="read">{t('Read')}</SelectItem>
|
||||
<SelectItem value="write">{t('Write')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<CircleX
|
||||
size={16}
|
||||
onClick={() => removeMailboxRelay(mailboxRelay.url)}
|
||||
className="text-muted-foreground hover:text-destructive clickable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
src/components/MailboxSetting/NewMailboxRelayInput.tsx
Normal file
52
src/components/MailboxSetting/NewMailboxRelayInput.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function NewMailboxRelayInput({
|
||||
saveNewMailboxRelay
|
||||
}: {
|
||||
saveNewMailboxRelay: (url: string) => string | null
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [newRelayUrl, setNewRelayUrl] = useState('')
|
||||
const [newRelayUrlError, setNewRelayUrlError] = useState<string | null>(null)
|
||||
|
||||
const save = () => {
|
||||
const error = saveNewMailboxRelay(newRelayUrl)
|
||||
if (error) {
|
||||
setNewRelayUrlError(error)
|
||||
} else {
|
||||
setNewRelayUrl('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRelayUrlInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
const handleRelayUrlInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNewRelayUrl(e.target.value)
|
||||
setNewRelayUrlError(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
className={newRelayUrlError ? 'border-destructive' : ''}
|
||||
placeholder={t('Add a new relay')}
|
||||
value={newRelayUrl}
|
||||
onKeyDown={handleRelayUrlInputKeyDown}
|
||||
onChange={handleRelayUrlInputChange}
|
||||
onBlur={save}
|
||||
/>
|
||||
<Button onClick={save}>{t('Add')}</Button>
|
||||
</div>
|
||||
{newRelayUrlError && <div className="text-destructive text-xs mt-1">{newRelayUrlError}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
src/components/MailboxSetting/SaveButton.tsx
Normal file
52
src/components/MailboxSetting/SaveButton.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useToast } from '@/hooks'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import dayjs from 'dayjs'
|
||||
import { CloudUpload, Loader } from 'lucide-react'
|
||||
import { kinds } from 'nostr-tools'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '../ui/button'
|
||||
import { TMailboxRelay } from './types'
|
||||
|
||||
export default function SaveButton({
|
||||
mailboxRelays,
|
||||
hasChange,
|
||||
setHasChange
|
||||
}: {
|
||||
mailboxRelays: TMailboxRelay[]
|
||||
hasChange: boolean
|
||||
setHasChange: (hasChange: boolean) => void
|
||||
}) {
|
||||
const { toast } = useToast()
|
||||
const { pubkey, publish, updateRelayList } = useNostr()
|
||||
const [pushing, setPushing] = useState(false)
|
||||
|
||||
const save = async () => {
|
||||
setPushing(true)
|
||||
const event = {
|
||||
kind: kinds.RelayList,
|
||||
content: '',
|
||||
tags: mailboxRelays.map(({ url, scope }) =>
|
||||
scope === 'both' ? ['r', url] : ['r', url, scope]
|
||||
),
|
||||
created_at: dayjs().unix()
|
||||
}
|
||||
await publish(event)
|
||||
updateRelayList({
|
||||
write: mailboxRelays.filter(({ scope }) => scope !== 'read').map(({ url }) => url),
|
||||
read: mailboxRelays.filter(({ scope }) => scope !== 'write').map(({ url }) => url)
|
||||
})
|
||||
toast({
|
||||
title: 'Save Successful',
|
||||
description: 'Successfully saved mailbox relays'
|
||||
})
|
||||
setHasChange(false)
|
||||
setPushing(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className="w-full" disabled={!pubkey || pushing || !hasChange} onClick={save}>
|
||||
{pushing ? <Loader className="animate-spin" /> : <CloudUpload />}
|
||||
Save
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
82
src/components/MailboxSetting/index.tsx
Normal file
82
src/components/MailboxSetting/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { normalizeUrl } from '@/lib/url'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MailboxRelay from './MailboxRelay'
|
||||
import NewMailboxRelayInput from './NewMailboxRelayInput'
|
||||
import SaveButton from './SaveButton'
|
||||
import { TMailboxRelay, TMailboxRelayScope } from './types'
|
||||
|
||||
export default function MailboxSetting() {
|
||||
const { t } = useTranslation()
|
||||
const { pubkey, relayList } = useNostr()
|
||||
const [relays, setRelays] = useState<TMailboxRelay[]>([])
|
||||
const [hasChange, setHasChange] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!relayList) return
|
||||
|
||||
const mailboxRelays: TMailboxRelay[] = relayList.read.map((url) => ({ url, scope: 'read' }))
|
||||
relayList.write.forEach((url) => {
|
||||
const item = mailboxRelays.find((r) => r.url === url)
|
||||
if (item) {
|
||||
item.scope = 'both'
|
||||
} else {
|
||||
mailboxRelays.push({ url, scope: 'write' })
|
||||
}
|
||||
})
|
||||
setRelays(mailboxRelays)
|
||||
}, [relayList])
|
||||
|
||||
if (!pubkey) {
|
||||
return <Button size="lg">Login to set</Button>
|
||||
}
|
||||
|
||||
if (!relayList) {
|
||||
return <div className="text-center text-sm text-muted-foreground">{t('loading...')}</div>
|
||||
}
|
||||
|
||||
const changeMailboxRelayScope = (url: string, scope: TMailboxRelayScope) => {
|
||||
setRelays((prev) => prev.map((r) => (r.url === url ? { ...r, scope } : r)))
|
||||
setHasChange(true)
|
||||
}
|
||||
|
||||
const removeMailboxRelay = (url: string) => {
|
||||
setRelays((prev) => prev.filter((r) => r.url !== url))
|
||||
setHasChange(true)
|
||||
}
|
||||
|
||||
const saveNewMailboxRelay = (url: string) => {
|
||||
if (url === '') return null
|
||||
const normalizedUrl = normalizeUrl(url)
|
||||
if (relays.some((r) => r.url === normalizedUrl)) {
|
||||
return t('Relay already exists')
|
||||
}
|
||||
setRelays([...relays, { url: normalizedUrl, scope: 'both' }])
|
||||
setHasChange(true)
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<div>{t('read relays description')}</div>
|
||||
<div>{t('write relays description')}</div>
|
||||
<div>{t('read & write relays notice')}</div>
|
||||
</div>
|
||||
<SaveButton mailboxRelays={relays} hasChange={hasChange} setHasChange={setHasChange} />
|
||||
<div className="space-y-2">
|
||||
{relays.map((relay) => (
|
||||
<MailboxRelay
|
||||
key={relay.url}
|
||||
mailboxRelay={relay}
|
||||
changeMailboxRelayScope={changeMailboxRelayScope}
|
||||
removeMailboxRelay={removeMailboxRelay}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<NewMailboxRelayInput saveNewMailboxRelay={saveNewMailboxRelay} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
5
src/components/MailboxSetting/types.ts
Normal file
5
src/components/MailboxSetting/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type TMailboxRelayScope = 'read' | 'write' | 'both'
|
||||
export type TMailboxRelay = {
|
||||
url: string
|
||||
scope: TMailboxRelayScope
|
||||
}
|
||||
Reference in New Issue
Block a user