feat: favorite relays (#250)
This commit is contained in:
@@ -4,12 +4,12 @@ import './index.css'
|
|||||||
import { Toaster } from '@/components/ui/toaster'
|
import { Toaster } from '@/components/ui/toaster'
|
||||||
import { ThemeProvider } from '@/providers/ThemeProvider'
|
import { ThemeProvider } from '@/providers/ThemeProvider'
|
||||||
import { PageManager } from './PageManager'
|
import { PageManager } from './PageManager'
|
||||||
|
import { FavoriteRelaysProvider } from './providers/FavoriteRelaysProvider'
|
||||||
import { FeedProvider } from './providers/FeedProvider'
|
import { FeedProvider } from './providers/FeedProvider'
|
||||||
import { FollowListProvider } from './providers/FollowListProvider'
|
import { FollowListProvider } from './providers/FollowListProvider'
|
||||||
import { MuteListProvider } from './providers/MuteListProvider'
|
import { MuteListProvider } from './providers/MuteListProvider'
|
||||||
import { NostrProvider } from './providers/NostrProvider'
|
import { NostrProvider } from './providers/NostrProvider'
|
||||||
import { NoteStatsProvider } from './providers/NoteStatsProvider'
|
import { NoteStatsProvider } from './providers/NoteStatsProvider'
|
||||||
import { RelaySetsProvider } from './providers/RelaySetsProvider'
|
|
||||||
import { ScreenSizeProvider } from './providers/ScreenSizeProvider'
|
import { ScreenSizeProvider } from './providers/ScreenSizeProvider'
|
||||||
import { ZapProvider } from './providers/ZapProvider'
|
import { ZapProvider } from './providers/ZapProvider'
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export default function App(): JSX.Element {
|
|||||||
<ScreenSizeProvider>
|
<ScreenSizeProvider>
|
||||||
<NostrProvider>
|
<NostrProvider>
|
||||||
<ZapProvider>
|
<ZapProvider>
|
||||||
<RelaySetsProvider>
|
<FavoriteRelaysProvider>
|
||||||
<FollowListProvider>
|
<FollowListProvider>
|
||||||
<MuteListProvider>
|
<MuteListProvider>
|
||||||
<FeedProvider>
|
<FeedProvider>
|
||||||
@@ -30,7 +30,7 @@ export default function App(): JSX.Element {
|
|||||||
</FeedProvider>
|
</FeedProvider>
|
||||||
</MuteListProvider>
|
</MuteListProvider>
|
||||||
</FollowListProvider>
|
</FollowListProvider>
|
||||||
</RelaySetsProvider>
|
</FavoriteRelaysProvider>
|
||||||
</ZapProvider>
|
</ZapProvider>
|
||||||
</NostrProvider>
|
</NostrProvider>
|
||||||
</ScreenSizeProvider>
|
</ScreenSizeProvider>
|
||||||
|
|||||||
25
src/components/DrawerMenuItem/index.tsx
Normal file
25
src/components/DrawerMenuItem/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { DrawerClose } from '@/components/ui/drawer'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export default function DrawerMenuItem({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
onClick
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
onClick?: (e: React.MouseEvent) => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DrawerClose className="w-full">
|
||||||
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn('w-full p-6 justify-start text-lg gap-4 [&_svg]:size-5', className)}
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
</DrawerClose>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
src/components/FavoriteRelaysSetting/AddNewRelay.tsx
Normal file
56
src/components/FavoriteRelaysSetting/AddNewRelay.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { normalizeUrl } from '@/lib/url'
|
||||||
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export default function AddNewRelay() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { favoriteRelays, addFavoriteRelays } = useFavoriteRelays()
|
||||||
|
const [input, setInput] = useState('')
|
||||||
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
|
|
||||||
|
const saveRelay = async () => {
|
||||||
|
if (!input) return
|
||||||
|
const normalizedUrl = normalizeUrl(input)
|
||||||
|
if (!normalizedUrl) {
|
||||||
|
setErrorMsg(t('Invalid URL'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (favoriteRelays.includes(normalizedUrl)) {
|
||||||
|
setErrorMsg(t('Already saved'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await addFavoriteRelays([normalizedUrl])
|
||||||
|
setInput('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewRelayInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInput(e.target.value)
|
||||||
|
setErrorMsg('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewRelayInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
saveRelay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Input
|
||||||
|
placeholder={t('Add a new relay')}
|
||||||
|
value={input}
|
||||||
|
onChange={handleNewRelayInputChange}
|
||||||
|
onKeyDown={handleNewRelayInputKeyDown}
|
||||||
|
className={errorMsg ? 'border-destructive' : ''}
|
||||||
|
/>
|
||||||
|
<Button onClick={saveRelay}>{t('Add')}</Button>
|
||||||
|
</div>
|
||||||
|
{errorMsg && <div className="text-destructive text-sm pl-8">{errorMsg}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
42
src/components/FavoriteRelaysSetting/AddNewRelaySet.tsx
Normal file
42
src/components/FavoriteRelaysSetting/AddNewRelaySet.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export default function AddNewRelaySet() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { addRelaySet } = useFavoriteRelays()
|
||||||
|
const [newRelaySetName, setNewRelaySetName] = useState('')
|
||||||
|
|
||||||
|
const saveRelaySet = () => {
|
||||||
|
if (!newRelaySetName) return
|
||||||
|
addRelaySet(newRelaySetName)
|
||||||
|
setNewRelaySetName('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewRelaySetNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setNewRelaySetName(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewRelaySetNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
saveRelaySet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Input
|
||||||
|
placeholder={t('Add a new relay set')}
|
||||||
|
value={newRelaySetName}
|
||||||
|
onChange={handleNewRelaySetNameChange}
|
||||||
|
onKeyDown={handleNewRelaySetNameKeyDown}
|
||||||
|
/>
|
||||||
|
<Button onClick={saveRelaySet}>{t('Add')}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
19
src/components/FavoriteRelaysSetting/RelayItem.tsx
Normal file
19
src/components/FavoriteRelaysSetting/RelayItem.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { toRelay } from '@/lib/link'
|
||||||
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
|
import RelayIcon from '../RelayIcon'
|
||||||
|
import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu'
|
||||||
|
|
||||||
|
export default function RelayItem({ relay }: { relay: string }) {
|
||||||
|
const { push } = useSecondaryPage()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex gap-2 border rounded-lg p-4 items-center clickable select-none"
|
||||||
|
onClick={() => push(toRelay(relay))}
|
||||||
|
>
|
||||||
|
<RelayIcon url={relay} />
|
||||||
|
<div className="flex-1 w-0 truncate font-semibold">{relay}</div>
|
||||||
|
<SaveRelayDropdownMenu urls={[relay]} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -6,29 +7,35 @@ import {
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
|
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { TRelaySet } from '@/types'
|
import { TRelaySet } from '@/types'
|
||||||
import { Check, ChevronDown, Circle, CircleCheck, EllipsisVertical } from 'lucide-react'
|
import {
|
||||||
import { useMemo, useState } from 'react'
|
Check,
|
||||||
|
ChevronDown,
|
||||||
|
Edit,
|
||||||
|
EllipsisVertical,
|
||||||
|
FolderClosed,
|
||||||
|
Link,
|
||||||
|
Trash2
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import DrawerMenuItem from '../DrawerMenuItem'
|
||||||
import RelayUrls from './RelayUrl'
|
import RelayUrls from './RelayUrl'
|
||||||
import { useRelaySetsSettingComponent } from './provider'
|
import { useRelaySetsSettingComponent } from './provider'
|
||||||
|
|
||||||
export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) {
|
export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { expandedRelaySetId, selectedRelaySetIds } = useRelaySetsSettingComponent()
|
const { expandedRelaySetId } = useRelaySetsSettingComponent()
|
||||||
const isSelected = useMemo(
|
|
||||||
() => selectedRelaySetIds.includes(relaySet.id),
|
|
||||||
[selectedRelaySetIds, relaySet.id]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="w-full border rounded-lg pl-4 pr-2 py-2.5">
|
||||||
className={`w-full border rounded-lg p-4 ${isSelected ? 'border-highlight bg-highlight/5' : ''}`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<RelaySetActiveToggle relaySetId={relaySet.id} />
|
<div className="flex justify-center items-center w-6 h-6 shrink-0">
|
||||||
|
<FolderClosed className="size-4" />
|
||||||
|
</div>
|
||||||
<RelaySetName relaySet={relaySet} />
|
<RelaySetName relaySet={relaySet} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
@@ -43,37 +50,10 @@ export default function RelaySet({ relaySet }: { relaySet: TRelaySet }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RelaySetActiveToggle({ relaySetId }: { relaySetId: string }) {
|
|
||||||
const { selectedRelaySetIds, toggleSelectedRelaySetId } = useRelaySetsSettingComponent()
|
|
||||||
const isSelected = useMemo(
|
|
||||||
() => selectedRelaySetIds.includes(relaySetId),
|
|
||||||
[selectedRelaySetIds, relaySetId]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
toggleSelectedRelaySetId(relaySetId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return isSelected ? (
|
|
||||||
<CircleCheck
|
|
||||||
size={18}
|
|
||||||
className="text-highlight shrink-0 cursor-pointer"
|
|
||||||
onClick={handleClick}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Circle
|
|
||||||
size={18}
|
|
||||||
className="text-muted-foreground shrink-0 cursor-pointer hover:text-foreground"
|
|
||||||
onClick={handleClick}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function RelaySetName({ relaySet }: { relaySet: TRelaySet }) {
|
function RelaySetName({ relaySet }: { relaySet: TRelaySet }) {
|
||||||
const [newSetName, setNewSetName] = useState(relaySet.name)
|
const [newSetName, setNewSetName] = useState(relaySet.name)
|
||||||
const { updateRelaySet } = useRelaySets()
|
const { updateRelaySet } = useFavoriteRelays()
|
||||||
const { renamingRelaySetId, setRenamingRelaySetId, toggleSelectedRelaySetId } =
|
const { renamingRelaySetId, setRenamingRelaySetId } = useRelaySetsSettingComponent()
|
||||||
useRelaySetsSettingComponent()
|
|
||||||
|
|
||||||
const saveNewRelaySetName = () => {
|
const saveNewRelaySetName = () => {
|
||||||
if (relaySet.name === newSetName) {
|
if (relaySet.name === newSetName) {
|
||||||
@@ -108,12 +88,7 @@ function RelaySetName({ relaySet }: { relaySet: TRelaySet }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="h-8 font-semibold flex items-center select-none">{relaySet.name}</div>
|
||||||
className="h-8 font-semibold flex items-center cursor-pointer select-none"
|
|
||||||
onClick={() => toggleSelectedRelaySetId(relaySet.id)}
|
|
||||||
>
|
|
||||||
{relaySet.name}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,33 +116,70 @@ function RelayUrlsExpandToggle({
|
|||||||
|
|
||||||
function RelaySetOptions({ relaySet }: { relaySet: TRelaySet }) {
|
function RelaySetOptions({ relaySet }: { relaySet: TRelaySet }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { deleteRelaySet } = useRelaySets()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const { deleteRelaySet } = useFavoriteRelays()
|
||||||
const { setRenamingRelaySetId } = useRelaySetsSettingComponent()
|
const { setRenamingRelaySetId } = useRelaySetsSettingComponent()
|
||||||
|
|
||||||
return (
|
const trigger = (
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<EllipsisVertical />
|
<EllipsisVertical />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
)
|
||||||
<DropdownMenuContent>
|
|
||||||
<DropdownMenuItem onClick={() => setRenamingRelaySetId(relaySet.id)}>
|
const rename = () => {
|
||||||
{t('Rename')}
|
setRenamingRelaySetId(relaySet.id)
|
||||||
</DropdownMenuItem>
|
}
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
const copyShareLink = () => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
`https://jumble.social/?${relaySet.relayUrls.map((url) => 'r=' + url).join('&')}`
|
`https://jumble.social/?${relaySet.relayUrls.map((url) => 'r=' + url).join('&')}`
|
||||||
)
|
)
|
||||||
}}
|
}
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||||
|
<DrawerContent>
|
||||||
|
<div className="py-2">
|
||||||
|
<DrawerMenuItem onClick={rename}>
|
||||||
|
<Edit />
|
||||||
|
{t('Rename')}
|
||||||
|
</DrawerMenuItem>
|
||||||
|
<DrawerMenuItem onClick={copyShareLink}>
|
||||||
|
<Link />
|
||||||
|
{t('Copy share link')}
|
||||||
|
</DrawerMenuItem>
|
||||||
|
<DrawerMenuItem
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
onClick={() => deleteRelaySet(relaySet.id)}
|
||||||
>
|
>
|
||||||
|
<Trash2 />
|
||||||
|
{t('Delete')}
|
||||||
|
</DrawerMenuItem>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem onClick={rename}>
|
||||||
|
<Edit />
|
||||||
|
{t('Rename')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={copyShareLink}>
|
||||||
|
<Link />
|
||||||
{t('Copy share link')}
|
{t('Copy share link')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
onClick={() => deleteRelaySet(relaySet.id)}
|
onClick={() => deleteRelaySet(relaySet.id)}
|
||||||
>
|
>
|
||||||
|
<Trash2 />
|
||||||
{t('Delete')}
|
{t('Delete')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { useFetchRelayInfo } from '@/hooks'
|
|
||||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
|
||||||
import { CircleX, SearchCheck } from 'lucide-react'
|
import { CircleX } from 'lucide-react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import RelayIcon from '../RelayIcon'
|
||||||
|
|
||||||
export default function RelayUrls({ relaySetId }: { relaySetId: string }) {
|
export default function RelayUrls({ relaySetId }: { relaySetId: string }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { relaySets, updateRelaySet } = useRelaySets()
|
const { relaySets, updateRelaySet } = useFavoriteRelays()
|
||||||
const [newRelayUrl, setNewRelayUrl] = useState('')
|
const [newRelayUrl, setNewRelayUrl] = useState('')
|
||||||
const [newRelayUrlError, setNewRelayUrlError] = useState<string | null>(null)
|
const [newRelayUrlError, setNewRelayUrlError] = useState<string | null>(null)
|
||||||
const relaySet = useMemo(
|
const relaySet = useMemo(
|
||||||
@@ -79,20 +79,13 @@ export default function RelayUrls({ relaySetId }: { relaySetId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RelayUrl({ url, onRemove }: { url: string; onRemove: () => void }) {
|
function RelayUrl({ url, onRemove }: { url: string; onRemove: () => void }) {
|
||||||
const { t } = useTranslation()
|
|
||||||
const { relayInfo } = useFetchRelayInfo(url)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between pl-1 pr-3">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-3 items-center flex-1 w-0">
|
||||||
<div className="text-muted-foreground text-sm">{url}</div>
|
<RelayIcon url={url} className="w-4 h-4" iconSize={10} />
|
||||||
{relayInfo?.supported_nips?.includes(50) && (
|
<div className="text-muted-foreground text-sm truncate">{url}</div>
|
||||||
<div title={t('supports search')} className="text-highlight">
|
|
||||||
<SearchCheck size={14} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="shrink-0">
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CircleX
|
<CircleX
|
||||||
size={16}
|
size={16}
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
28
src/components/FavoriteRelaysSetting/TemporaryRelaySet.tsx
Normal file
28
src/components/FavoriteRelaysSetting/TemporaryRelaySet.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { useFeed } from '@/providers/FeedProvider'
|
||||||
|
import RelayIcon from '../RelayIcon'
|
||||||
|
import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu'
|
||||||
|
|
||||||
|
export default function TemporaryRelaySet() {
|
||||||
|
const { temporaryRelayUrls } = useFeed()
|
||||||
|
|
||||||
|
if (!temporaryRelayUrls.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full border border-dashed rounded-lg p-4 border-highlight bg-highlight/5 flex gap-4 justify-between">
|
||||||
|
<div className="flex-1 w-0">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="h-8 font-semibold">Temporary</div>
|
||||||
|
</div>
|
||||||
|
{temporaryRelayUrls.map((url) => (
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<RelayIcon url={url} className="w-4 h-4" iconSize={10} />
|
||||||
|
<div className="text-muted-foreground text-sm truncate">{url}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<SaveRelayDropdownMenu urls={temporaryRelayUrls} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
src/components/FavoriteRelaysSetting/index.tsx
Normal file
35
src/components/FavoriteRelaysSetting/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import AddNewRelay from './AddNewRelay'
|
||||||
|
import AddNewRelaySet from './AddNewRelaySet'
|
||||||
|
import { RelaySetsSettingComponentProvider } from './provider'
|
||||||
|
import RelayItem from './RelayItem'
|
||||||
|
import RelaySet from './RelaySet'
|
||||||
|
import TemporaryRelaySet from './TemporaryRelaySet'
|
||||||
|
|
||||||
|
export default function FavoriteRelaysSetting() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { relaySets, favoriteRelays } = useFavoriteRelays()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RelaySetsSettingComponentProvider>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<TemporaryRelaySet />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-muted-foreground font-semibold select-none">{t('Relay sets')}</div>
|
||||||
|
{relaySets.map((relaySet) => (
|
||||||
|
<RelaySet key={relaySet.id} relaySet={relaySet} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<AddNewRelaySet />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-muted-foreground font-semibold select-none">{t('Relays')}</div>
|
||||||
|
{favoriteRelays.map((relay) => (
|
||||||
|
<RelayItem key={relay} relay={relay} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<AddNewRelay />
|
||||||
|
</div>
|
||||||
|
</RelaySetsSettingComponentProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@ type TRelaySetsSettingComponentContext = {
|
|||||||
setRenamingRelaySetId: React.Dispatch<React.SetStateAction<string | null>>
|
setRenamingRelaySetId: React.Dispatch<React.SetStateAction<string | null>>
|
||||||
expandedRelaySetId: string | null
|
expandedRelaySetId: string | null
|
||||||
setExpandedRelaySetId: React.Dispatch<React.SetStateAction<string | null>>
|
setExpandedRelaySetId: React.Dispatch<React.SetStateAction<string | null>>
|
||||||
selectedRelaySetIds: string[]
|
|
||||||
toggleSelectedRelaySetId: (relaySetId: string) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RelaySetsSettingComponentContext = createContext<
|
export const RelaySetsSettingComponentContext = createContext<
|
||||||
@@ -26,7 +24,6 @@ export const useRelaySetsSettingComponent = () => {
|
|||||||
export function RelaySetsSettingComponentProvider({ children }: { children: React.ReactNode }) {
|
export function RelaySetsSettingComponentProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [renamingRelaySetId, setRenamingRelaySetId] = useState<string | null>(null)
|
const [renamingRelaySetId, setRenamingRelaySetId] = useState<string | null>(null)
|
||||||
const [expandedRelaySetId, setExpandedRelaySetId] = useState<string | null>(null)
|
const [expandedRelaySetId, setExpandedRelaySetId] = useState<string | null>(null)
|
||||||
const [selectedRelaySetIds, setSelectedRelaySetIds] = useState<string[]>([])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RelaySetsSettingComponentContext.Provider
|
<RelaySetsSettingComponentContext.Provider
|
||||||
@@ -34,16 +31,7 @@ export function RelaySetsSettingComponentProvider({ children }: { children: Reac
|
|||||||
renamingRelaySetId,
|
renamingRelaySetId,
|
||||||
setRenamingRelaySetId,
|
setRenamingRelaySetId,
|
||||||
expandedRelaySetId,
|
expandedRelaySetId,
|
||||||
setExpandedRelaySetId,
|
setExpandedRelaySetId
|
||||||
selectedRelaySetIds,
|
|
||||||
toggleSelectedRelaySetId: (relaySetId) => {
|
|
||||||
setSelectedRelaySetIds((pre) => {
|
|
||||||
if (pre.includes(relaySetId)) {
|
|
||||||
return pre.filter((id) => id !== relaySetId)
|
|
||||||
}
|
|
||||||
return [...pre, relaySetId]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -1,67 +1,71 @@
|
|||||||
import { toRelaySettings } from '@/lib/link'
|
import { toRelaySettings } from '@/lib/link'
|
||||||
import { simplifyUrl } from '@/lib/url'
|
import { simplifyUrl } from '@/lib/url'
|
||||||
import { SecondaryPageLink } from '@/PageManager'
|
import { SecondaryPageLink } from '@/PageManager'
|
||||||
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
|
||||||
import { useFeed } from '@/providers/FeedProvider'
|
import { useFeed } from '@/providers/FeedProvider'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
|
||||||
import { Circle, CircleCheck } from 'lucide-react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import RelayIcon from '../RelayIcon'
|
||||||
import RelaySetCard from '../RelaySetCard'
|
import RelaySetCard from '../RelaySetCard'
|
||||||
import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu'
|
import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu'
|
||||||
|
import { UsersRound } from 'lucide-react'
|
||||||
|
|
||||||
export default function FeedSwitcher({ close }: { close?: () => void }) {
|
export default function FeedSwitcher({ close }: { close?: () => void }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { feedType, switchFeed, activeRelaySetId, temporaryRelayUrls } = useFeed()
|
|
||||||
const { pubkey } = useNostr()
|
const { pubkey } = useNostr()
|
||||||
const { relaySets } = useRelaySets()
|
const { relaySets, favoriteRelays } = useFavoriteRelays()
|
||||||
|
const { feedInfo, switchFeed, temporaryRelayUrls } = useFeed()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{pubkey && (
|
{pubkey && (
|
||||||
<FeedSwitcherItem
|
<FeedSwitcherItem
|
||||||
itemName={t('Following')}
|
isActive={feedInfo.feedType === 'following'}
|
||||||
isActive={feedType === 'following'}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!pubkey) return
|
if (!pubkey) return
|
||||||
switchFeed('following', { pubkey })
|
switchFeed('following', { pubkey })
|
||||||
close?.()
|
close?.()
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between px-2">
|
|
||||||
<div className="text-muted-foreground text-sm font-semibold">{t('relay sets')}</div>
|
|
||||||
<SecondaryPageLink
|
|
||||||
to={toRelaySettings()}
|
|
||||||
className="text-highlight text-sm font-semibold"
|
|
||||||
onClick={() => close?.()}
|
|
||||||
>
|
>
|
||||||
{t('edit')}
|
<div className="flex gap-2 items-center">
|
||||||
</SecondaryPageLink>
|
<div className="flex justify-center items-center w-6 h-6 shrink-0">
|
||||||
|
<UsersRound className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>{t('Following')}</div>
|
||||||
|
</div>
|
||||||
|
</FeedSwitcherItem>
|
||||||
|
)}
|
||||||
{temporaryRelayUrls.length > 0 && (
|
{temporaryRelayUrls.length > 0 && (
|
||||||
<FeedSwitcherItem
|
<FeedSwitcherItem
|
||||||
key="temporary"
|
key="temporary"
|
||||||
itemName={
|
isActive={feedInfo.feedType === 'temporary'}
|
||||||
temporaryRelayUrls.length === 1 ? simplifyUrl(temporaryRelayUrls[0]) : t('Temporary')
|
|
||||||
}
|
|
||||||
isActive={feedType === 'temporary'}
|
|
||||||
temporary
|
temporary
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
switchFeed('temporary')
|
switchFeed('temporary')
|
||||||
close?.()
|
close?.()
|
||||||
}}
|
}}
|
||||||
controls={<SaveRelayDropdownMenu urls={temporaryRelayUrls} />}
|
controls={<SaveRelayDropdownMenu urls={temporaryRelayUrls} />}
|
||||||
/>
|
>
|
||||||
|
{temporaryRelayUrls.length === 1 ? simplifyUrl(temporaryRelayUrls[0]) : t('Temporary')}
|
||||||
|
</FeedSwitcherItem>
|
||||||
)}
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-end items-center text-sm">
|
||||||
|
<SecondaryPageLink
|
||||||
|
to={toRelaySettings()}
|
||||||
|
className="text-highlight font-semibold"
|
||||||
|
onClick={() => close?.()}
|
||||||
|
>
|
||||||
|
{t('edit')}
|
||||||
|
</SecondaryPageLink>
|
||||||
|
</div>
|
||||||
{relaySets
|
{relaySets
|
||||||
.filter((set) => set.relayUrls.length > 0)
|
.filter((set) => set.relayUrls.length > 0)
|
||||||
.map((set) => (
|
.map((set) => (
|
||||||
<RelaySetCard
|
<RelaySetCard
|
||||||
key={set.id}
|
key={set.id}
|
||||||
relaySet={set}
|
relaySet={set}
|
||||||
select={feedType === 'relays' && set.id === activeRelaySetId}
|
select={feedInfo.feedType === 'relays' && set.id === feedInfo.id}
|
||||||
showConnectionStatus={feedType === 'relays' && set.id === activeRelaySetId}
|
|
||||||
onSelectChange={(select) => {
|
onSelectChange={(select) => {
|
||||||
if (!select) return
|
if (!select) return
|
||||||
switchFeed('relays', { activeRelaySetId: set.id })
|
switchFeed('relays', { activeRelaySetId: set.id })
|
||||||
@@ -69,19 +73,34 @@ export default function FeedSwitcher({ close }: { close?: () => void }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{favoriteRelays.map((relay) => (
|
||||||
|
<FeedSwitcherItem
|
||||||
|
key={relay}
|
||||||
|
isActive={feedInfo.feedType === 'relay' && feedInfo.id === relay}
|
||||||
|
onClick={() => {
|
||||||
|
switchFeed('relay', { relay })
|
||||||
|
close?.()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center w-full">
|
||||||
|
<RelayIcon url={relay} />
|
||||||
|
<div className="flex-1 w-0 truncate">{simplifyUrl(relay)}</div>
|
||||||
|
</div>
|
||||||
|
</FeedSwitcherItem>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeedSwitcherItem({
|
function FeedSwitcherItem({
|
||||||
itemName,
|
children,
|
||||||
isActive,
|
isActive,
|
||||||
temporary = false,
|
temporary = false,
|
||||||
onClick,
|
onClick,
|
||||||
controls
|
controls
|
||||||
}: {
|
}: {
|
||||||
itemName: string
|
children: React.ReactNode
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
temporary?: boolean
|
temporary?: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
@@ -93,20 +112,9 @@ function FeedSwitcherItem({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="font-semibold flex-1">{children}</div>
|
||||||
<FeedToggle isActive={isActive} />
|
|
||||||
<div className="font-semibold">{itemName}</div>
|
|
||||||
</div>
|
|
||||||
{controls}
|
{controls}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function FeedToggle({ isActive }: { isActive: boolean }) {
|
|
||||||
return isActive ? (
|
|
||||||
<CircleCheck size={18} className="text-highlight shrink-0" />
|
|
||||||
) : (
|
|
||||||
<Circle size={18} className="text-muted-foreground shrink-0" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants'
|
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||||
import { isCommentEvent, isProtectedEvent } from '@/lib/event'
|
import { isCommentEvent, isProtectedEvent } from '@/lib/event'
|
||||||
import { tagNameEquals } from '@/lib/tag'
|
import { tagNameEquals } from '@/lib/tag'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -71,7 +71,7 @@ export default function Nip22ReplyNoteList({
|
|||||||
relayUrls.slice(0, 4),
|
relayUrls.slice(0, 4),
|
||||||
{
|
{
|
||||||
'#E': [event.id],
|
'#E': [event.id],
|
||||||
kinds: [COMMENT_EVENT_KIND],
|
kinds: [ExtendedKind.COMMENT],
|
||||||
limit: LIMIT
|
limit: LIMIT
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GROUP_METADATA_EVENT_KIND } from '@/constants'
|
import { ExtendedKind } from '@/constants'
|
||||||
import { isSupportedKind } from '@/lib/event'
|
import { isSupportedKind } from '@/lib/event'
|
||||||
import { useMuteList } from '@/providers/MuteListProvider'
|
import { useMuteList } from '@/providers/MuteListProvider'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
@@ -58,7 +58,7 @@ export default function GenericNoteCard({
|
|||||||
<LiveEventCard event={event} className={className} reposter={reposter} embedded={embedded} />
|
<LiveEventCard event={event} className={className} reposter={reposter} embedded={embedded} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (event.kind === GROUP_METADATA_EVENT_KIND) {
|
if (event.kind === ExtendedKind.GROUP_METADATA) {
|
||||||
return (
|
return (
|
||||||
<GroupMetadataCard
|
<GroupMetadataCard
|
||||||
className={className}
|
className={className}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { PICTURE_EVENT_KIND } from '@/constants'
|
import { ExtendedKind } from '@/constants'
|
||||||
import { isReplyNoteEvent } from '@/lib/event'
|
import { isReplyNoteEvent } from '@/lib/event'
|
||||||
import { checkAlgoRelay } from '@/lib/relay'
|
import { checkAlgoRelay } from '@/lib/relay'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -53,7 +53,7 @@ export default function NoteList({
|
|||||||
const isPictures = useMemo(() => listMode === 'pictures', [listMode])
|
const isPictures = useMemo(() => listMode === 'pictures', [listMode])
|
||||||
const noteFilter = useMemo(() => {
|
const noteFilter = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
kinds: isPictures ? [PICTURE_EVENT_KIND] : [kinds.ShortTextNote, kinds.Repost],
|
kinds: isPictures ? [ExtendedKind.PICTURE] : [kinds.ShortTextNote, kinds.Repost],
|
||||||
...filter
|
...filter
|
||||||
}
|
}
|
||||||
}, [JSON.stringify(filter), isPictures])
|
}, [JSON.stringify(filter), isPictures])
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function NoteOptions({ event, className }: { event: Event; classN
|
|||||||
<div className={className} onClick={(e) => e.stopPropagation()}>
|
<div className={className} onClick={(e) => e.stopPropagation()}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent collisionPadding={8} className="min-w-52">
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => navigator.clipboard.writeText(getSharableEventId(event))}
|
onClick={() => navigator.clipboard.writeText(getSharableEventId(event))}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export default function RepostButton({ event }: { event: Event }) {
|
|||||||
<>
|
<>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="min-w-44">
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function SeenOnButton({ event }: { event: Event }) {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent collisionPadding={8}>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuLabel>{t('Seen on')}</DropdownMenuLabel>
|
<DropdownMenuLabel>{t('Seen on')}</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{relays.map((relay) => (
|
{relays.map((relay) => (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PICTURE_EVENT_KIND } from '@/constants'
|
import { ExtendedKind } from '@/constants'
|
||||||
import { toNote } from '@/lib/link'
|
import { toNote } from '@/lib/link'
|
||||||
import { tagNameEquals } from '@/lib/tag'
|
import { tagNameEquals } from '@/lib/tag'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -24,7 +24,7 @@ export function CommentNotification({
|
|||||||
!rootEventId ||
|
!rootEventId ||
|
||||||
!rootPubkey ||
|
!rootPubkey ||
|
||||||
!rootKind ||
|
!rootKind ||
|
||||||
![kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(parseInt(rootKind))
|
![kinds.ShortTextNote, ExtendedKind.PICTURE].includes(parseInt(rootKind))
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Image from '@/components/Image'
|
import Image from '@/components/Image'
|
||||||
import { PICTURE_EVENT_KIND } from '@/constants'
|
import { ExtendedKind } from '@/constants'
|
||||||
import { useFetchEvent } from '@/hooks'
|
import { useFetchEvent } from '@/hooks'
|
||||||
import { toNote } from '@/lib/link'
|
import { toNote } from '@/lib/link'
|
||||||
import { extractEmojiFromEventTags, tagNameEquals } from '@/lib/tag'
|
import { extractEmojiFromEventTags, tagNameEquals } from '@/lib/tag'
|
||||||
@@ -53,7 +53,7 @@ export function ReactionNotification({
|
|||||||
return notification.content
|
return notification.content
|
||||||
}, [notification])
|
}, [notification])
|
||||||
|
|
||||||
if (!event || !eventId || ![kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(event.kind)) {
|
if (!event || !eventId || ![kinds.ShortTextNote, ExtendedKind.PICTURE].includes(event.kind)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { COMMENT_EVENT_KIND } from '@/constants'
|
import { ExtendedKind } from '@/constants'
|
||||||
import { useMuteList } from '@/providers/MuteListProvider'
|
import { useMuteList } from '@/providers/MuteListProvider'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { CommentNotification } from './CommentNotification'
|
import { CommentNotification } from './CommentNotification'
|
||||||
@@ -30,7 +30,7 @@ export function NotificationItem({
|
|||||||
if (notification.kind === kinds.Zap) {
|
if (notification.kind === kinds.Zap) {
|
||||||
return <ZapNotification notification={notification} isNew={isNew} />
|
return <ZapNotification notification={notification} isNew={isNew} />
|
||||||
}
|
}
|
||||||
if (notification.kind === COMMENT_EVENT_KIND) {
|
if (notification.kind === ExtendedKind.COMMENT) {
|
||||||
return <CommentNotification notification={notification} isNew={isNew} />
|
return <CommentNotification notification={notification} isNew={isNew} />
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants'
|
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
|
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
@@ -44,13 +44,13 @@ const NotificationList = forwardRef((_, ref) => {
|
|||||||
const filterKinds = useMemo(() => {
|
const filterKinds = useMemo(() => {
|
||||||
switch (notificationType) {
|
switch (notificationType) {
|
||||||
case 'mentions':
|
case 'mentions':
|
||||||
return [kinds.ShortTextNote, COMMENT_EVENT_KIND]
|
return [kinds.ShortTextNote, ExtendedKind.COMMENT]
|
||||||
case 'reactions':
|
case 'reactions':
|
||||||
return [kinds.Reaction, kinds.Repost]
|
return [kinds.Reaction, kinds.Repost]
|
||||||
case 'zaps':
|
case 'zaps':
|
||||||
return [kinds.Zap]
|
return [kinds.Zap]
|
||||||
default:
|
default:
|
||||||
return [kinds.ShortTextNote, kinds.Repost, kinds.Reaction, kinds.Zap, COMMENT_EVENT_KIND]
|
return [kinds.ShortTextNote, kinds.Repost, kinds.Reaction, kinds.Zap, ExtendedKind.COMMENT]
|
||||||
}
|
}
|
||||||
}, [notificationType])
|
}, [notificationType])
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function ProfileOptions({ pubkey }: { pubkey: string }) {
|
|||||||
<Ellipsis />
|
<Ellipsis />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent collisionPadding={8}>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))}
|
onClick={() => navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { useFetchRelayInfo } from '@/hooks'
|
import { useFetchRelayInfo } from '@/hooks'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { Server } from 'lucide-react'
|
import { Server } from 'lucide-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
export default function RelayIcon({
|
export default function RelayIcon({
|
||||||
url,
|
url,
|
||||||
className = 'w-6 h-6',
|
className,
|
||||||
iconSize = 14
|
iconSize = 14
|
||||||
}: {
|
}: {
|
||||||
url?: string
|
url?: string
|
||||||
@@ -23,7 +24,7 @@ export default function RelayIcon({
|
|||||||
}, [url, relayInfo])
|
}, [url, relayInfo])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar className={className}>
|
<Avatar className={cn('w-6 h-6', className)}>
|
||||||
<AvatarImage src={iconUrl} className="object-cover object-center" />
|
<AvatarImage src={iconUrl} className="object-cover object-center" />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
<Server size={iconSize} />
|
<Server size={iconSize} />
|
||||||
|
|||||||
@@ -1,33 +1,31 @@
|
|||||||
import client from '@/services/client.service'
|
|
||||||
import { TRelaySet } from '@/types'
|
import { TRelaySet } from '@/types'
|
||||||
import { ChevronDown, Circle, CircleCheck } from 'lucide-react'
|
import { ChevronDown, FolderClosed } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import RelayIcon from '../RelayIcon'
|
||||||
|
|
||||||
export default function RelaySetCard({
|
export default function RelaySetCard({
|
||||||
relaySet,
|
relaySet,
|
||||||
select,
|
select,
|
||||||
onSelectChange,
|
onSelectChange
|
||||||
showConnectionStatus = false
|
|
||||||
}: {
|
}: {
|
||||||
relaySet: TRelaySet
|
relaySet: TRelaySet
|
||||||
select: boolean
|
select: boolean
|
||||||
onSelectChange: (select: boolean) => void
|
onSelectChange: (select: boolean) => void
|
||||||
showConnectionStatus?: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [expand, setExpand] = useState(false)
|
const [expand, setExpand] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-full border rounded-lg p-4 ${select ? 'border-highlight bg-highlight/5' : ''}`}
|
className={`w-full border rounded-lg p-4 ${select ? 'border-highlight bg-highlight/5' : 'clickable'}`}
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div
|
|
||||||
className="flex space-x-2 items-center cursor-pointer"
|
|
||||||
onClick={() => onSelectChange(!select)}
|
onClick={() => onSelectChange(!select)}
|
||||||
>
|
>
|
||||||
<RelaySetActiveToggle select={select} />
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex space-x-2 items-center cursor-pointer">
|
||||||
|
<div className="flex justify-center items-center w-6 h-6 shrink-0">
|
||||||
|
<FolderClosed className="size-4" />
|
||||||
|
</div>
|
||||||
<div className="h-8 font-semibold flex items-center select-none">{relaySet.name}</div>
|
<div className="h-8 font-semibold flex items-center select-none">{relaySet.name}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
@@ -36,21 +34,11 @@ export default function RelaySetCard({
|
|||||||
</RelayUrlsExpandToggle>
|
</RelayUrlsExpandToggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{expand && (
|
{expand && <RelayUrls urls={relaySet.relayUrls} />}
|
||||||
<RelayUrls urls={relaySet.relayUrls} showConnectionStatus={showConnectionStatus} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RelaySetActiveToggle({ select }: { select: boolean }) {
|
|
||||||
return select ? (
|
|
||||||
<CircleCheck size={18} className="text-highlight shrink-0" />
|
|
||||||
) : (
|
|
||||||
<Circle size={18} className="shrink-0" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function RelayUrlsExpandToggle({
|
function RelayUrlsExpandToggle({
|
||||||
children,
|
children,
|
||||||
expand,
|
expand,
|
||||||
@@ -63,7 +51,10 @@ function RelayUrlsExpandToggle({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="text-sm text-muted-foreground flex items-center gap-1 cursor-pointer hover:text-foreground"
|
className="text-sm text-muted-foreground flex items-center gap-1 cursor-pointer hover:text-foreground"
|
||||||
onClick={() => onExpandChange(!expand)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onExpandChange(!expand)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="select-none">{children}</div>
|
<div className="select-none">{children}</div>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
@@ -74,49 +65,15 @@ function RelayUrlsExpandToggle({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RelayUrls({
|
function RelayUrls({ urls }: { urls: string[] }) {
|
||||||
showConnectionStatus = false,
|
|
||||||
urls
|
|
||||||
}: {
|
|
||||||
showConnectionStatus?: boolean
|
|
||||||
urls: string[]
|
|
||||||
}) {
|
|
||||||
const [relays, setRelays] = useState<
|
|
||||||
{
|
|
||||||
url: string
|
|
||||||
isConnected: boolean
|
|
||||||
}[]
|
|
||||||
>(urls.map((url) => ({ url, isConnected: false })) ?? [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!showConnectionStatus || urls.length === 0) return
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
const connectionStatusMap = client.listConnectionStatus()
|
|
||||||
setRelays((pre) => {
|
|
||||||
return pre.map((relay) => {
|
|
||||||
const isConnected = connectionStatusMap.get(relay.url) || false
|
|
||||||
return { ...relay, isConnected }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [showConnectionStatus, urls])
|
|
||||||
|
|
||||||
if (!urls) return null
|
if (!urls) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="pl-8 space-y-1">
|
||||||
{relays.map(({ url, isConnected: isConnected }, index) => (
|
{urls.map((url) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={url} className="flex items-center gap-2">
|
||||||
{showConnectionStatus &&
|
<RelayIcon url={url} className="w-4 h-4" iconSize={10} />
|
||||||
(isConnected ? (
|
<div className="text-muted-foreground text-sm truncate">{url}</div>
|
||||||
<div className="text-green-500 text-xs">●</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-red-500 text-xs">●</div>
|
|
||||||
))}
|
|
||||||
<div className="text-muted-foreground text-sm">{url}</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerDescription,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerTrigger
|
|
||||||
} from '@/components/ui/drawer'
|
|
||||||
import { BIG_RELAY_URLS } from '@/constants'
|
|
||||||
import { tagNameEquals } from '@/lib/tag'
|
|
||||||
import { isWebsocketUrl, simplifyUrl } from '@/lib/url'
|
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
|
||||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
|
||||||
import client from '@/services/client.service'
|
|
||||||
import { TRelaySet } from '@/types'
|
|
||||||
import { CloudDownload } from 'lucide-react'
|
|
||||||
import { kinds } from 'nostr-tools'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import RelaySetCard from '../RelaySetCard'
|
|
||||||
|
|
||||||
export default function PullFromRelaysButton() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { pubkey } = useNostr()
|
|
||||||
const { isSmallScreen } = useScreenSize()
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
const trigger = (
|
|
||||||
<Button variant="secondary" className="w-full" disabled={!pubkey}>
|
|
||||||
<CloudDownload />
|
|
||||||
{t('Pull from relays')}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isSmallScreen) {
|
|
||||||
return (
|
|
||||||
<Drawer open={open} onOpenChange={setOpen}>
|
|
||||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
|
||||||
<DrawerContent className="max-h-[90vh]">
|
|
||||||
<div className="flex flex-col p-4 gap-4 overflow-auto">
|
|
||||||
<DrawerHeader>
|
|
||||||
<DrawerTitle>{t('Select the relay sets you want to pull')}</DrawerTitle>
|
|
||||||
<DrawerDescription className="hidden" />
|
|
||||||
</DrawerHeader>
|
|
||||||
<RemoteRelaySets close={() => setOpen(false)} />
|
|
||||||
</div>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-[90vh] overflow-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t('Select the relay sets you want to pull')}</DialogTitle>
|
|
||||||
<DialogDescription className="hidden" />
|
|
||||||
</DialogHeader>
|
|
||||||
<RemoteRelaySets close={() => setOpen(false)} />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function RemoteRelaySets({ close }: { close?: () => void }) {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { pubkey, relayList } = useNostr()
|
|
||||||
const { mergeRelaySets } = useRelaySets()
|
|
||||||
const [initialed, setInitialed] = useState(false)
|
|
||||||
const [relaySets, setRelaySets] = useState<TRelaySet[]>([])
|
|
||||||
const [selectedRelaySetIds, setSelectedRelaySetIds] = useState<string[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pubkey) return
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
setInitialed(false)
|
|
||||||
const events = await client.fetchEvents(
|
|
||||||
(relayList?.write ?? []).concat(BIG_RELAY_URLS).slice(0, 4),
|
|
||||||
{
|
|
||||||
kinds: [kinds.Relaysets],
|
|
||||||
authors: [pubkey],
|
|
||||||
limit: 50
|
|
||||||
}
|
|
||||||
)
|
|
||||||
events.sort((a, b) => b.created_at - a.created_at)
|
|
||||||
|
|
||||||
const relaySetIds = new Set<string>()
|
|
||||||
const relaySets: TRelaySet[] = []
|
|
||||||
events.forEach((evt) => {
|
|
||||||
const id = evt.tags.find(tagNameEquals('d'))?.[1]
|
|
||||||
if (!id || relaySetIds.has(id)) return
|
|
||||||
|
|
||||||
relaySetIds.add(id)
|
|
||||||
const relayUrls = evt.tags
|
|
||||||
.filter(tagNameEquals('relay'))
|
|
||||||
.map((tag) => tag[1])
|
|
||||||
.filter((url) => url && isWebsocketUrl(url))
|
|
||||||
if (!relayUrls.length) return
|
|
||||||
|
|
||||||
let title = evt.tags.find(tagNameEquals('title'))?.[1]
|
|
||||||
if (!title) {
|
|
||||||
title = relayUrls.length === 1 ? simplifyUrl(relayUrls[0]) : id
|
|
||||||
}
|
|
||||||
relaySets.push({ id, name: title, relayUrls })
|
|
||||||
})
|
|
||||||
|
|
||||||
setRelaySets(relaySets)
|
|
||||||
setInitialed(true)
|
|
||||||
}
|
|
||||||
init()
|
|
||||||
}, [pubkey])
|
|
||||||
|
|
||||||
if (!pubkey) return null
|
|
||||||
if (!initialed) return <div className="text-center text-muted-foreground">{t('loading...')}</div>
|
|
||||||
if (!relaySets.length) {
|
|
||||||
return <div className="text-center text-muted-foreground">{t('No relay sets found')}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{relaySets.map((relaySet) => (
|
|
||||||
<RelaySetCard
|
|
||||||
key={relaySet.id}
|
|
||||||
relaySet={relaySet}
|
|
||||||
select={selectedRelaySetIds.includes(relaySet.id)}
|
|
||||||
onSelectChange={(select) => {
|
|
||||||
if (select) {
|
|
||||||
setSelectedRelaySetIds([...selectedRelaySetIds, relaySet.id])
|
|
||||||
} else {
|
|
||||||
setSelectedRelaySetIds(selectedRelaySetIds.filter((id) => id !== relaySet.id))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
className="w-20 shrink-0"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setSelectedRelaySetIds(relaySets.map((r) => r.id))}
|
|
||||||
>
|
|
||||||
{t('Select all')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
disabled={!selectedRelaySetIds.length}
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedRelaySetIds.length > 0) {
|
|
||||||
mergeRelaySets(relaySets.filter((set) => selectedRelaySetIds.includes(set.id)))
|
|
||||||
close?.()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedRelaySetIds.length > 0
|
|
||||||
? t('Pull n relay sets', { n: selectedRelaySetIds.length })
|
|
||||||
: t('Pull')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { useToast } from '@/hooks'
|
|
||||||
import { createRelaySetDraftEvent } from '@/lib/draft-event'
|
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
|
||||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
|
||||||
import { CloudUpload, Loader } from 'lucide-react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useRelaySetsSettingComponent } from './provider'
|
|
||||||
|
|
||||||
export default function PushToRelaysButton() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { toast } = useToast()
|
|
||||||
const { pubkey, publish } = useNostr()
|
|
||||||
const { relaySets } = useRelaySets()
|
|
||||||
const { selectedRelaySetIds } = useRelaySetsSettingComponent()
|
|
||||||
const [pushing, setPushing] = useState(false)
|
|
||||||
|
|
||||||
const push = async () => {
|
|
||||||
const selectedRelaySets = relaySets.filter((r) => selectedRelaySetIds.includes(r.id))
|
|
||||||
if (!selectedRelaySets.length) return
|
|
||||||
|
|
||||||
setPushing(true)
|
|
||||||
const draftEvents = selectedRelaySets.map((relaySet) => createRelaySetDraftEvent(relaySet))
|
|
||||||
await Promise.allSettled(draftEvents.map((event) => publish(event)))
|
|
||||||
toast({
|
|
||||||
title: t('Push Successful'),
|
|
||||||
description: t('Successfully pushed relay sets to relays')
|
|
||||||
})
|
|
||||||
setPushing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
className="w-full"
|
|
||||||
disabled={!pubkey || pushing || selectedRelaySetIds.length === 0}
|
|
||||||
onClick={push}
|
|
||||||
>
|
|
||||||
<CloudUpload />
|
|
||||||
{t('Push to relays')}
|
|
||||||
{pushing && <Loader className="animate-spin" />}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { useFetchRelayInfos } from '@/hooks'
|
|
||||||
import { useFeed } from '@/providers/FeedProvider'
|
|
||||||
import client from '@/services/client.service'
|
|
||||||
import { SearchCheck } from 'lucide-react'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu'
|
|
||||||
|
|
||||||
export default function TemporaryRelaySet() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { temporaryRelayUrls } = useFeed()
|
|
||||||
const [relays, setRelays] = useState<
|
|
||||||
{
|
|
||||||
url: string
|
|
||||||
isConnected: boolean
|
|
||||||
}[]
|
|
||||||
>(temporaryRelayUrls.map((url) => ({ url, isConnected: false })))
|
|
||||||
const { relayInfos } = useFetchRelayInfos(relays.map((relay) => relay.url))
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
const connectionStatusMap = client.listConnectionStatus()
|
|
||||||
setRelays((pre) => {
|
|
||||||
return pre.map((relay) => {
|
|
||||||
const isConnected = connectionStatusMap.get(relay.url) || false
|
|
||||||
return { ...relay, isConnected }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setRelays(temporaryRelayUrls.map((url) => ({ url, isConnected: false })))
|
|
||||||
}, [temporaryRelayUrls])
|
|
||||||
|
|
||||||
if (!relays.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full border border-dashed rounded-lg p-4 border-highlight bg-highlight/5 flex gap-4 justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="h-8 font-semibold">Temporary</div>
|
|
||||||
</div>
|
|
||||||
{relays.map((relay, index) => (
|
|
||||||
<div key={index} className="flex items-center justify-between">
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
{relay.isConnected ? (
|
|
||||||
<div className="text-green-500 text-xs">●</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-red-500 text-xs">●</div>
|
|
||||||
)}
|
|
||||||
<div className="text-muted-foreground text-sm">{relay.url}</div>
|
|
||||||
{relayInfos[index]?.supported_nips?.includes(50) && (
|
|
||||||
<div title={t('supports search')} className="text-highlight">
|
|
||||||
<SearchCheck size={14} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<SaveRelayDropdownMenu urls={temporaryRelayUrls} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { RelaySetsSettingComponentProvider } from './provider'
|
|
||||||
import RelaySet from './RelaySet'
|
|
||||||
import TemporaryRelaySet from './TemporaryRelaySet'
|
|
||||||
import PushToRelaysButton from './PushToRelaysButton'
|
|
||||||
import PullFromRelaysButton from './PullFromRelaysButton'
|
|
||||||
|
|
||||||
export default function RelaySetsSetting() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { relaySets, addRelaySet } = useRelaySets()
|
|
||||||
const [newRelaySetName, setNewRelaySetName] = useState('')
|
|
||||||
const dummyRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (dummyRef.current) {
|
|
||||||
dummyRef.current.focus()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const saveRelaySet = () => {
|
|
||||||
if (!newRelaySetName) return
|
|
||||||
addRelaySet(newRelaySetName)
|
|
||||||
setNewRelaySetName('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNewRelaySetNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setNewRelaySetName(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNewRelaySetNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault()
|
|
||||||
saveRelaySet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RelaySetsSettingComponentProvider>
|
|
||||||
<div ref={dummyRef} tabIndex={-1} style={{ position: 'absolute', opacity: 0 }}></div>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<PushToRelaysButton />
|
|
||||||
<PullFromRelaysButton />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 mt-4">
|
|
||||||
<TemporaryRelaySet />
|
|
||||||
{relaySets.map((relaySet) => (
|
|
||||||
<RelaySet key={relaySet.id} relaySet={relaySet} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{relaySets.length < 10 && (
|
|
||||||
<>
|
|
||||||
<Separator className="my-4" />
|
|
||||||
<div className="w-full border rounded-lg p-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="font-semibold">{t('Add a new relay set')}</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex gap-2">
|
|
||||||
<Input
|
|
||||||
placeholder={t('Relay set name')}
|
|
||||||
value={newRelaySetName}
|
|
||||||
onChange={handleNewRelaySetNameChange}
|
|
||||||
onKeyDown={handleNewRelaySetNameKeyDown}
|
|
||||||
onBlur={saveRelaySet}
|
|
||||||
/>
|
|
||||||
<Button onClick={saveRelaySet}>{t('Add')}</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</RelaySetsSettingComponentProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Drawer,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTitle
|
||||||
|
} from '@/components/ui/drawer'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -7,12 +14,15 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { normalizeUrl } from '@/lib/url'
|
import { normalizeUrl } from '@/lib/url'
|
||||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
|
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { TRelaySet } from '@/types'
|
import { TRelaySet } from '@/types'
|
||||||
import { Check, FolderPlus, Plus, Star } from 'lucide-react'
|
import { Check, FolderPlus, Plus, Star } from 'lucide-react'
|
||||||
import { useMemo } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import DrawerMenuItem from '../DrawerMenuItem'
|
||||||
|
|
||||||
export default function SaveRelayDropdownMenu({
|
export default function SaveRelayDropdownMenu({
|
||||||
urls,
|
urls,
|
||||||
@@ -22,29 +32,66 @@ export default function SaveRelayDropdownMenu({
|
|||||||
atTitlebar?: boolean
|
atTitlebar?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { relaySets } = useRelaySets()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const { favoriteRelays, relaySets } = useFavoriteRelays()
|
||||||
const normalizedUrls = useMemo(() => urls.map((url) => normalizeUrl(url)).filter(Boolean), [urls])
|
const normalizedUrls = useMemo(() => urls.map((url) => normalizeUrl(url)).filter(Boolean), [urls])
|
||||||
const alreadySaved = useMemo(
|
const alreadySaved = useMemo(() => {
|
||||||
() => relaySets.some((set) => normalizedUrls.every((url) => set.relayUrls.includes(url))),
|
|
||||||
[relaySets, normalizedUrls]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
normalizedUrls.every((url) => favoriteRelays.includes(url)) ||
|
||||||
<DropdownMenuTrigger asChild>
|
relaySets.some((set) => normalizedUrls.every((url) => set.relayUrls.includes(url)))
|
||||||
{atTitlebar ? (
|
)
|
||||||
<Button variant="ghost" size="titlebar-icon">
|
}, [relaySets, normalizedUrls])
|
||||||
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||||
|
|
||||||
|
const trigger = atTitlebar ? (
|
||||||
|
<Button variant="ghost" size="titlebar-icon" onClick={() => setIsDrawerOpen(true)}>
|
||||||
<Star className={alreadySaved ? 'fill-primary stroke-primary' : ''} />
|
<Star className={alreadySaved ? 'fill-primary stroke-primary' : ''} />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<button className="enabled:hover:text-primary [&_svg]:size-5">
|
<button
|
||||||
|
className="enabled:hover:text-primary [&_svg]:size-5"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsDrawerOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Star className={alreadySaved ? 'fill-primary stroke-primary' : ''} />
|
<Star className={alreadySaved ? 'fill-primary stroke-primary' : ''} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{trigger}
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Drawer open={isDrawerOpen} onOpenChange={setIsDrawerOpen}>
|
||||||
|
<DrawerOverlay onClick={() => setIsDrawerOpen(false)} />
|
||||||
|
<DrawerContent hideOverlay>
|
||||||
|
<DrawerHeader>
|
||||||
|
<DrawerTitle>{t('Save to')} ...</DrawerTitle>
|
||||||
|
</DrawerHeader>
|
||||||
|
<div className="py-2">
|
||||||
|
<RelayItem urls={normalizedUrls} />
|
||||||
|
{relaySets.map((set) => (
|
||||||
|
<RelaySetItem key={set.id} set={set} urls={normalizedUrls} />
|
||||||
|
))}
|
||||||
|
<Separator />
|
||||||
|
<SaveToNewSet urls={normalizedUrls} />
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent onClick={(e) => e.stopPropagation()}>
|
||||||
<DropdownMenuLabel>{t('Save to')} ...</DropdownMenuLabel>
|
<DropdownMenuLabel>{t('Save to')} ...</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<RelayItem urls={normalizedUrls} />
|
||||||
{relaySets.map((set) => (
|
{relaySets.map((set) => (
|
||||||
<RelaySetItem key={set.id} set={set} urls={normalizedUrls} />
|
<RelaySetItem key={set.id} set={set} urls={normalizedUrls} />
|
||||||
))}
|
))}
|
||||||
@@ -55,8 +102,43 @@ export default function SaveRelayDropdownMenu({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RelayItem({ urls }: { urls: string[] }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const { favoriteRelays, addFavoriteRelays, deleteFavoriteRelays } = useFavoriteRelays()
|
||||||
|
const saved = useMemo(
|
||||||
|
() => urls.every((url) => favoriteRelays.includes(url)),
|
||||||
|
[favoriteRelays, urls]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
if (saved) {
|
||||||
|
await deleteFavoriteRelays(urls)
|
||||||
|
} else {
|
||||||
|
await addFavoriteRelays(urls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<DrawerMenuItem onClick={handleClick}>
|
||||||
|
{saved ? <Check /> : <Plus />}
|
||||||
|
{t('Favorite')}
|
||||||
|
</DrawerMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem className="flex gap-2" onClick={handleClick}>
|
||||||
|
{saved ? <Check /> : <Plus />}
|
||||||
|
{t('Favorite')}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) {
|
function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) {
|
||||||
const { updateRelaySet } = useRelaySets()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const { updateRelaySet } = useFavoriteRelays()
|
||||||
const saved = urls.every((url) => set.relayUrls.includes(url))
|
const saved = urls.every((url) => set.relayUrls.includes(url))
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@@ -73,6 +155,15 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<DrawerMenuItem onClick={handleClick}>
|
||||||
|
{saved ? <Check /> : <Plus />}
|
||||||
|
{set.name}
|
||||||
|
</DrawerMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem key={set.id} className="flex gap-2" onClick={handleClick}>
|
<DropdownMenuItem key={set.id} className="flex gap-2" onClick={handleClick}>
|
||||||
{saved ? <Check /> : <Plus />}
|
{saved ? <Check /> : <Plus />}
|
||||||
@@ -83,7 +174,8 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) {
|
|||||||
|
|
||||||
function SaveToNewSet({ urls }: { urls: string[] }) {
|
function SaveToNewSet({ urls }: { urls: string[] }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { addRelaySet } = useRelaySets()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const { addRelaySet } = useFavoriteRelays()
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
const newSetName = prompt(t('Enter a name for the new relay set'))
|
const newSetName = prompt(t('Enter a name for the new relay set'))
|
||||||
@@ -92,6 +184,15 @@ function SaveToNewSet({ urls }: { urls: string[] }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSmallScreen) {
|
||||||
|
return (
|
||||||
|
<DrawerMenuItem onClick={handleSave}>
|
||||||
|
<FolderPlus />
|
||||||
|
{t('Save to a new relay set')}
|
||||||
|
</DrawerMenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem onClick={handleSave}>
|
<DropdownMenuItem onClick={handleSave}>
|
||||||
<FolderPlus />
|
<FolderPlus />
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function ProfileButton() {
|
|||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" side="top">
|
<DropdownMenuContent side="top">
|
||||||
<DropdownMenuItem onClick={() => push(toProfile(pubkey))}>
|
<DropdownMenuItem onClick={() => push(toProfile(pubkey))}>
|
||||||
<UserRound />
|
<UserRound />
|
||||||
{t('Profile')}
|
{t('Profile')}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ const DrawerContent = React.forwardRef<
|
|||||||
style={{
|
style={{
|
||||||
paddingBottom: 'env(safe-area-inset-bottom)'
|
paddingBottom: 'env(safe-area-inset-bottom)'
|
||||||
}}
|
}}
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 min-w-[8rem] overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'z-50 min-w-52 overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
collisionPadding={10}
|
collisionPadding={10}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const PopoverContent = React.forwardRef<
|
|||||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
export const StorageKey = {
|
export const StorageKey = {
|
||||||
|
VERSION: 'version',
|
||||||
THEME_SETTING: 'themeSetting',
|
THEME_SETTING: 'themeSetting',
|
||||||
RELAY_SETS: 'relaySets',
|
RELAY_SETS: 'relaySets',
|
||||||
ACTIVE_RELAY_SET_ID: 'activeRelaySetId',
|
|
||||||
FEED_TYPE: 'feedType',
|
|
||||||
ACCOUNTS: 'accounts',
|
ACCOUNTS: 'accounts',
|
||||||
CURRENT_ACCOUNT: 'currentAccount',
|
CURRENT_ACCOUNT: 'currentAccount',
|
||||||
ADD_CLIENT_TAG: 'addClientTag',
|
ADD_CLIENT_TAG: 'addClientTag',
|
||||||
@@ -12,11 +11,14 @@ export const StorageKey = {
|
|||||||
DEFAULT_ZAP_COMMENT: 'defaultZapComment',
|
DEFAULT_ZAP_COMMENT: 'defaultZapComment',
|
||||||
QUICK_ZAP: 'quickZap',
|
QUICK_ZAP: 'quickZap',
|
||||||
LAST_READ_NOTIFICATION_TIME_MAP: 'lastReadNotificationTimeMap',
|
LAST_READ_NOTIFICATION_TIME_MAP: 'lastReadNotificationTimeMap',
|
||||||
|
ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap',
|
||||||
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
|
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
|
||||||
ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap', // deprecated
|
ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap', // deprecated
|
||||||
ACCOUNT_MUTE_LIST_EVENT_MAP: 'accountMuteListEventMap', // deprecated
|
ACCOUNT_MUTE_LIST_EVENT_MAP: 'accountMuteListEventMap', // deprecated
|
||||||
ACCOUNT_MUTE_DECRYPTED_TAGS_MAP: 'accountMuteDecryptedTagsMap', // deprecated
|
ACCOUNT_MUTE_DECRYPTED_TAGS_MAP: 'accountMuteDecryptedTagsMap', // deprecated
|
||||||
ACCOUNT_PROFILE_EVENT_MAP: 'accountProfileEventMap' // deprecated
|
ACCOUNT_PROFILE_EVENT_MAP: 'accountProfileEventMap', // deprecated
|
||||||
|
ACTIVE_RELAY_SET_ID: 'activeRelaySetId', // deprecated
|
||||||
|
FEED_TYPE: 'feedType' // deprecated
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BIG_RELAY_URLS = [
|
export const BIG_RELAY_URLS = [
|
||||||
@@ -28,10 +30,15 @@ export const BIG_RELAY_URLS = [
|
|||||||
|
|
||||||
export const SEARCHABLE_RELAY_URLS = ['wss://relay.nostr.band/', 'wss://search.nos.today/']
|
export const SEARCHABLE_RELAY_URLS = ['wss://relay.nostr.band/', 'wss://search.nos.today/']
|
||||||
|
|
||||||
export const PICTURE_EVENT_KIND = 20
|
|
||||||
export const COMMENT_EVENT_KIND = 1111
|
|
||||||
export const GROUP_METADATA_EVENT_KIND = 39000
|
export const GROUP_METADATA_EVENT_KIND = 39000
|
||||||
|
|
||||||
|
export const ExtendedKind = {
|
||||||
|
PICTURE: 20,
|
||||||
|
FAVORITE_RELAYS: 10012,
|
||||||
|
COMMENT: 1111,
|
||||||
|
GROUP_METADATA: 39000
|
||||||
|
}
|
||||||
|
|
||||||
export const URL_REGEX = /https?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_:!~*]+/gu
|
export const URL_REGEX = /https?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_:!~*]+/gu
|
||||||
export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||||
|
|
||||||
@@ -39,3 +46,11 @@ export const MONITOR = '9bbbb845e5b6c831c29789900769843ab43bb5047abe697870cb50b6
|
|||||||
export const MONITOR_RELAYS = ['wss://relay.nostr.watch/']
|
export const MONITOR_RELAYS = ['wss://relay.nostr.watch/']
|
||||||
|
|
||||||
export const CODY_PUBKEY = '8125b911ed0e94dbe3008a0be48cfe5cd0c0b05923cfff917ae7e87da8400883'
|
export const CODY_PUBKEY = '8125b911ed0e94dbe3008a0be48cfe5cd0c0b05923cfff917ae7e87da8400883'
|
||||||
|
|
||||||
|
export const DEFAULT_FAVORITE_RELAYS = [
|
||||||
|
'wss://nostr.wine/',
|
||||||
|
'wss://pyramid.fiatjaf.com/',
|
||||||
|
'wss://140.f7z.io/',
|
||||||
|
'wss://news.utxo.one/',
|
||||||
|
'wss://algo.utxo.one'
|
||||||
|
]
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export default {
|
|||||||
'Reply to': 'الرد على',
|
'Reply to': 'الرد على',
|
||||||
Search: 'بحث',
|
Search: 'بحث',
|
||||||
'The relays you are connected to do not support search': 'الريلايات المتصلة لا تدعم البحث',
|
'The relays you are connected to do not support search': 'الريلايات المتصلة لا تدعم البحث',
|
||||||
'supports search': 'يدعم البحث',
|
|
||||||
'Show more...': 'عرض المزيد...',
|
'Show more...': 'عرض المزيد...',
|
||||||
'All users': 'جميع المستخدمين',
|
'All users': 'جميع المستخدمين',
|
||||||
'Display replies': 'عرض الردود',
|
'Display replies': 'عرض الردود',
|
||||||
@@ -116,14 +115,6 @@ export default {
|
|||||||
'R & W': 'قراءة وكتابة',
|
'R & W': 'قراءة وكتابة',
|
||||||
Read: 'قراءة',
|
Read: 'قراءة',
|
||||||
Write: 'كتابة',
|
Write: 'كتابة',
|
||||||
'Push to relays': 'إرسال إلى الريلايات',
|
|
||||||
'Push Successful': 'تم الإرسال بنجاح',
|
|
||||||
'Successfully pushed relay sets to relays': 'تم إرسال مجموعات الريلاي إلى الريلايات بنجاح',
|
|
||||||
'Pull from relays': 'استلام من الريلايات',
|
|
||||||
'Select the relay sets you want to pull': 'اختر مجموعات الريلاي التي تريد استلامها',
|
|
||||||
'No relay sets found': 'لم يتم العثور على مجموعات ريلاي',
|
|
||||||
'Pull n relay sets': 'سحب {{n}} مجموعات ريلاي',
|
|
||||||
Pull: 'سحب',
|
|
||||||
'Select all': 'اختر الكل',
|
'Select all': 'اختر الكل',
|
||||||
'Relay Sets': 'مجموعات الريلاي',
|
'Relay Sets': 'مجموعات الريلاي',
|
||||||
'Read & Write Relays': 'ريلايات القراءة والكتابة',
|
'Read & Write Relays': 'ريلايات القراءة والكتابة',
|
||||||
@@ -210,6 +201,8 @@ export default {
|
|||||||
'Seen on': 'شوهد على',
|
'Seen on': 'شوهد على',
|
||||||
'Temporarily display this reply': 'عرض هذا الرد مؤقتاً',
|
'Temporarily display this reply': 'عرض هذا الرد مؤقتاً',
|
||||||
'Not found the note': 'لم يتم العثور على الملاحظة',
|
'Not found the note': 'لم يتم العثور على الملاحظة',
|
||||||
'no more replies': 'لا توجد مزيد من الردود'
|
'no more replies': 'لا توجد مزيد من الردود',
|
||||||
|
'Relay sets': 'مجموعات الريلاي',
|
||||||
|
'Favorite Relays': 'الريلايات المفضلة'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export default {
|
|||||||
Search: 'Suchen',
|
Search: 'Suchen',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'Die verbundenen Relays unterstützen keine Suche',
|
'Die verbundenen Relays unterstützen keine Suche',
|
||||||
'supports search': 'unterstützt Suche',
|
|
||||||
'Show more...': 'Mehr anzeigen...',
|
'Show more...': 'Mehr anzeigen...',
|
||||||
'All users': 'Alle Benutzer',
|
'All users': 'Alle Benutzer',
|
||||||
'Display replies': 'Antworten anzeigen',
|
'Display replies': 'Antworten anzeigen',
|
||||||
@@ -117,14 +116,6 @@ export default {
|
|||||||
'R & W': 'R & W',
|
'R & W': 'R & W',
|
||||||
Read: 'Lesen',
|
Read: 'Lesen',
|
||||||
Write: 'Schreiben',
|
Write: 'Schreiben',
|
||||||
'Push to relays': 'An Relays senden',
|
|
||||||
'Push Successful': 'Senden erfolgreich',
|
|
||||||
'Successfully pushed relay sets to relays': 'Relay-Sets erfolgreich an Relays gesendet',
|
|
||||||
'Pull from relays': 'Von Relays abrufen',
|
|
||||||
'Select the relay sets you want to pull': 'Wähle die Relay-Sets, die du abrufen möchtest',
|
|
||||||
'No relay sets found': 'Keine Relay-Sets gefunden',
|
|
||||||
'Pull n relay sets': 'Hole {{n}} Relay-Sets',
|
|
||||||
Pull: 'Abrufen',
|
|
||||||
'Select all': 'Alle auswählen',
|
'Select all': 'Alle auswählen',
|
||||||
'Relay Sets': 'Relay-Sets',
|
'Relay Sets': 'Relay-Sets',
|
||||||
'Read & Write Relays': 'Lese- & Schreib-Relays',
|
'Read & Write Relays': 'Lese- & Schreib-Relays',
|
||||||
@@ -214,6 +205,8 @@ export default {
|
|||||||
'Seen on': 'Gesehen auf',
|
'Seen on': 'Gesehen auf',
|
||||||
'Temporarily display this reply': 'Antwort vorübergehend anzeigen',
|
'Temporarily display this reply': 'Antwort vorübergehend anzeigen',
|
||||||
'Not found the note': 'Die Notiz wurde nicht gefunden',
|
'Not found the note': 'Die Notiz wurde nicht gefunden',
|
||||||
'no more replies': 'keine weiteren Antworten'
|
'no more replies': 'keine weiteren Antworten',
|
||||||
|
'Relay sets': 'Relay-Sets',
|
||||||
|
'Favorite Relays': 'Lieblings-Relays'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export default {
|
|||||||
Search: 'Search',
|
Search: 'Search',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'The relays you are connected to do not support search',
|
'The relays you are connected to do not support search',
|
||||||
'supports search': 'supports search',
|
|
||||||
'Show more...': 'Show more...',
|
'Show more...': 'Show more...',
|
||||||
'All users': 'All users',
|
'All users': 'All users',
|
||||||
'Display replies': 'Display replies',
|
'Display replies': 'Display replies',
|
||||||
@@ -116,14 +115,6 @@ export default {
|
|||||||
'R & W': 'R & W',
|
'R & W': 'R & W',
|
||||||
Read: 'Read',
|
Read: 'Read',
|
||||||
Write: 'Write',
|
Write: 'Write',
|
||||||
'Push to relays': 'Push to relays',
|
|
||||||
'Push Successful': 'Push Successful',
|
|
||||||
'Successfully pushed relay sets to relays': 'Successfully pushed relay sets to relays',
|
|
||||||
'Pull from relays': 'Pull from relays',
|
|
||||||
'Select the relay sets you want to pull': 'Select the relay sets you want to pull',
|
|
||||||
'No relay sets found': 'No relay sets found',
|
|
||||||
'Pull n relay sets': 'Pull {{n}} relay sets',
|
|
||||||
Pull: 'Pull',
|
|
||||||
'Select all': 'Select all',
|
'Select all': 'Select all',
|
||||||
'Relay Sets': 'Relay Sets',
|
'Relay Sets': 'Relay Sets',
|
||||||
'Read & Write Relays': 'Read & Write Relays',
|
'Read & Write Relays': 'Read & Write Relays',
|
||||||
@@ -210,6 +201,8 @@ export default {
|
|||||||
'Seen on': 'Seen on',
|
'Seen on': 'Seen on',
|
||||||
'Temporarily display this reply': 'Temporarily display this reply',
|
'Temporarily display this reply': 'Temporarily display this reply',
|
||||||
'Not found the note': 'Not found the note',
|
'Not found the note': 'Not found the note',
|
||||||
'no more replies': 'no more replies'
|
'no more replies': 'no more replies',
|
||||||
|
'Relay sets': 'Relay sets',
|
||||||
|
'Favorite Relays': 'Favorite Relays'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export default {
|
|||||||
Search: 'Buscar',
|
Search: 'Buscar',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'Los relés a los que estás conectado no soportan la búsqueda',
|
'Los relés a los que estás conectado no soportan la búsqueda',
|
||||||
'supports search': 'soporta la búsqueda',
|
|
||||||
'Show more...': 'Mostrar más...',
|
'Show more...': 'Mostrar más...',
|
||||||
'All users': 'Todos los usuarios',
|
'All users': 'Todos los usuarios',
|
||||||
'Display replies': 'Mostrar respuestas',
|
'Display replies': 'Mostrar respuestas',
|
||||||
@@ -117,15 +116,6 @@ export default {
|
|||||||
'R & W': 'L y E',
|
'R & W': 'L y E',
|
||||||
Read: 'Leer',
|
Read: 'Leer',
|
||||||
Write: 'Escribir',
|
Write: 'Escribir',
|
||||||
'Push to relays': 'Enviar a relés',
|
|
||||||
'Push Successful': 'Envío exitoso',
|
|
||||||
'Successfully pushed relay sets to relays': 'Conjuntos de relés enviados con éxito a relés',
|
|
||||||
'Pull from relays': 'Recibir de relés',
|
|
||||||
'Select the relay sets you want to pull':
|
|
||||||
'Selecciona los conjuntos de relés que deseas recibir',
|
|
||||||
'No relay sets found': 'No se encontraron conjuntos de relés',
|
|
||||||
'Pull n relay sets': 'Recibir {{n}} conjuntos de relés',
|
|
||||||
Pull: 'Recibir',
|
|
||||||
'Select all': 'Seleccionar todo',
|
'Select all': 'Seleccionar todo',
|
||||||
'Relay Sets': 'Conjuntos de relés',
|
'Relay Sets': 'Conjuntos de relés',
|
||||||
'Read & Write Relays': 'Relés de lectura y escritura',
|
'Read & Write Relays': 'Relés de lectura y escritura',
|
||||||
@@ -214,6 +204,8 @@ export default {
|
|||||||
'Seen on': 'Visto en',
|
'Seen on': 'Visto en',
|
||||||
'Temporarily display this reply': 'Mostrar temporalmente esta respuesta',
|
'Temporarily display this reply': 'Mostrar temporalmente esta respuesta',
|
||||||
'Not found the note': 'No se encontró la nota',
|
'Not found the note': 'No se encontró la nota',
|
||||||
'no more replies': 'no hay más respuestas'
|
'no more replies': 'no hay más respuestas',
|
||||||
|
'Relay sets': 'Conjuntos de relés',
|
||||||
|
'Favorite Relays': 'Relés favoritos'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export default {
|
|||||||
Search: 'Recherche',
|
Search: 'Recherche',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'Les relais auxquels vous êtes connecté ne prennent pas en charge la recherche',
|
'Les relais auxquels vous êtes connecté ne prennent pas en charge la recherche',
|
||||||
'supports search': 'prend en charge la recherche',
|
|
||||||
'Show more...': 'Afficher plus...',
|
'Show more...': 'Afficher plus...',
|
||||||
'All users': 'Tous les utilisateurs',
|
'All users': 'Tous les utilisateurs',
|
||||||
'Display replies': 'Afficher les réponses',
|
'Display replies': 'Afficher les réponses',
|
||||||
@@ -117,14 +116,6 @@ export default {
|
|||||||
'R & W': 'R & W',
|
'R & W': 'R & W',
|
||||||
Read: 'Lire',
|
Read: 'Lire',
|
||||||
Write: 'Écrire',
|
Write: 'Écrire',
|
||||||
'Push to relays': 'Envoyer aux relais',
|
|
||||||
'Push Successful': 'Envoi réussi',
|
|
||||||
'Successfully pushed relay sets to relays': 'Groupes de relais envoyés avec succès aux relais',
|
|
||||||
'Pull from relays': 'Récupérer depuis les relais',
|
|
||||||
'Select the relay sets you want to pull': 'Sélectionnez les groupes de relais à récupérer',
|
|
||||||
'No relay sets found': 'Aucun groupe de relais trouvé',
|
|
||||||
'Pull n relay sets': 'Récupérer {{n}} groupes de relais',
|
|
||||||
Pull: 'Récupérer',
|
|
||||||
'Select all': 'Tout sélectionner',
|
'Select all': 'Tout sélectionner',
|
||||||
'Relay Sets': 'Groupes de relais',
|
'Relay Sets': 'Groupes de relais',
|
||||||
'Read & Write Relays': 'Relais lecture & écriture',
|
'Read & Write Relays': 'Relais lecture & écriture',
|
||||||
@@ -213,6 +204,8 @@ export default {
|
|||||||
'Seen on': 'Vu sur',
|
'Seen on': 'Vu sur',
|
||||||
'Temporarily display this reply': 'Afficher temporairement cette réponse',
|
'Temporarily display this reply': 'Afficher temporairement cette réponse',
|
||||||
'Not found the note': 'Note introuvable',
|
'Not found the note': 'Note introuvable',
|
||||||
'no more replies': 'aucune autre réponse'
|
'no more replies': 'aucune autre réponse',
|
||||||
|
'Relay sets': 'Groupes de relais',
|
||||||
|
'Favorite Relays': 'Relais favoris'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ export default {
|
|||||||
'switch to system theme': 'passa al tema di sistema',
|
'switch to system theme': 'passa al tema di sistema',
|
||||||
Note: 'Nota',
|
Note: 'Nota',
|
||||||
note: 'nota',
|
note: 'nota',
|
||||||
"username's following": "{{username}} seguiti",
|
"username's following": '{{username}} seguiti',
|
||||||
"username's used relays": "{{username}} relays usati",
|
"username's used relays": '{{username}} relays usati',
|
||||||
"username's muted": "{{username}} zittiti",
|
"username's muted": '{{username}} zittiti',
|
||||||
Login: 'Accedi',
|
Login: 'Accedi',
|
||||||
'Follows you': 'Ti segue',
|
'Follows you': 'Ti segue',
|
||||||
'Relay Settings': 'Impostazioni Relay',
|
'Relay Settings': 'Impostazioni Relay',
|
||||||
@@ -74,7 +74,6 @@ export default {
|
|||||||
Search: 'Ricerca',
|
Search: 'Ricerca',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'I relays a cui siete collegati non supportano la ricerca.',
|
'I relays a cui siete collegati non supportano la ricerca.',
|
||||||
'supports search': 'ricerche supportate',
|
|
||||||
'Show more...': 'Mostra di più...',
|
'Show more...': 'Mostra di più...',
|
||||||
'All users': 'Tutti gli utenti',
|
'All users': 'Tutti gli utenti',
|
||||||
'Display replies': 'Visualizza repliche',
|
'Display replies': 'Visualizza repliche',
|
||||||
@@ -92,7 +91,8 @@ export default {
|
|||||||
'Add an Account': 'Aggiungi un Account',
|
'Add an Account': 'Aggiungi un Account',
|
||||||
'More options': 'Più opzioni',
|
'More options': 'Più opzioni',
|
||||||
'Add client tag': 'Aggiungi etichetta del client',
|
'Add client tag': 'Aggiungi etichetta del client',
|
||||||
'Show others this was sent via Jumble': 'Mostra agli altri che questo è stato inviato tramite Jumble',
|
'Show others this was sent via Jumble':
|
||||||
|
'Mostra agli altri che questo è stato inviato tramite Jumble',
|
||||||
'Are you sure you want to logout?': 'Sei sicuro di volerti scollegare?',
|
'Are you sure you want to logout?': 'Sei sicuro di volerti scollegare?',
|
||||||
'relay sets': 'set di relay',
|
'relay sets': 'set di relay',
|
||||||
edit: 'modifica',
|
edit: 'modifica',
|
||||||
@@ -116,14 +116,6 @@ export default {
|
|||||||
'R & W': 'L & S',
|
'R & W': 'L & S',
|
||||||
Read: 'Leggi',
|
Read: 'Leggi',
|
||||||
Write: 'Scrivi',
|
Write: 'Scrivi',
|
||||||
'Push to relays': 'Invia ai relays',
|
|
||||||
'Push Successful': 'Invio riuscito',
|
|
||||||
'Successfully pushed relay sets to relays': 'Invio riuscito del set di relay ai relays',
|
|
||||||
'Pull from relays': 'Ottieni dai relays',
|
|
||||||
'Select the relay sets you want to pull': 'Selezionare i set di relay che si desidera ottenere',
|
|
||||||
'No relay sets found': 'Nessun set di relay trovato',
|
|
||||||
'Pull n relay sets': 'Ottieni {{n}} set di relay',
|
|
||||||
Pull: 'Ottieni',
|
|
||||||
'Select all': 'Seleziona tutto',
|
'Select all': 'Seleziona tutto',
|
||||||
'Relay Sets': 'Set di Relay',
|
'Relay Sets': 'Set di Relay',
|
||||||
'Read & Write Relays': 'Relay Leggi & Scrivi',
|
'Read & Write Relays': 'Relay Leggi & Scrivi',
|
||||||
@@ -133,7 +125,7 @@ export default {
|
|||||||
'I relay di scrittura sono utilizzati per pubblicare i tuoi eventi. Gli altri utenti cercheranno i tuoi eventi dai vostri relay di scrittura.',
|
'I relay di scrittura sono utilizzati per pubblicare i tuoi eventi. Gli altri utenti cercheranno i tuoi eventi dai vostri relay di scrittura.',
|
||||||
'read & write relays notice':
|
'read & write relays notice':
|
||||||
'Il numero di server di lettura e scrittura dovrebbe essere mantenuto idealmente tra 2 e 4.',
|
'Il numero di server di lettura e scrittura dovrebbe essere mantenuto idealmente tra 2 e 4.',
|
||||||
"Don't have an account yet?": "Non hai ancora un account?",
|
"Don't have an account yet?": 'Non hai ancora un account?',
|
||||||
'or simply generate a private key': 'o semplicemente genera una chiave privata',
|
'or simply generate a private key': 'o semplicemente genera una chiave privata',
|
||||||
'This is a private key. Do not share it with anyone. Keep it safe and secure. You will not be able to recover it if you lose it.':
|
'This is a private key. Do not share it with anyone. Keep it safe and secure. You will not be able to recover it if you lose it.':
|
||||||
'Questa è una chiave privata. Non condividetela con nessuno. Conservatela al sicuro. Non sarà possibile recuperarla in caso di smarrimento.',
|
'Questa è una chiave privata. Non condividetela con nessuno. Conservatela al sicuro. Non sarà possibile recuperarla in caso di smarrimento.',
|
||||||
@@ -144,7 +136,8 @@ export default {
|
|||||||
'Nostr Address (NIP-05)': 'Indirizzo Nostr (NIP-05)',
|
'Nostr Address (NIP-05)': 'Indirizzo Nostr (NIP-05)',
|
||||||
'Invalid NIP-05 address': 'Indirizzo NIP-05 non valido',
|
'Invalid NIP-05 address': 'Indirizzo NIP-05 non valido',
|
||||||
'Copy private key': 'Copia la chiave privata',
|
'Copy private key': 'Copia la chiave privata',
|
||||||
'Enter the password to decrypt your ncryptsec': 'Inserisci la password per decriptare la tua ncryptsec',
|
'Enter the password to decrypt your ncryptsec':
|
||||||
|
'Inserisci la password per decriptare la tua ncryptsec',
|
||||||
Back: 'Indietro',
|
Back: 'Indietro',
|
||||||
'optional: encrypt nsec': 'opzione: cripta nsec',
|
'optional: encrypt nsec': 'opzione: cripta nsec',
|
||||||
password: 'password',
|
password: 'password',
|
||||||
@@ -205,11 +198,14 @@ export default {
|
|||||||
'Earlier notifications': 'Notifiche precedenti',
|
'Earlier notifications': 'Notifiche precedenti',
|
||||||
'Temporarily display this note': 'Visualizza temporaneamente questa nota',
|
'Temporarily display this note': 'Visualizza temporaneamente questa nota',
|
||||||
buttonFollowing: 'Seguendo',
|
buttonFollowing: 'Seguendo',
|
||||||
'Are you sure you want to unfollow this user?': 'Sei sicuro di voler disiscrivere questo utente?',
|
'Are you sure you want to unfollow this user?':
|
||||||
|
'Sei sicuro di voler disiscrivere questo utente?',
|
||||||
'Recent Supporters': 'Recenti Sostenitori',
|
'Recent Supporters': 'Recenti Sostenitori',
|
||||||
'Seen on': 'Visto su',
|
'Seen on': 'Visto su',
|
||||||
'Temporarily display this reply': 'Mostra temporaneamente questa replica',
|
'Temporarily display this reply': 'Mostra temporaneamente questa replica',
|
||||||
'Not found the note': 'Non è stata trovata la nota',
|
'Not found the note': 'Non è stata trovata la nota',
|
||||||
'no more replies': 'niente più repliche'
|
'no more replies': 'niente più repliche',
|
||||||
|
'Relay sets': 'Set di Relay',
|
||||||
|
'Favorite Relays': 'Relay preferiti'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export default {
|
|||||||
Search: '検索',
|
Search: '検索',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'接続しているリレイは検索をサポートしていません',
|
'接続しているリレイは検索をサポートしていません',
|
||||||
'supports search': '検索対応',
|
|
||||||
'Show more...': 'さらに表示...',
|
'Show more...': 'さらに表示...',
|
||||||
'All users': '全ユーザー',
|
'All users': '全ユーザー',
|
||||||
'Display replies': '返信を表示',
|
'Display replies': '返信を表示',
|
||||||
@@ -117,14 +116,6 @@ export default {
|
|||||||
'R & W': '読&書',
|
'R & W': '読&書',
|
||||||
Read: '読む',
|
Read: '読む',
|
||||||
Write: '書く',
|
Write: '書く',
|
||||||
'Push to relays': 'リレイへプッシュ',
|
|
||||||
'Push Successful': 'プッシュ成功',
|
|
||||||
'Successfully pushed relay sets to relays': 'リレイセットをリレイへ正常にプッシュしました',
|
|
||||||
'Pull from relays': 'リレイからプル',
|
|
||||||
'Select the relay sets you want to pull': 'プルするリレイセットを選択',
|
|
||||||
'No relay sets found': 'リレイセットが見つかりません',
|
|
||||||
'Pull n relay sets': '{{n}} 個のリレイセットをプル',
|
|
||||||
Pull: 'プル',
|
|
||||||
'Select all': 'すべて選択',
|
'Select all': 'すべて選択',
|
||||||
'Relay Sets': 'リレイセット',
|
'Relay Sets': 'リレイセット',
|
||||||
'Read & Write Relays': '読み&書きリレイ',
|
'Read & Write Relays': '読み&書きリレイ',
|
||||||
@@ -211,6 +202,8 @@ export default {
|
|||||||
'Seen on': '見た',
|
'Seen on': '見た',
|
||||||
'Temporarily display this reply': 'この返信を一時的に表示',
|
'Temporarily display this reply': 'この返信を一時的に表示',
|
||||||
'Not found the note': 'ノートが見つかりません',
|
'Not found the note': 'ノートが見つかりません',
|
||||||
'no more replies': 'これ以上の返信はありません'
|
'no more replies': 'これ以上の返信はありません',
|
||||||
|
'Relay sets': 'リレイセット',
|
||||||
|
'Favorite Relays': 'お気に入りのリレイ'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export default {
|
|||||||
Search: 'Wyszukiwarka',
|
Search: 'Wyszukiwarka',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'Podłączone transmitery nie obsługują wyszukiwania',
|
'Podłączone transmitery nie obsługują wyszukiwania',
|
||||||
'supports search': 'Obsługa wyszukiwania',
|
|
||||||
'Show more...': 'Więcej...',
|
'Show more...': 'Więcej...',
|
||||||
'All users': 'Wszyscy użytkownicy',
|
'All users': 'Wszyscy użytkownicy',
|
||||||
'Display replies': 'Wyświetl komentarze',
|
'Display replies': 'Wyświetl komentarze',
|
||||||
@@ -116,14 +115,6 @@ export default {
|
|||||||
'R & W': 'O & Z',
|
'R & W': 'O & Z',
|
||||||
Read: 'Odczyt',
|
Read: 'Odczyt',
|
||||||
Write: 'Zapis',
|
Write: 'Zapis',
|
||||||
'Push to relays': 'Wyślij do transmiterów',
|
|
||||||
'Push Successful': 'Wysłano Pomyślnie',
|
|
||||||
'Successfully pushed relay sets to relays': 'Pomyślnie wysłano zestaw do transmiterów',
|
|
||||||
'Pull from relays': 'Pobierz z transmiterów',
|
|
||||||
'Select the relay sets you want to pull': 'Wybierz zestaw transmiterów do pobrania',
|
|
||||||
'No relay sets found': 'Nie znaleziono zestawu transmiterów',
|
|
||||||
'Pull n relay sets': 'Pobierz {{n}} zestawów transmiterów',
|
|
||||||
Pull: 'Pobierz',
|
|
||||||
'Select all': 'Wszystkie',
|
'Select all': 'Wszystkie',
|
||||||
'Relay Sets': 'Grupy transmiterów',
|
'Relay Sets': 'Grupy transmiterów',
|
||||||
'Read & Write Relays': 'Transmitery zapisu i odczytu',
|
'Read & Write Relays': 'Transmitery zapisu i odczytu',
|
||||||
@@ -212,6 +203,8 @@ export default {
|
|||||||
'Seen on': 'Widziany na',
|
'Seen on': 'Widziany na',
|
||||||
'Temporarily display this reply': 'Tymczasowo wyświetl tę odpowiedź',
|
'Temporarily display this reply': 'Tymczasowo wyświetl tę odpowiedź',
|
||||||
'Not found the note': 'Nie znaleziono wpisu',
|
'Not found the note': 'Nie znaleziono wpisu',
|
||||||
'no more replies': 'brak kolejnych odpowiedzi'
|
'no more replies': 'brak kolejnych odpowiedzi',
|
||||||
|
'Relay sets': 'Zestawy transmiterów',
|
||||||
|
'Favorite Relays': 'Ulubione transmitery'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ export default {
|
|||||||
Search: 'Pesquisar',
|
Search: 'Pesquisar',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'Os relés aos quais você está conectado não suportam pesquisa',
|
'Os relés aos quais você está conectado não suportam pesquisa',
|
||||||
'supports search': 'suporta pesquisa',
|
|
||||||
'Show more...': 'Mostrar mais...',
|
'Show more...': 'Mostrar mais...',
|
||||||
'All users': 'Todos os usuários',
|
'All users': 'Todos os usuários',
|
||||||
'Display replies': 'Exibir respostas',
|
'Display replies': 'Exibir respostas',
|
||||||
@@ -116,14 +115,6 @@ export default {
|
|||||||
'R & W': 'Leitura & Escrita',
|
'R & W': 'Leitura & Escrita',
|
||||||
Read: 'Ler',
|
Read: 'Ler',
|
||||||
Write: 'Escrever',
|
Write: 'Escrever',
|
||||||
'Push to relays': 'Enviar para relés',
|
|
||||||
'Push Successful': 'Envio bem-sucedido',
|
|
||||||
'Successfully pushed relay sets to relays': 'Conjuntos de relé enviados com sucesso',
|
|
||||||
'Pull from relays': 'Receber de relés',
|
|
||||||
'Select the relay sets you want to pull': 'Selecione os conjuntos de relé que deseja receber',
|
|
||||||
'No relay sets found': 'Nenhum conjunto de relé encontrado',
|
|
||||||
'Pull n relay sets': 'Receber {{n}} conjuntos de relé',
|
|
||||||
Pull: 'Receber',
|
|
||||||
'Select all': 'Selecionar todos',
|
'Select all': 'Selecionar todos',
|
||||||
'Relay Sets': 'Conjuntos de relé',
|
'Relay Sets': 'Conjuntos de relé',
|
||||||
'Read & Write Relays': 'Relés de Leitura & Escrita',
|
'Read & Write Relays': 'Relés de Leitura & Escrita',
|
||||||
@@ -212,6 +203,8 @@ export default {
|
|||||||
'Seen on': 'Visto em',
|
'Seen on': 'Visto em',
|
||||||
'Temporarily display this reply': 'Exibir temporariamente esta resposta',
|
'Temporarily display this reply': 'Exibir temporariamente esta resposta',
|
||||||
'Not found the note': 'Nota não encontrada',
|
'Not found the note': 'Nota não encontrada',
|
||||||
'no more replies': 'não há mais respostas'
|
'no more replies': 'não há mais respostas',
|
||||||
|
'Relay sets': 'Conjuntos de relé',
|
||||||
|
'Favorite Relays': 'Relés favoritos'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export default {
|
|||||||
Search: 'Pesquisar',
|
Search: 'Pesquisar',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'Os relés aos quais você está conectado não suportam pesquisa',
|
'Os relés aos quais você está conectado não suportam pesquisa',
|
||||||
'supports search': 'suporta pesquisa',
|
|
||||||
'Show more...': 'Mostrar mais...',
|
'Show more...': 'Mostrar mais...',
|
||||||
'All users': 'Todos os usuários',
|
'All users': 'Todos os usuários',
|
||||||
'Display replies': 'Exibir respostas',
|
'Display replies': 'Exibir respostas',
|
||||||
@@ -117,14 +116,6 @@ export default {
|
|||||||
'R & W': 'Leitura & Escrita',
|
'R & W': 'Leitura & Escrita',
|
||||||
Read: 'Ler',
|
Read: 'Ler',
|
||||||
Write: 'Escrever',
|
Write: 'Escrever',
|
||||||
'Push to relays': 'Enviar para relés',
|
|
||||||
'Push Successful': 'Envio Bem-sucedido',
|
|
||||||
'Successfully pushed relay sets to relays': 'Conjuntos de relé enviados com sucesso',
|
|
||||||
'Pull from relays': 'Receber de relés',
|
|
||||||
'Select the relay sets you want to pull': 'Selecione os conjuntos de relé que deseja receber',
|
|
||||||
'No relay sets found': 'Nenhum conjunto de relé encontrado',
|
|
||||||
'Pull n relay sets': 'Receber {{n}} conjuntos de relé',
|
|
||||||
Pull: 'Receber',
|
|
||||||
'Select all': 'Selecionar todos',
|
'Select all': 'Selecionar todos',
|
||||||
'Relay Sets': 'Conjuntos de relé',
|
'Relay Sets': 'Conjuntos de relé',
|
||||||
'Read & Write Relays': 'Relés de Leitura & Escrita',
|
'Read & Write Relays': 'Relés de Leitura & Escrita',
|
||||||
@@ -213,6 +204,8 @@ export default {
|
|||||||
'Seen on': 'Visto em',
|
'Seen on': 'Visto em',
|
||||||
'Temporarily display this reply': 'Exibir temporariamente esta resposta',
|
'Temporarily display this reply': 'Exibir temporariamente esta resposta',
|
||||||
'Not found the note': 'Nota não encontrada',
|
'Not found the note': 'Nota não encontrada',
|
||||||
'no more replies': 'não há mais respostas'
|
'no more replies': 'não há mais respostas',
|
||||||
|
'Relay sets': 'Conjuntos de Relé',
|
||||||
|
'Favorite Relays': 'Relés Favoritos'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export default {
|
|||||||
Search: 'Поиск',
|
Search: 'Поиск',
|
||||||
'The relays you are connected to do not support search':
|
'The relays you are connected to do not support search':
|
||||||
'Подключённые ретрансляторы не поддерживают поиск',
|
'Подключённые ретрансляторы не поддерживают поиск',
|
||||||
'supports search': 'поддерживает поиск',
|
|
||||||
'Show more...': 'Показать больше...',
|
'Show more...': 'Показать больше...',
|
||||||
'All users': 'Все пользователи',
|
'All users': 'Все пользователи',
|
||||||
'Display replies': 'Показать ответы',
|
'Display replies': 'Показать ответы',
|
||||||
@@ -118,14 +117,6 @@ export default {
|
|||||||
'R & W': 'Чтение & Запись',
|
'R & W': 'Чтение & Запись',
|
||||||
Read: 'Читать',
|
Read: 'Читать',
|
||||||
Write: 'Писать',
|
Write: 'Писать',
|
||||||
'Push to relays': 'Отправить на ретрансляторы',
|
|
||||||
'Push Successful': 'Отправка успешна',
|
|
||||||
'Successfully pushed relay sets to relays': 'Наборы ретрансляторов успешно отправлены',
|
|
||||||
'Pull from relays': 'Получить с ретрансляторов',
|
|
||||||
'Select the relay sets you want to pull': 'Выберите наборы ретрансляторов для получения',
|
|
||||||
'No relay sets found': 'Наборы ретрансляторов не найдены',
|
|
||||||
'Pull n relay sets': 'Получить {{n}} наборов ретрансляторов',
|
|
||||||
Pull: 'Получить',
|
|
||||||
'Select all': 'Выбрать все',
|
'Select all': 'Выбрать все',
|
||||||
'Relay Sets': 'Наборы ретрансляторов',
|
'Relay Sets': 'Наборы ретрансляторов',
|
||||||
'Read & Write Relays': 'Ретрансляторы для чтения и записи',
|
'Read & Write Relays': 'Ретрансляторы для чтения и записи',
|
||||||
@@ -214,6 +205,8 @@ export default {
|
|||||||
'Seen on': 'Просмотрено на',
|
'Seen on': 'Просмотрено на',
|
||||||
'Temporarily display this reply': 'Временно отобразить этот ответ',
|
'Temporarily display this reply': 'Временно отобразить этот ответ',
|
||||||
'Not found the note': 'Заметка не найдена',
|
'Not found the note': 'Заметка не найдена',
|
||||||
'no more replies': 'больше нет ответов'
|
'no more replies': 'больше нет ответов',
|
||||||
|
'Relay sets': 'Наборы ретрансляторов',
|
||||||
|
'Favorite Relays': 'Избранные ретрансляторы'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export default {
|
|||||||
'Reply to': '回复',
|
'Reply to': '回复',
|
||||||
Search: '搜索',
|
Search: '搜索',
|
||||||
'The relays you are connected to do not support search': '您连接的服务器不支持搜索',
|
'The relays you are connected to do not support search': '您连接的服务器不支持搜索',
|
||||||
'supports search': '支持搜索',
|
|
||||||
'Show more...': '查看更多...',
|
'Show more...': '查看更多...',
|
||||||
'All users': '所有用户',
|
'All users': '所有用户',
|
||||||
'Display replies': '显示回复',
|
'Display replies': '显示回复',
|
||||||
@@ -116,14 +115,6 @@ export default {
|
|||||||
'R & W': '读写',
|
'R & W': '读写',
|
||||||
Read: '只读',
|
Read: '只读',
|
||||||
Write: '只写',
|
Write: '只写',
|
||||||
'Push to relays': '保存到服务器',
|
|
||||||
'Push Successful': '保存成功',
|
|
||||||
'Successfully pushed relay sets to relays': '成功保存到服务器',
|
|
||||||
'Pull from relays': '从服务器拉取',
|
|
||||||
'Select the relay sets you want to pull': '选择要拉取的服务器组',
|
|
||||||
'No relay sets found': '未找到服务器组',
|
|
||||||
'Pull n relay sets': '拉取 {{n}} 个服务器组',
|
|
||||||
Pull: '拉取',
|
|
||||||
'Select all': '全选',
|
'Select all': '全选',
|
||||||
'Relay Sets': '服务器组',
|
'Relay Sets': '服务器组',
|
||||||
Mailbox: '邮箱',
|
Mailbox: '邮箱',
|
||||||
@@ -211,6 +202,8 @@ export default {
|
|||||||
'Seen on': '来自',
|
'Seen on': '来自',
|
||||||
'Temporarily display this reply': '临时显示此回复',
|
'Temporarily display this reply': '临时显示此回复',
|
||||||
'Not found the note': '未找到该笔记',
|
'Not found the note': '未找到该笔记',
|
||||||
'no more replies': '没有更多回复了'
|
'no more replies': '没有更多回复了',
|
||||||
|
'Relay sets': '服务器组',
|
||||||
|
'Favorite Relays': '收藏的服务器'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants'
|
import { ExtendedKind } from '@/constants'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import { TDraftEvent, TMailboxRelay, TRelaySet } from '@/types'
|
import { TDraftEvent, TMailboxRelay, TRelaySet } from '@/types'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@@ -150,7 +150,7 @@ export async function createPictureNoteDraftEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: PICTURE_EVENT_KIND,
|
kind: ExtendedKind.PICTURE,
|
||||||
content,
|
content,
|
||||||
tags,
|
tags,
|
||||||
created_at: dayjs().unix()
|
created_at: dayjs().unix()
|
||||||
@@ -210,7 +210,7 @@ export async function createCommentDraftEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: COMMENT_EVENT_KIND,
|
kind: ExtendedKind.COMMENT,
|
||||||
content,
|
content,
|
||||||
tags,
|
tags,
|
||||||
created_at: dayjs().unix()
|
created_at: dayjs().unix()
|
||||||
@@ -255,6 +255,25 @@ export function createProfileDraftEvent(content: string, tags: string[][] = []):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createFavoriteRelaysDraftEvent(
|
||||||
|
favoriteRelays: string[],
|
||||||
|
relaySetEvents: Event[]
|
||||||
|
): TDraftEvent {
|
||||||
|
const tags: string[][] = []
|
||||||
|
favoriteRelays.forEach((url) => {
|
||||||
|
tags.push(['relay', url])
|
||||||
|
})
|
||||||
|
relaySetEvents.forEach((event) => {
|
||||||
|
tags.push(['a', getEventCoordinate(event)])
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
kind: ExtendedKind.FAVORITE_RELAYS,
|
||||||
|
content: '',
|
||||||
|
tags,
|
||||||
|
created_at: dayjs().unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function generateImetaTags(imageUrls: string[], pictureInfos: { url: string; tags: string[][] }[]) {
|
function generateImetaTags(imageUrls: string[], pictureInfos: { url: string; tags: string[][] }[]) {
|
||||||
return imageUrls.map((imageUrl) => {
|
return imageUrls.map((imageUrl) => {
|
||||||
const pictureInfo = pictureInfos.find((info) => info.url === imageUrl)
|
const pictureInfo = pictureInfos.find((info) => info.url === imageUrl)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BIG_RELAY_URLS, COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants'
|
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import { TImageInfo, TRelayList } from '@/types'
|
import { TImageInfo, TRelayList, TRelaySet } from '@/types'
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from 'lru-cache'
|
||||||
import { Event, kinds, nip19 } from 'nostr-tools'
|
import { Event, kinds, nip19 } from 'nostr-tools'
|
||||||
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
|
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
|
||||||
@@ -47,11 +47,11 @@ export function isReplyNoteEvent(event: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isCommentEvent(event: Event) {
|
export function isCommentEvent(event: Event) {
|
||||||
return event.kind === COMMENT_EVENT_KIND
|
return event.kind === ExtendedKind.COMMENT
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPictureEvent(event: Event) {
|
export function isPictureEvent(event: Event) {
|
||||||
return event.kind === PICTURE_EVENT_KIND
|
return event.kind === ExtendedKind.PICTURE
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isProtectedEvent(event: Event) {
|
export function isProtectedEvent(event: Event) {
|
||||||
@@ -59,7 +59,7 @@ export function isProtectedEvent(event: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isSupportedKind(kind: number) {
|
export function isSupportedKind(kind: number) {
|
||||||
return [kinds.ShortTextNote, PICTURE_EVENT_KIND].includes(kind)
|
return [kinds.ShortTextNote, ExtendedKind.PICTURE].includes(kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getParentEventTag(event?: Event) {
|
export function getParentEventTag(event?: Event) {
|
||||||
@@ -195,6 +195,22 @@ export function getProfileFromProfileEvent(event: Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRelaySetFromRelaySetEvent(event: Event): TRelaySet {
|
||||||
|
const id = getReplaceableEventIdentifier(event)
|
||||||
|
const relayUrls = event.tags
|
||||||
|
.filter(tagNameEquals('relay'))
|
||||||
|
.map((tag) => tag[1])
|
||||||
|
.filter((url) => url && isWebsocketUrl(url))
|
||||||
|
.map((url) => normalizeUrl(url))
|
||||||
|
|
||||||
|
let name = event.tags.find(tagNameEquals('title'))?.[1]
|
||||||
|
if (!name) {
|
||||||
|
name = id
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id, name, relayUrls }
|
||||||
|
}
|
||||||
|
|
||||||
export async function extractMentions(content: string, parentEvent?: Event) {
|
export async function extractMentions(content: string, parentEvent?: Event) {
|
||||||
const parentEventPubkey = parentEvent ? parentEvent.pubkey : undefined
|
const parentEventPubkey = parentEvent ? parentEvent.pubkey : undefined
|
||||||
const pubkeys: string[] = []
|
const pubkeys: string[] = []
|
||||||
@@ -485,3 +501,7 @@ export function extractEmbeddedEventIds(event: Event) {
|
|||||||
export function getLatestEvent(events: Event[]) {
|
export function getLatestEvent(events: Event[]) {
|
||||||
return events.sort((a, b) => b.created_at - a.created_at)[0]
|
return events.sort((a, b) => b.created_at - a.created_at)[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getReplaceableEventIdentifier(event: Event) {
|
||||||
|
return event.tags.find(tagNameEquals('d'))?.[1] ?? ''
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const toOthersRelaySettings = (pubkey: string) => {
|
|||||||
const npub = nip19.npubEncode(pubkey)
|
const npub = nip19.npubEncode(pubkey)
|
||||||
return `/users/${npub}/relays`
|
return `/users/${npub}/relays`
|
||||||
}
|
}
|
||||||
export const toRelaySettings = (tag?: 'mailbox' | 'relay-sets') => {
|
export const toRelaySettings = (tag?: 'mailbox' | 'favorite-relays') => {
|
||||||
return '/relay-settings' + (tag ? '#' + tag : '')
|
return '/relay-settings' + (tag ? '#' + tag : '')
|
||||||
}
|
}
|
||||||
export const toSettings = () => '/settings'
|
export const toSettings = () => '/settings'
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ import { Separator } from '@/components/ui/separator'
|
|||||||
import { SimpleUserAvatar } from '@/components/UserAvatar'
|
import { SimpleUserAvatar } from '@/components/UserAvatar'
|
||||||
import { SimpleUsername } from '@/components/Username'
|
import { SimpleUsername } from '@/components/Username'
|
||||||
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
||||||
import { toProfile, toSettings, toWallet } from '@/lib/link'
|
import { toProfile, toRelaySettings, toSettings, toWallet } from '@/lib/link'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useSecondaryPage } from '@/PageManager'
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import { ArrowDownUp, ChevronRight, LogOut, Settings, UserRound, Wallet } from 'lucide-react'
|
import {
|
||||||
|
ArrowDownUp,
|
||||||
|
ChevronRight,
|
||||||
|
LogOut,
|
||||||
|
Server,
|
||||||
|
Settings,
|
||||||
|
UserRound,
|
||||||
|
Wallet
|
||||||
|
} from 'lucide-react'
|
||||||
import { forwardRef, HTMLProps, useState } from 'react'
|
import { forwardRef, HTMLProps, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@@ -54,6 +62,9 @@ const MePage = forwardRef((_, ref) => {
|
|||||||
<UserRound />
|
<UserRound />
|
||||||
{t('Profile')}
|
{t('Profile')}
|
||||||
</Item>
|
</Item>
|
||||||
|
<Item onClick={() => push(toRelaySettings())}>
|
||||||
|
<Server /> {t('Relays')}
|
||||||
|
</Item>
|
||||||
<Item onClick={() => push(toWallet())}>
|
<Item onClick={() => push(toWallet())}>
|
||||||
<Wallet />
|
<Wallet />
|
||||||
{t('Wallet')}
|
{t('Wallet')}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { Drawer, DrawerContent } from '@/components/ui/drawer'
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
import { simplifyUrl } from '@/lib/url'
|
import { simplifyUrl } from '@/lib/url'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
|
||||||
import { useFeed } from '@/providers/FeedProvider'
|
import { useFeed } from '@/providers/FeedProvider'
|
||||||
import { useRelaySets } from '@/providers/RelaySetsProvider'
|
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { ChevronDown, Server, UsersRound } from 'lucide-react'
|
import { ChevronDown, Server, UsersRound } from 'lucide-react'
|
||||||
import { forwardRef, HTMLAttributes, useState } from 'react'
|
import { forwardRef, HTMLAttributes, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function FeedButton({ className }: { className?: string }) {
|
export default function FeedButton({ className }: { className?: string }) {
|
||||||
@@ -20,7 +20,7 @@ export default function FeedButton({ className }: { className?: string }) {
|
|||||||
<FeedSwitcherTrigger className={className} onClick={() => setOpen(true)} />
|
<FeedSwitcherTrigger className={className} onClick={() => setOpen(true)} />
|
||||||
<Drawer open={open} onOpenChange={setOpen}>
|
<Drawer open={open} onOpenChange={setOpen}>
|
||||||
<DrawerContent className="max-h-[80vh]">
|
<DrawerContent className="max-h-[80vh]">
|
||||||
<div className="p-4 overflow-auto">
|
<div className="py-4 px-2 overflow-auto">
|
||||||
<FeedSwitcher close={() => setOpen(false)} />
|
<FeedSwitcher close={() => setOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
@@ -44,21 +44,32 @@ export default function FeedButton({ className }: { className?: string }) {
|
|||||||
const FeedSwitcherTrigger = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
const FeedSwitcherTrigger = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { feedType, relayUrls, activeRelaySetId } = useFeed()
|
const { feedInfo, relayUrls } = useFeed()
|
||||||
const { relaySets } = useRelaySets()
|
const { relaySets } = useFavoriteRelays()
|
||||||
const activeRelaySet = activeRelaySetId
|
const activeRelaySet = useMemo(() => {
|
||||||
? relaySets.find((set) => set.id === activeRelaySetId)
|
return feedInfo.feedType === 'relays' && feedInfo.id
|
||||||
|
? relaySets.find((set) => set.id === feedInfo.id)
|
||||||
: undefined
|
: undefined
|
||||||
const title =
|
}, [feedInfo, relaySets])
|
||||||
feedType === 'following'
|
const title = useMemo(() => {
|
||||||
? t('Following')
|
if (feedInfo.feedType === 'following') {
|
||||||
: relayUrls.length > 0
|
return t('Following')
|
||||||
? relayUrls.length === 1
|
}
|
||||||
|
if (relayUrls.length === 0) {
|
||||||
|
return t('Choose a relay')
|
||||||
|
}
|
||||||
|
if (feedInfo.feedType === 'relay') {
|
||||||
|
return simplifyUrl(feedInfo.id ?? '')
|
||||||
|
}
|
||||||
|
if (feedInfo.feedType === 'relays') {
|
||||||
|
return activeRelaySet?.name ?? activeRelaySet?.id
|
||||||
|
}
|
||||||
|
if (feedInfo.feedType === 'temporary') {
|
||||||
|
return relayUrls.length === 1
|
||||||
? simplifyUrl(relayUrls[0])
|
? simplifyUrl(relayUrls[0])
|
||||||
: activeRelaySet
|
: (activeRelaySet?.name ?? t('Temporary'))
|
||||||
? activeRelaySet.name
|
}
|
||||||
: t('Temporary')
|
}, [feedInfo, activeRelaySet])
|
||||||
: t('Choose a relay set')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -66,7 +77,7 @@ const FeedSwitcherTrigger = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEle
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{feedType === 'following' ? <UsersRound /> : <Server />}
|
{feedInfo.feedType === 'following' ? <UsersRound /> : <Server />}
|
||||||
<div className="text-lg font-semibold truncate">{title}</div>
|
<div className="text-lg font-semibold truncate">{title}</div>
|
||||||
<ChevronDown />
|
<ChevronDown />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ const NoteListPage = forwardRef((_, ref) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const layoutRef = useRef<TPageRef>(null)
|
const layoutRef = useRef<TPageRef>(null)
|
||||||
const { pubkey, checkLogin } = useNostr()
|
const { pubkey, checkLogin } = useNostr()
|
||||||
const { feedType, relayUrls, isReady, filter } = useFeed()
|
const { feedInfo, relayUrls, isReady, filter } = useFeed()
|
||||||
useImperativeHandle(ref, () => layoutRef.current)
|
useImperativeHandle(ref, () => layoutRef.current)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (layoutRef.current) {
|
if (layoutRef.current) {
|
||||||
layoutRef.current.scrollToTop()
|
layoutRef.current.scrollToTop()
|
||||||
}
|
}
|
||||||
}, [JSON.stringify(relayUrls), feedType])
|
}, [JSON.stringify(relayUrls), feedInfo])
|
||||||
|
|
||||||
let content = <div className="text-center text-sm text-muted-foreground">{t('loading...')}</div>
|
let content = <div className="text-center text-sm text-muted-foreground">{t('loading...')}</div>
|
||||||
if (feedType === 'following' && !pubkey) {
|
if (feedInfo.feedType === 'following' && !pubkey) {
|
||||||
content = (
|
content = (
|
||||||
<div className="flex justify-center w-full">
|
<div className="flex justify-center w-full">
|
||||||
<Button size="lg" onClick={() => checkLogin()}>
|
<Button size="lg" onClick={() => checkLogin()}>
|
||||||
@@ -40,7 +40,7 @@ const NoteListPage = forwardRef((_, ref) => {
|
|||||||
<NoteList
|
<NoteList
|
||||||
relayUrls={relayUrls}
|
relayUrls={relayUrls}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
needCheckAlgoRelay={feedType !== 'following'}
|
needCheckAlgoRelay={feedInfo.feedType !== 'following'}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,9 @@ const NoteListPage = forwardRef((_, ref) => {
|
|||||||
pageName="home"
|
pageName="home"
|
||||||
ref={layoutRef}
|
ref={layoutRef}
|
||||||
titlebar={
|
titlebar={
|
||||||
<NoteListPageTitlebar temporaryRelayUrls={feedType === 'temporary' ? relayUrls : []} />
|
<NoteListPageTitlebar
|
||||||
|
temporaryRelayUrls={feedInfo.feedType === 'temporary' ? relayUrls : []}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
displayScrollToTopButton
|
displayScrollToTopButton
|
||||||
>
|
>
|
||||||
@@ -67,7 +69,7 @@ function NoteListPageTitlebar({ temporaryRelayUrls = [] }: { temporaryRelayUrls?
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-1 items-center h-full justify-between">
|
<div className="flex gap-1 items-center h-full justify-between">
|
||||||
<FeedButton className="flex-1 max-w-fit w-0" />
|
<FeedButton className="flex-1 max-w-fit w-0" />
|
||||||
<div className="shrink-0">
|
<div className="shrink-0 flex gap-1 items-center">
|
||||||
{temporaryRelayUrls.length > 0 && (
|
{temporaryRelayUrls.length > 0 && (
|
||||||
<SaveRelayDropdownMenu urls={temporaryRelayUrls} atTitlebar />
|
<SaveRelayDropdownMenu urls={temporaryRelayUrls} atTitlebar />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import MailboxSetting from '@/components/MailboxSetting'
|
import MailboxSetting from '@/components/MailboxSetting'
|
||||||
import RelaySetsSetting from '@/components/RelaySetsSetting'
|
import FavoriteRelaysSetting from '@/components/FavoriteRelaysSetting'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||||
import { forwardRef, useEffect, useState } from 'react'
|
import { forwardRef, useEffect, useState } from 'react'
|
||||||
@@ -7,28 +7,28 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
const RelaySettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
const RelaySettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [tabValue, setTabValue] = useState('relay-sets')
|
const [tabValue, setTabValue] = useState('favorite-relays')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (window.location.hash) {
|
switch (window.location.hash) {
|
||||||
case '#mailbox':
|
case '#mailbox':
|
||||||
setTabValue('mailbox')
|
setTabValue('mailbox')
|
||||||
break
|
break
|
||||||
case '#relay-sets':
|
case '#favorite-relays':
|
||||||
setTabValue('relay-sets')
|
setTabValue('favorite-relays')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SecondaryPageLayout ref={ref} index={index} title={t('Relay settings')}>
|
<SecondaryPageLayout ref={ref} index={index} title={t('Relay settings')}>
|
||||||
<Tabs value={tabValue} onValueChange={setTabValue} className="px-4 space-y-4">
|
<Tabs value={tabValue} onValueChange={setTabValue} className="px-4 pb-4 space-y-4">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="relay-sets">{t('Relay Sets')}</TabsTrigger>
|
<TabsTrigger value="favorite-relays">{t('Favorite Relays')}</TabsTrigger>
|
||||||
<TabsTrigger value="mailbox">{t('Read & Write Relays')}</TabsTrigger>
|
<TabsTrigger value="mailbox">{t('Read & Write Relays')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="relay-sets">
|
<TabsContent value="favorite-relays">
|
||||||
<RelaySetsSetting />
|
<FavoriteRelaysSetting />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="mailbox">
|
<TabsContent value="mailbox">
|
||||||
<MailboxSetting />
|
<MailboxSetting />
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ const SettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem onClick={() => push(toRelaySettings())}>
|
<SettingItem className="clickable" onClick={() => push(toRelaySettings())}>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Server />
|
<Server />
|
||||||
<div>{t('Relays')}</div>
|
<div>{t('Relays')}</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight />
|
<ChevronRight />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem onClick={() => push(toWallet())}>
|
<SettingItem className="clickable" onClick={() => push(toWallet())}>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Wallet />
|
<Wallet />
|
||||||
<div>{t('Wallet')}</div>
|
<div>{t('Wallet')}</div>
|
||||||
@@ -89,6 +89,7 @@ const SettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
{!!nsec && (
|
{!!nsec && (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
|
className="clickable"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(nsec)
|
navigator.clipboard.writeText(nsec)
|
||||||
setCopiedNsec(true)
|
setCopiedNsec(true)
|
||||||
@@ -104,6 +105,7 @@ const SettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
)}
|
)}
|
||||||
{!!ncryptsec && (
|
{!!ncryptsec && (
|
||||||
<SettingItem
|
<SettingItem
|
||||||
|
className="clickable"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(ncryptsec)
|
navigator.clipboard.writeText(ncryptsec)
|
||||||
setCopiedNcryptsec(true)
|
setCopiedNcryptsec(true)
|
||||||
@@ -118,7 +120,7 @@ const SettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
<AboutInfoDialog>
|
<AboutInfoDialog>
|
||||||
<SettingItem>
|
<SettingItem className="clickable">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Info />
|
<Info />
|
||||||
<div>{t('About')}</div>
|
<div>{t('About')}</div>
|
||||||
@@ -145,7 +147,7 @@ const SettingItem = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex clickable justify-between items-center px-4 py-2 h-[52px] rounded-lg [&_svg]:size-4 [&_svg]:shrink-0',
|
'flex justify-between select-none items-center px-4 py-2 h-[52px] rounded-lg [&_svg]:size-4 [&_svg]:shrink-0',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
226
src/providers/FavoriteRelaysProvider.tsx
Normal file
226
src/providers/FavoriteRelaysProvider.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { BIG_RELAY_URLS, DEFAULT_FAVORITE_RELAYS } from '@/constants'
|
||||||
|
import { createFavoriteRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event'
|
||||||
|
import { getRelaySetFromRelaySetEvent, getReplaceableEventIdentifier } from '@/lib/event'
|
||||||
|
import { randomString } from '@/lib/random'
|
||||||
|
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||||
|
import client from '@/services/client.service'
|
||||||
|
import indexedDb from '@/services/indexed-db.service'
|
||||||
|
import storage from '@/services/local-storage.service'
|
||||||
|
import { TRelaySet } from '@/types'
|
||||||
|
import { Event, kinds } from 'nostr-tools'
|
||||||
|
import { createContext, useContext, useEffect, useState } from 'react'
|
||||||
|
import { useNostr } from './NostrProvider'
|
||||||
|
|
||||||
|
type TFavoriteRelaysContext = {
|
||||||
|
favoriteRelays: string[]
|
||||||
|
addFavoriteRelays: (relayUrls: string[]) => Promise<void>
|
||||||
|
deleteFavoriteRelays: (relayUrls: string[]) => Promise<void>
|
||||||
|
relaySets: TRelaySet[]
|
||||||
|
addRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise<void>
|
||||||
|
deleteRelaySet: (id: string) => Promise<void>
|
||||||
|
updateRelaySet: (newSet: TRelaySet) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const FavoriteRelaysContext = createContext<TFavoriteRelaysContext | undefined>(undefined)
|
||||||
|
|
||||||
|
export const useFavoriteRelays = () => {
|
||||||
|
const context = useContext(FavoriteRelaysContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useFavoriteRelays must be used within a FavoriteRelaysProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FavoriteRelaysProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { favoriteRelaysEvent, updateFavoriteRelaysEvent, pubkey, relayList, publish } = useNostr()
|
||||||
|
const [favoriteRelays, setFavoriteRelays] = useState<string[]>([])
|
||||||
|
const [relaySetEvents, setRelaySetEvents] = useState<Event[]>([])
|
||||||
|
const [relaySets, setRelaySets] = useState<TRelaySet[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!favoriteRelaysEvent) {
|
||||||
|
const favoriteRelays: string[] = DEFAULT_FAVORITE_RELAYS
|
||||||
|
const storedRelaySets = storage.getRelaySets()
|
||||||
|
storedRelaySets.forEach(({ relayUrls }) => {
|
||||||
|
relayUrls.forEach((url) => {
|
||||||
|
if (!favoriteRelays.includes(url)) {
|
||||||
|
favoriteRelays.push(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
setFavoriteRelays(favoriteRelays)
|
||||||
|
setRelaySetEvents([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
const relays: string[] = []
|
||||||
|
const relaySetIds: string[] = []
|
||||||
|
|
||||||
|
favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => {
|
||||||
|
if (!tagValue) return
|
||||||
|
|
||||||
|
if (tagName === 'relay') {
|
||||||
|
const normalizedUrl = normalizeUrl(tagValue)
|
||||||
|
if (normalizedUrl && !relays.includes(normalizedUrl)) {
|
||||||
|
relays.push(normalizedUrl)
|
||||||
|
}
|
||||||
|
} else if (tagName === 'a') {
|
||||||
|
const [kind, author, relaySetId] = tagValue.split(':')
|
||||||
|
if (kind !== kinds.Relaysets.toString()) return
|
||||||
|
if (!pubkey || author !== pubkey) return // TODO: support others relay sets
|
||||||
|
if (!relaySetId) return
|
||||||
|
|
||||||
|
if (!relaySetIds.includes(relaySetId)) {
|
||||||
|
relaySetIds.push(relaySetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setFavoriteRelays(relays)
|
||||||
|
|
||||||
|
if (!pubkey) return
|
||||||
|
const relaySetEvents = await Promise.all(
|
||||||
|
relaySetIds.map((id) => indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id))
|
||||||
|
)
|
||||||
|
const nonExistingRelaySetIds = relaySetIds.filter((_, index) => {
|
||||||
|
return !relaySetEvents[index]
|
||||||
|
})
|
||||||
|
if (nonExistingRelaySetIds.length) {
|
||||||
|
const newRelaySetEvents = await client.fetchEvents(
|
||||||
|
(relayList?.write ?? []).concat(BIG_RELAY_URLS).slice(0, 5),
|
||||||
|
{
|
||||||
|
kinds: [kinds.Relaysets],
|
||||||
|
authors: [pubkey],
|
||||||
|
'#d': nonExistingRelaySetIds
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const relaySetEventMap = new Map<string, Event>()
|
||||||
|
newRelaySetEvents.forEach((event) => {
|
||||||
|
const d = getReplaceableEventIdentifier(event)
|
||||||
|
if (!d) return
|
||||||
|
|
||||||
|
const old = relaySetEventMap.get(d)
|
||||||
|
if (!old || old.created_at < event.created_at) {
|
||||||
|
relaySetEventMap.set(d, event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(relaySetEventMap.values()).map((event) => {
|
||||||
|
return indexedDb.putReplaceableEvent(event)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
nonExistingRelaySetIds.forEach((id) => {
|
||||||
|
const event = relaySetEventMap.get(id)
|
||||||
|
if (event) {
|
||||||
|
const index = relaySetIds.indexOf(id)
|
||||||
|
if (index !== -1) {
|
||||||
|
relaySetEvents[index] = event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setRelaySetEvents(relaySetEvents.filter(Boolean) as Event[])
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
}, [favoriteRelaysEvent])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRelaySets(
|
||||||
|
relaySetEvents.map((evt) => getRelaySetFromRelaySetEvent(evt)).filter(Boolean) as TRelaySet[]
|
||||||
|
)
|
||||||
|
}, [relaySetEvents])
|
||||||
|
|
||||||
|
const addFavoriteRelays = async (relayUrls: string[]) => {
|
||||||
|
const normalizedUrls = relayUrls
|
||||||
|
.map((relayUrl) => normalizeUrl(relayUrl))
|
||||||
|
.filter((url) => !!url && !favoriteRelays.includes(url))
|
||||||
|
if (!normalizedUrls.length) return
|
||||||
|
|
||||||
|
const draftEvent = createFavoriteRelaysDraftEvent(
|
||||||
|
[...favoriteRelays, ...normalizedUrls],
|
||||||
|
relaySetEvents
|
||||||
|
)
|
||||||
|
const newFavoriteRelaysEvent = await publish(draftEvent)
|
||||||
|
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteFavoriteRelays = async (relayUrls: string[]) => {
|
||||||
|
const normalizedUrls = relayUrls
|
||||||
|
.map((relayUrl) => normalizeUrl(relayUrl))
|
||||||
|
.filter((url) => !!url && favoriteRelays.includes(url))
|
||||||
|
if (!normalizedUrls.length) return
|
||||||
|
|
||||||
|
const draftEvent = createFavoriteRelaysDraftEvent(
|
||||||
|
favoriteRelays.filter((url) => !normalizedUrls.includes(url)),
|
||||||
|
relaySetEvents
|
||||||
|
)
|
||||||
|
const newFavoriteRelaysEvent = await publish(draftEvent)
|
||||||
|
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addRelaySet = async (relaySetName: string, relayUrls: string[] = []) => {
|
||||||
|
const normalizedUrls = relayUrls
|
||||||
|
.map((url) => normalizeUrl(url))
|
||||||
|
.filter((url) => isWebsocketUrl(url))
|
||||||
|
const id = randomString()
|
||||||
|
const relaySetDraftEvent = createRelaySetDraftEvent({
|
||||||
|
id,
|
||||||
|
name: relaySetName,
|
||||||
|
relayUrls: normalizedUrls
|
||||||
|
})
|
||||||
|
const newRelaySetEvent = await publish(relaySetDraftEvent)
|
||||||
|
await indexedDb.putReplaceableEvent(newRelaySetEvent)
|
||||||
|
|
||||||
|
const favoriteRelaysDraftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, [
|
||||||
|
...relaySetEvents,
|
||||||
|
newRelaySetEvent
|
||||||
|
])
|
||||||
|
const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
|
||||||
|
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRelaySet = async (id: string) => {
|
||||||
|
const newRelaySetEvents = relaySetEvents.filter((event) => {
|
||||||
|
return getReplaceableEventIdentifier(event) !== id
|
||||||
|
})
|
||||||
|
if (newRelaySetEvents.length === relaySetEvents.length) return
|
||||||
|
|
||||||
|
const draftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, newRelaySetEvents)
|
||||||
|
const newFavoriteRelaysEvent = await publish(draftEvent)
|
||||||
|
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRelaySet = async (newSet: TRelaySet) => {
|
||||||
|
const draftEvent = createRelaySetDraftEvent(newSet)
|
||||||
|
const newRelaySetEvent = await publish(draftEvent)
|
||||||
|
await indexedDb.putReplaceableEvent(newRelaySetEvent)
|
||||||
|
|
||||||
|
setRelaySetEvents((prev) => {
|
||||||
|
return prev.map((event) => {
|
||||||
|
if (getReplaceableEventIdentifier(event) === newSet.id) {
|
||||||
|
return newRelaySetEvent
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FavoriteRelaysContext.Provider
|
||||||
|
value={{
|
||||||
|
favoriteRelays,
|
||||||
|
addFavoriteRelays,
|
||||||
|
deleteFavoriteRelays,
|
||||||
|
relaySets,
|
||||||
|
addRelaySet,
|
||||||
|
deleteRelaySet,
|
||||||
|
updateRelaySet
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</FavoriteRelaysContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
|
import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
|
||||||
import { checkAlgoRelay } from '@/lib/relay'
|
import { checkAlgoRelay } from '@/lib/relay'
|
||||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import storage from '@/services/local-storage.service'
|
import storage from '@/services/local-storage.service'
|
||||||
import relayInfoService from '@/services/relay-info.service'
|
import relayInfoService from '@/services/relay-info.service'
|
||||||
import { TFeedType } from '@/types'
|
import { TFeedInfo, TFeedType } from '@/types'
|
||||||
import { Filter } from 'nostr-tools'
|
import { Filter } from 'nostr-tools'
|
||||||
import { createContext, useContext, useEffect, useRef, useState } from 'react'
|
import { createContext, useContext, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useFavoriteRelays } from './FavoriteRelaysProvider'
|
||||||
import { useNostr } from './NostrProvider'
|
import { useNostr } from './NostrProvider'
|
||||||
import { useRelaySets } from './RelaySetsProvider'
|
|
||||||
|
|
||||||
type TFeedContext = {
|
type TFeedContext = {
|
||||||
feedType: TFeedType
|
feedInfo: TFeedInfo
|
||||||
relayUrls: string[]
|
relayUrls: string[]
|
||||||
temporaryRelayUrls: string[]
|
temporaryRelayUrls: string[]
|
||||||
filter: Filter
|
filter: Filter
|
||||||
isReady: boolean
|
isReady: boolean
|
||||||
activeRelaySetId: string | null
|
|
||||||
switchFeed: (
|
switchFeed: (
|
||||||
feedType: TFeedType,
|
feedType: TFeedType,
|
||||||
options?: { activeRelaySetId?: string; pubkey?: string }
|
options?: { activeRelaySetId?: string; pubkey?: string; relay?: string | null }
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,16 +35,16 @@ export const useFeed = () => {
|
|||||||
export function FeedProvider({ children }: { children: React.ReactNode }) {
|
export function FeedProvider({ children }: { children: React.ReactNode }) {
|
||||||
const isFirstRenderRef = useRef(true)
|
const isFirstRenderRef = useRef(true)
|
||||||
const { pubkey } = useNostr()
|
const { pubkey } = useNostr()
|
||||||
const { relaySets } = useRelaySets()
|
const { relaySets, favoriteRelays } = useFavoriteRelays()
|
||||||
const feedTypeRef = useRef<TFeedType>(storage.getFeedType())
|
|
||||||
const [feedType, setFeedType] = useState<TFeedType>(feedTypeRef.current)
|
|
||||||
const [relayUrls, setRelayUrls] = useState<string[]>([])
|
const [relayUrls, setRelayUrls] = useState<string[]>([])
|
||||||
const [temporaryRelayUrls, setTemporaryRelayUrls] = useState<string[]>([])
|
const [temporaryRelayUrls, setTemporaryRelayUrls] = useState<string[]>([])
|
||||||
const [filter, setFilter] = useState<Filter>({})
|
const [filter, setFilter] = useState<Filter>({})
|
||||||
const [isReady, setIsReady] = useState(false)
|
const [isReady, setIsReady] = useState(false)
|
||||||
const [activeRelaySetId, setActiveRelaySetId] = useState<string | null>(
|
const [feedInfo, setFeedInfo] = useState<TFeedInfo>({
|
||||||
storage.getActiveRelaySetId()
|
feedType: 'relay',
|
||||||
)
|
id: DEFAULT_FAVORITE_RELAYS[0]
|
||||||
|
})
|
||||||
|
const feedInfoRef = useRef<TFeedInfo>(feedInfo)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
@@ -60,13 +60,33 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
|||||||
if (temporaryRelayUrls.length) {
|
if (temporaryRelayUrls.length) {
|
||||||
return await switchFeed('temporary', { temporaryRelayUrls })
|
return await switchFeed('temporary', { temporaryRelayUrls })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (feedTypeRef.current === 'relays') {
|
if (feedInfoRef.current.feedType === 'temporary') {
|
||||||
return await switchFeed('relays', { activeRelaySetId })
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let feedInfo: TFeedInfo = {
|
||||||
|
feedType: 'relay',
|
||||||
|
id: favoriteRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0]
|
||||||
|
}
|
||||||
|
if (pubkey) {
|
||||||
|
const storedFeedInfo = storage.getFeedInfo(pubkey)
|
||||||
|
if (storedFeedInfo) {
|
||||||
|
feedInfo = storedFeedInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feedTypeRef.current === 'following' && pubkey) {
|
if (feedInfo.feedType === 'relays') {
|
||||||
|
return await switchFeed('relays', { activeRelaySetId: feedInfo.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feedInfo.feedType === 'relay') {
|
||||||
|
return await switchFeed('relay', { relay: feedInfo.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
// update following feed if pubkey changes
|
||||||
|
if (feedInfo.feedType === 'following' && pubkey) {
|
||||||
return await switchFeed('following', { pubkey })
|
return await switchFeed('following', { pubkey })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,26 +100,46 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
|||||||
activeRelaySetId?: string | null
|
activeRelaySetId?: string | null
|
||||||
temporaryRelayUrls?: string[] | null
|
temporaryRelayUrls?: string[] | null
|
||||||
pubkey?: string | null
|
pubkey?: string | null
|
||||||
|
relay?: string | null
|
||||||
} = {}
|
} = {}
|
||||||
) => {
|
) => {
|
||||||
setIsReady(false)
|
setIsReady(false)
|
||||||
|
if (feedType === 'relay') {
|
||||||
|
const normalizedUrl = normalizeUrl(options.relay ?? '')
|
||||||
|
if (!normalizedUrl || !isWebsocketUrl(normalizedUrl)) {
|
||||||
|
setIsReady(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFeedInfo = { feedType, id: normalizedUrl }
|
||||||
|
setFeedInfo(newFeedInfo)
|
||||||
|
feedInfoRef.current = newFeedInfo
|
||||||
|
setRelayUrls([normalizedUrl])
|
||||||
|
setFilter({})
|
||||||
|
storage.setFeedInfo(newFeedInfo, pubkey)
|
||||||
|
setIsReady(true)
|
||||||
|
|
||||||
|
const relayInfo = await relayInfoService.getRelayInfo(normalizedUrl)
|
||||||
|
client.setCurrentRelayUrls(checkAlgoRelay(relayInfo) ? [] : [normalizedUrl])
|
||||||
|
return
|
||||||
|
}
|
||||||
if (feedType === 'relays') {
|
if (feedType === 'relays') {
|
||||||
const relaySetId = options.activeRelaySetId ?? (relaySets.length > 0 ? relaySets[0].id : null)
|
const relaySetId = options.activeRelaySetId ?? (relaySets.length > 0 ? relaySets[0].id : null)
|
||||||
if (!relaySetId) {
|
if (!relaySetId) {
|
||||||
return setIsReady(true)
|
setIsReady(true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const relaySet =
|
const relaySet =
|
||||||
relaySets.find((set) => set.id === options.activeRelaySetId) ??
|
relaySets.find((set) => set.id === options.activeRelaySetId) ??
|
||||||
(relaySets.length > 0 ? relaySets[0] : null)
|
(relaySets.length > 0 ? relaySets[0] : null)
|
||||||
if (relaySet) {
|
if (relaySet) {
|
||||||
feedTypeRef.current = feedType
|
const newFeedInfo = { feedType, id: relaySet.id }
|
||||||
setFeedType(feedType)
|
setFeedInfo(newFeedInfo)
|
||||||
|
feedInfoRef.current = newFeedInfo
|
||||||
setRelayUrls(relaySet.relayUrls)
|
setRelayUrls(relaySet.relayUrls)
|
||||||
setActiveRelaySetId(relaySet.id)
|
|
||||||
setFilter({})
|
setFilter({})
|
||||||
storage.setActiveRelaySetId(relaySet.id)
|
storage.setFeedInfo(newFeedInfo, pubkey)
|
||||||
storage.setFeedType(feedType)
|
|
||||||
setIsReady(true)
|
setIsReady(true)
|
||||||
|
|
||||||
const relayInfos = await relayInfoService.getRelayInfos(relaySet.relayUrls)
|
const relayInfos = await relayInfoService.getRelayInfos(relaySet.relayUrls)
|
||||||
@@ -107,21 +147,23 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
|||||||
relaySet.relayUrls.filter((_, i) => !relayInfos[i] || !checkAlgoRelay(relayInfos[i]))
|
relaySet.relayUrls.filter((_, i) => !relayInfos[i] || !checkAlgoRelay(relayInfos[i]))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return setIsReady(true)
|
setIsReady(true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if (feedType === 'following') {
|
if (feedType === 'following') {
|
||||||
if (!options.pubkey) {
|
if (!options.pubkey) {
|
||||||
return setIsReady(true)
|
return setIsReady(true)
|
||||||
}
|
}
|
||||||
feedTypeRef.current = feedType
|
const newFeedInfo = { feedType }
|
||||||
setFeedType(feedType)
|
setFeedInfo(newFeedInfo)
|
||||||
setActiveRelaySetId(null)
|
feedInfoRef.current = newFeedInfo
|
||||||
|
storage.setFeedInfo(newFeedInfo, pubkey)
|
||||||
|
|
||||||
const followings = await client.fetchFollowings(options.pubkey, true)
|
const followings = await client.fetchFollowings(options.pubkey, true)
|
||||||
setRelayUrls([])
|
setRelayUrls([])
|
||||||
setFilter({
|
setFilter({
|
||||||
authors: followings.includes(options.pubkey) ? followings : [...followings, options.pubkey]
|
authors: followings.includes(options.pubkey) ? followings : [...followings, options.pubkey]
|
||||||
})
|
})
|
||||||
storage.setFeedType(feedType)
|
|
||||||
return setIsReady(true)
|
return setIsReady(true)
|
||||||
}
|
}
|
||||||
if (feedType === 'temporary') {
|
if (feedType === 'temporary') {
|
||||||
@@ -130,11 +172,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return setIsReady(true)
|
return setIsReady(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
feedTypeRef.current = feedType
|
const newFeedInfo = { feedType }
|
||||||
setFeedType(feedType)
|
setFeedInfo(newFeedInfo)
|
||||||
|
feedInfoRef.current = newFeedInfo
|
||||||
setTemporaryRelayUrls(urls)
|
setTemporaryRelayUrls(urls)
|
||||||
setRelayUrls(urls)
|
setRelayUrls(urls)
|
||||||
setActiveRelaySetId(null)
|
|
||||||
setFilter({})
|
setFilter({})
|
||||||
setIsReady(true)
|
setIsReady(true)
|
||||||
|
|
||||||
@@ -150,12 +192,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<FeedContext.Provider
|
<FeedContext.Provider
|
||||||
value={{
|
value={{
|
||||||
feedType,
|
feedInfo,
|
||||||
relayUrls,
|
relayUrls,
|
||||||
temporaryRelayUrls,
|
temporaryRelayUrls,
|
||||||
filter,
|
filter,
|
||||||
isReady,
|
isReady,
|
||||||
activeRelaySetId,
|
|
||||||
switchFeed
|
switchFeed
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import LoginDialog from '@/components/LoginDialog'
|
import LoginDialog from '@/components/LoginDialog'
|
||||||
import { BIG_RELAY_URLS, COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants'
|
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||||
import { useToast } from '@/hooks'
|
import { useToast } from '@/hooks'
|
||||||
import {
|
import {
|
||||||
getLatestEvent,
|
getLatestEvent,
|
||||||
@@ -30,6 +30,7 @@ type TNostrContext = {
|
|||||||
relayList: TRelayList | null
|
relayList: TRelayList | null
|
||||||
followListEvent?: Event
|
followListEvent?: Event
|
||||||
muteListEvent?: Event
|
muteListEvent?: Event
|
||||||
|
favoriteRelaysEvent?: Event
|
||||||
account: TAccountPointer | null
|
account: TAccountPointer | null
|
||||||
accounts: TAccountPointer[]
|
accounts: TAccountPointer[]
|
||||||
nsec: string | null
|
nsec: string | null
|
||||||
@@ -55,6 +56,7 @@ type TNostrContext = {
|
|||||||
updateProfileEvent: (profileEvent: Event) => Promise<void>
|
updateProfileEvent: (profileEvent: Event) => Promise<void>
|
||||||
updateFollowListEvent: (followListEvent: Event) => Promise<void>
|
updateFollowListEvent: (followListEvent: Event) => Promise<void>
|
||||||
updateMuteListEvent: (muteListEvent: Event, tags: string[][]) => Promise<void>
|
updateMuteListEvent: (muteListEvent: Event, tags: string[][]) => Promise<void>
|
||||||
|
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const NostrContext = createContext<TNostrContext | undefined>(undefined)
|
const NostrContext = createContext<TNostrContext | undefined>(undefined)
|
||||||
@@ -80,6 +82,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const [relayList, setRelayList] = useState<TRelayList | null>(null)
|
const [relayList, setRelayList] = useState<TRelayList | null>(null)
|
||||||
const [followListEvent, setFollowListEvent] = useState<Event | undefined>(undefined)
|
const [followListEvent, setFollowListEvent] = useState<Event | undefined>(undefined)
|
||||||
const [muteListEvent, setMuteListEvent] = useState<Event | undefined>(undefined)
|
const [muteListEvent, setMuteListEvent] = useState<Event | undefined>(undefined)
|
||||||
|
const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | undefined>(undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
@@ -131,12 +134,18 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
} else {
|
} else {
|
||||||
setNcryptsec(null)
|
setNcryptsec(null)
|
||||||
}
|
}
|
||||||
const [storedRelayListEvent, storedProfileEvent, storedFollowListEvent, storedMuteListEvent] =
|
const [
|
||||||
await Promise.all([
|
storedRelayListEvent,
|
||||||
|
storedProfileEvent,
|
||||||
|
storedFollowListEvent,
|
||||||
|
storedMuteListEvent,
|
||||||
|
storedFavoriteRelaysEvent
|
||||||
|
] = await Promise.all([
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
|
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
|
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Contacts),
|
indexedDb.getReplaceableEvent(account.pubkey, kinds.Contacts),
|
||||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Mutelist)
|
indexedDb.getReplaceableEvent(account.pubkey, kinds.Mutelist),
|
||||||
|
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.FAVORITE_RELAYS)
|
||||||
])
|
])
|
||||||
if (storedRelayListEvent) {
|
if (storedRelayListEvent) {
|
||||||
setRelayList(
|
setRelayList(
|
||||||
@@ -153,6 +162,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
if (storedMuteListEvent) {
|
if (storedMuteListEvent) {
|
||||||
setMuteListEvent(storedMuteListEvent)
|
setMuteListEvent(storedMuteListEvent)
|
||||||
}
|
}
|
||||||
|
if (storedFavoriteRelaysEvent) {
|
||||||
|
setFavoriteRelaysEvent(storedFavoriteRelaysEvent)
|
||||||
|
}
|
||||||
|
|
||||||
const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, {
|
const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, {
|
||||||
kinds: [kinds.RelayList],
|
kinds: [kinds.RelayList],
|
||||||
@@ -167,13 +179,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setRelayList(relayList)
|
setRelayList(relayList)
|
||||||
|
|
||||||
const events = await client.fetchEvents(relayList.write.concat(BIG_RELAY_URLS).slice(0, 4), {
|
const events = await client.fetchEvents(relayList.write.concat(BIG_RELAY_URLS).slice(0, 4), {
|
||||||
kinds: [kinds.Metadata, kinds.Contacts, kinds.Mutelist],
|
kinds: [kinds.Metadata, kinds.Contacts, kinds.Mutelist, ExtendedKind.FAVORITE_RELAYS],
|
||||||
authors: [account.pubkey]
|
authors: [account.pubkey]
|
||||||
})
|
})
|
||||||
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
|
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
|
||||||
const profileEvent = sortedEvents.find((e) => e.kind === kinds.Metadata)
|
const profileEvent = sortedEvents.find((e) => e.kind === kinds.Metadata)
|
||||||
const followListEvent = sortedEvents.find((e) => e.kind === kinds.Contacts)
|
const followListEvent = sortedEvents.find((e) => e.kind === kinds.Contacts)
|
||||||
const muteListEvent = sortedEvents.find((e) => e.kind === kinds.Mutelist)
|
const muteListEvent = sortedEvents.find((e) => e.kind === kinds.Mutelist)
|
||||||
|
const favoriteRelaysEvent = sortedEvents.find((e) => e.kind === ExtendedKind.FAVORITE_RELAYS)
|
||||||
if (profileEvent) {
|
if (profileEvent) {
|
||||||
setProfileEvent(profileEvent)
|
setProfileEvent(profileEvent)
|
||||||
setProfile(getProfileFromProfileEvent(profileEvent))
|
setProfile(getProfileFromProfileEvent(profileEvent))
|
||||||
@@ -192,6 +205,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setMuteListEvent(muteListEvent)
|
setMuteListEvent(muteListEvent)
|
||||||
await indexedDb.putReplaceableEvent(muteListEvent)
|
await indexedDb.putReplaceableEvent(muteListEvent)
|
||||||
}
|
}
|
||||||
|
if (favoriteRelaysEvent) {
|
||||||
|
setFavoriteRelaysEvent(favoriteRelaysEvent)
|
||||||
|
await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
|
||||||
|
}
|
||||||
|
|
||||||
client.initUserIndexFromFollowings(account.pubkey, controller.signal)
|
client.initUserIndexFromFollowings(account.pubkey, controller.signal)
|
||||||
return controller
|
return controller
|
||||||
@@ -414,8 +431,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
kinds.ShortTextNote,
|
kinds.ShortTextNote,
|
||||||
kinds.Reaction,
|
kinds.Reaction,
|
||||||
kinds.Repost,
|
kinds.Repost,
|
||||||
COMMENT_EVENT_KIND,
|
ExtendedKind.COMMENT,
|
||||||
PICTURE_EVENT_KIND
|
ExtendedKind.PICTURE
|
||||||
].includes(draftEvent.kind)
|
].includes(draftEvent.kind)
|
||||||
) {
|
) {
|
||||||
const mentions: string[] = []
|
const mentions: string[] = []
|
||||||
@@ -509,6 +526,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setMuteListEvent(muteListEvent)
|
setMuteListEvent(muteListEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateFavoriteRelaysEvent = async (favoriteRelaysEvent: Event) => {
|
||||||
|
const newFavoriteRelaysEvent = await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
|
||||||
|
if (newFavoriteRelaysEvent.id !== favoriteRelaysEvent.id) return
|
||||||
|
|
||||||
|
setFavoriteRelaysEvent(newFavoriteRelaysEvent)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NostrContext.Provider
|
<NostrContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -518,6 +542,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
relayList,
|
relayList,
|
||||||
followListEvent,
|
followListEvent,
|
||||||
muteListEvent,
|
muteListEvent,
|
||||||
|
favoriteRelaysEvent,
|
||||||
account,
|
account,
|
||||||
accounts: storage
|
accounts: storage
|
||||||
.getAccounts()
|
.getAccounts()
|
||||||
@@ -541,7 +566,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
updateRelayListEvent,
|
updateRelayListEvent,
|
||||||
updateProfileEvent,
|
updateProfileEvent,
|
||||||
updateFollowListEvent,
|
updateFollowListEvent,
|
||||||
updateMuteListEvent
|
updateMuteListEvent,
|
||||||
|
updateFavoriteRelaysEvent
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants'
|
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||||
import { TPrimaryPageName, usePrimaryPage } from '@/PageManager'
|
import { TPrimaryPageName, usePrimaryPage } from '@/PageManager'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import storage from '@/services/local-storage.service'
|
import storage from '@/services/local-storage.service'
|
||||||
@@ -73,7 +73,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
|
|||||||
{
|
{
|
||||||
kinds: [
|
kinds: [
|
||||||
kinds.ShortTextNote,
|
kinds.ShortTextNote,
|
||||||
COMMENT_EVENT_KIND,
|
ExtendedKind.COMMENT,
|
||||||
kinds.Reaction,
|
kinds.Reaction,
|
||||||
kinds.Repost,
|
kinds.Repost,
|
||||||
kinds.Zap
|
kinds.Zap
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import { randomString } from '@/lib/random'
|
|
||||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
|
||||||
import storage from '@/services/local-storage.service'
|
|
||||||
import { TRelaySet } from '@/types'
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
type TRelaySetsContext = {
|
|
||||||
relaySets: TRelaySet[]
|
|
||||||
addRelaySet: (relaySetName: string, relayUrls?: string[]) => string
|
|
||||||
deleteRelaySet: (id: string) => void
|
|
||||||
updateRelaySet: (newSet: TRelaySet) => void
|
|
||||||
mergeRelaySets: (newSets: TRelaySet[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const RelaySetsContext = createContext<TRelaySetsContext | undefined>(undefined)
|
|
||||||
|
|
||||||
export const useRelaySets = () => {
|
|
||||||
const context = useContext(RelaySetsContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useRelaySets must be used within a RelaySetsProvider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RelaySetsProvider({ children }: { children: React.ReactNode }) {
|
|
||||||
const [relaySets, setRelaySets] = useState<TRelaySet[]>(() => storage.getRelaySets())
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
storage.setRelaySets(relaySets)
|
|
||||||
}, [relaySets])
|
|
||||||
|
|
||||||
const deleteRelaySet = (id: string) => {
|
|
||||||
setRelaySets((pre) => pre.filter((set) => set.id !== id))
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRelaySet = (newSet: TRelaySet) => {
|
|
||||||
setRelaySets((pre) => {
|
|
||||||
return pre.map((set) => (set.id === newSet.id ? newSet : set))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const addRelaySet = (relaySetName: string, relayUrls: string[] = []) => {
|
|
||||||
const normalizedUrls = relayUrls
|
|
||||||
.filter((url) => isWebsocketUrl(url))
|
|
||||||
.map((url) => normalizeUrl(url))
|
|
||||||
const id = randomString()
|
|
||||||
setRelaySets((pre) => {
|
|
||||||
return [
|
|
||||||
...pre,
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
name: relaySetName,
|
|
||||||
relayUrls: normalizedUrls
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergeRelaySets = (newSets: TRelaySet[]) => {
|
|
||||||
setRelaySets((pre) => {
|
|
||||||
const newIds = newSets.map((set) => set.id)
|
|
||||||
return pre.filter((set) => !newIds.includes(set.id)).concat(newSets)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RelaySetsContext.Provider
|
|
||||||
value={{
|
|
||||||
relaySets,
|
|
||||||
addRelaySet,
|
|
||||||
deleteRelaySet,
|
|
||||||
updateRelaySet,
|
|
||||||
mergeRelaySets
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</RelaySetsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -90,10 +90,6 @@ class ClientService extends EventTarget {
|
|||||||
await indexedDb.iterateProfileEvents((profileEvent) => this.addUsernameToIndex(profileEvent))
|
await indexedDb.iterateProfileEvents((profileEvent) => this.addUsernameToIndex(profileEvent))
|
||||||
}
|
}
|
||||||
|
|
||||||
listConnectionStatus() {
|
|
||||||
return this.pool.listConnectionStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentRelayUrls(urls: string[]) {
|
setCurrentRelayUrls(urls: string[]) {
|
||||||
this.currentRelayUrls = urls
|
this.currentRelayUrls = urls
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ExtendedKind } from '@/constants'
|
||||||
import { tagNameEquals } from '@/lib/tag'
|
import { tagNameEquals } from '@/lib/tag'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
|
|
||||||
@@ -13,7 +14,9 @@ const StoreNames = {
|
|||||||
FOLLOW_LIST_EVENTS: 'followListEvents',
|
FOLLOW_LIST_EVENTS: 'followListEvents',
|
||||||
MUTE_LIST_EVENTS: 'muteListEvents',
|
MUTE_LIST_EVENTS: 'muteListEvents',
|
||||||
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
|
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
|
||||||
RELAY_INFO_EVENTS: 'relayInfoEvents'
|
RELAY_INFO_EVENTS: 'relayInfoEvents',
|
||||||
|
FAVORITE_RELAYS: 'favoriteRelays',
|
||||||
|
RELAY_SETS: 'relaySets'
|
||||||
}
|
}
|
||||||
|
|
||||||
class IndexedDbService {
|
class IndexedDbService {
|
||||||
@@ -32,7 +35,7 @@ class IndexedDbService {
|
|||||||
init(): Promise<void> {
|
init(): Promise<void> {
|
||||||
if (!this.initPromise) {
|
if (!this.initPromise) {
|
||||||
this.initPromise = new Promise((resolve, reject) => {
|
this.initPromise = new Promise((resolve, reject) => {
|
||||||
const request = window.indexedDB.open('jumble', 2)
|
const request = window.indexedDB.open('jumble', 3)
|
||||||
|
|
||||||
request.onerror = (event) => {
|
request.onerror = (event) => {
|
||||||
reject(event)
|
reject(event)
|
||||||
@@ -63,6 +66,12 @@ class IndexedDbService {
|
|||||||
if (!db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) {
|
if (!db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) {
|
||||||
db.createObjectStore(StoreNames.RELAY_INFO_EVENTS, { keyPath: 'key' })
|
db.createObjectStore(StoreNames.RELAY_INFO_EVENTS, { keyPath: 'key' })
|
||||||
}
|
}
|
||||||
|
if (!db.objectStoreNames.contains(StoreNames.FAVORITE_RELAYS)) {
|
||||||
|
db.createObjectStore(StoreNames.FAVORITE_RELAYS, { keyPath: 'key' })
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains(StoreNames.RELAY_SETS)) {
|
||||||
|
db.createObjectStore(StoreNames.RELAY_SETS, { keyPath: 'key' })
|
||||||
|
}
|
||||||
this.db = db
|
this.db = db
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -84,14 +93,15 @@ class IndexedDbService {
|
|||||||
const transaction = this.db.transaction(storeName, 'readwrite')
|
const transaction = this.db.transaction(storeName, 'readwrite')
|
||||||
const store = transaction.objectStore(storeName)
|
const store = transaction.objectStore(storeName)
|
||||||
|
|
||||||
const getRequest = store.get(event.pubkey)
|
const key = this.getReplaceableEventKey(event)
|
||||||
|
const getRequest = store.get(key)
|
||||||
getRequest.onsuccess = () => {
|
getRequest.onsuccess = () => {
|
||||||
const oldValue = getRequest.result as TValue<Event> | undefined
|
const oldValue = getRequest.result as TValue<Event> | undefined
|
||||||
if (oldValue && oldValue.value.created_at >= event.created_at) {
|
if (oldValue && oldValue.value.created_at >= event.created_at) {
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
return resolve(oldValue.value)
|
return resolve(oldValue.value)
|
||||||
}
|
}
|
||||||
const putRequest = store.put(this.formatValue(event.pubkey, event))
|
const putRequest = store.put(this.formatValue(key, event))
|
||||||
putRequest.onsuccess = () => {
|
putRequest.onsuccess = () => {
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
resolve(event)
|
resolve(event)
|
||||||
@@ -110,7 +120,7 @@ class IndexedDbService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReplaceableEvent(pubkey: string, kind: number): Promise<Event | undefined> {
|
async getReplaceableEvent(pubkey: string, kind: number, d?: string): Promise<Event | undefined> {
|
||||||
const storeName = this.getStoreNameByKind(kind)
|
const storeName = this.getStoreNameByKind(kind)
|
||||||
if (!storeName) {
|
if (!storeName) {
|
||||||
return Promise.reject('store name not found')
|
return Promise.reject('store name not found')
|
||||||
@@ -122,7 +132,8 @@ class IndexedDbService {
|
|||||||
}
|
}
|
||||||
const transaction = this.db.transaction(storeName, 'readonly')
|
const transaction = this.db.transaction(storeName, 'readonly')
|
||||||
const store = transaction.objectStore(storeName)
|
const store = transaction.objectStore(storeName)
|
||||||
const request = store.get(pubkey)
|
const key = d === undefined ? pubkey : `${pubkey}:${d}`
|
||||||
|
const request = store.get(key)
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
@@ -298,6 +309,18 @@ class IndexedDbService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getReplaceableEventKey(event: Event): string {
|
||||||
|
if (
|
||||||
|
[kinds.Metadata, kinds.Contacts].includes(event.kind) ||
|
||||||
|
(event.kind >= 10000 && event.kind < 20000)
|
||||||
|
) {
|
||||||
|
return event.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, d] = event.tags.find(tagNameEquals('d')) ?? []
|
||||||
|
return `${event.pubkey}:${d ?? ''}`
|
||||||
|
}
|
||||||
|
|
||||||
private getStoreNameByKind(kind: number): string | undefined {
|
private getStoreNameByKind(kind: number): string | undefined {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case kinds.Metadata:
|
case kinds.Metadata:
|
||||||
@@ -308,6 +331,10 @@ class IndexedDbService {
|
|||||||
return StoreNames.FOLLOW_LIST_EVENTS
|
return StoreNames.FOLLOW_LIST_EVENTS
|
||||||
case kinds.Mutelist:
|
case kinds.Mutelist:
|
||||||
return StoreNames.MUTE_LIST_EVENTS
|
return StoreNames.MUTE_LIST_EVENTS
|
||||||
|
case kinds.Relaysets:
|
||||||
|
return StoreNames.RELAY_SETS
|
||||||
|
case ExtendedKind.FAVORITE_RELAYS:
|
||||||
|
return StoreNames.FAVORITE_RELAYS
|
||||||
default:
|
default:
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,41 +4,16 @@ import { randomString } from '@/lib/random'
|
|||||||
import {
|
import {
|
||||||
TAccount,
|
TAccount,
|
||||||
TAccountPointer,
|
TAccountPointer,
|
||||||
TFeedType,
|
TFeedInfo,
|
||||||
TNoteListMode,
|
TNoteListMode,
|
||||||
TRelaySet,
|
TRelaySet,
|
||||||
TThemeSetting
|
TThemeSetting
|
||||||
} from '@/types'
|
} from '@/types'
|
||||||
|
|
||||||
const DEFAULT_RELAY_SETS: TRelaySet[] = [
|
|
||||||
{
|
|
||||||
id: randomString(),
|
|
||||||
name: 'Safer Global',
|
|
||||||
relayUrls: ['wss://nostr.wine/', 'wss://pyramid.fiatjaf.com/']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: randomString(),
|
|
||||||
name: 'Short Notes',
|
|
||||||
relayUrls: ['wss://140.f7z.io/']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: randomString(),
|
|
||||||
name: 'News',
|
|
||||||
relayUrls: ['wss://news.utxo.one/']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: randomString(),
|
|
||||||
name: 'Algo',
|
|
||||||
relayUrls: ['wss://algo.utxo.one']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
class LocalStorageService {
|
class LocalStorageService {
|
||||||
static instance: LocalStorageService
|
static instance: LocalStorageService
|
||||||
|
|
||||||
private relaySets: TRelaySet[] = []
|
private relaySets: TRelaySet[] = []
|
||||||
private activeRelaySetId: string | null = null
|
|
||||||
private feedType: TFeedType = 'relays'
|
|
||||||
private themeSetting: TThemeSetting = 'system'
|
private themeSetting: TThemeSetting = 'system'
|
||||||
private accounts: TAccount[] = []
|
private accounts: TAccount[] = []
|
||||||
private currentAccount: TAccount | null = null
|
private currentAccount: TAccount | null = null
|
||||||
@@ -47,6 +22,7 @@ class LocalStorageService {
|
|||||||
private defaultZapSats: number = 21
|
private defaultZapSats: number = 21
|
||||||
private defaultZapComment: string = 'Zap!'
|
private defaultZapComment: string = 'Zap!'
|
||||||
private quickZap: boolean = false
|
private quickZap: boolean = false
|
||||||
|
private accountFeedInfoMap: Record<string, TFeedInfo | undefined> = {}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!LocalStorageService.instance) {
|
if (!LocalStorageService.instance) {
|
||||||
@@ -63,12 +39,6 @@ class LocalStorageService {
|
|||||||
this.accounts = accountsStr ? JSON.parse(accountsStr) : []
|
this.accounts = accountsStr ? JSON.parse(accountsStr) : []
|
||||||
const currentAccountStr = window.localStorage.getItem(StorageKey.CURRENT_ACCOUNT)
|
const currentAccountStr = window.localStorage.getItem(StorageKey.CURRENT_ACCOUNT)
|
||||||
this.currentAccount = currentAccountStr ? JSON.parse(currentAccountStr) : null
|
this.currentAccount = currentAccountStr ? JSON.parse(currentAccountStr) : null
|
||||||
const feedType = window.localStorage.getItem(StorageKey.FEED_TYPE)
|
|
||||||
if (feedType && ['following', 'relays'].includes(feedType)) {
|
|
||||||
this.feedType = feedType as 'following' | 'relays'
|
|
||||||
} else {
|
|
||||||
this.feedType = 'relays'
|
|
||||||
}
|
|
||||||
const noteListModeStr = window.localStorage.getItem(StorageKey.NOTE_LIST_MODE)
|
const noteListModeStr = window.localStorage.getItem(StorageKey.NOTE_LIST_MODE)
|
||||||
this.noteListMode =
|
this.noteListMode =
|
||||||
noteListModeStr && ['posts', 'postsAndReplies', 'pictures'].includes(noteListModeStr)
|
noteListModeStr && ['posts', 'postsAndReplies', 'pictures'].includes(noteListModeStr)
|
||||||
@@ -93,16 +63,12 @@ class LocalStorageService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!relaySets.length) {
|
if (!relaySets.length) {
|
||||||
relaySets = DEFAULT_RELAY_SETS
|
relaySets = []
|
||||||
}
|
}
|
||||||
const activeRelaySetId = relaySets[0].id
|
|
||||||
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(relaySets))
|
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(relaySets))
|
||||||
window.localStorage.setItem(StorageKey.ACTIVE_RELAY_SET_ID, activeRelaySetId)
|
|
||||||
this.relaySets = relaySets
|
this.relaySets = relaySets
|
||||||
this.activeRelaySetId = activeRelaySetId
|
|
||||||
} else {
|
} else {
|
||||||
this.relaySets = JSON.parse(relaySetsStr)
|
this.relaySets = JSON.parse(relaySetsStr)
|
||||||
this.activeRelaySetId = window.localStorage.getItem(StorageKey.ACTIVE_RELAY_SET_ID) ?? null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultZapSatsStr = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_SATS)
|
const defaultZapSatsStr = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_SATS)
|
||||||
@@ -115,12 +81,18 @@ class LocalStorageService {
|
|||||||
this.defaultZapComment = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_COMMENT) ?? 'Zap!'
|
this.defaultZapComment = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_COMMENT) ?? 'Zap!'
|
||||||
this.quickZap = window.localStorage.getItem(StorageKey.QUICK_ZAP) === 'true'
|
this.quickZap = window.localStorage.getItem(StorageKey.QUICK_ZAP) === 'true'
|
||||||
|
|
||||||
|
const accountFeedInfoMapStr =
|
||||||
|
window.localStorage.getItem(StorageKey.ACCOUNT_FEED_INFO_MAP) ?? '{}'
|
||||||
|
this.accountFeedInfoMap = JSON.parse(accountFeedInfoMapStr)
|
||||||
|
|
||||||
// Clean up deprecated data
|
// Clean up deprecated data
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_LIST_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_LIST_EVENT_MAP)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP)
|
||||||
|
window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
|
||||||
|
window.localStorage.removeItem(StorageKey.FEED_TYPE)
|
||||||
}
|
}
|
||||||
|
|
||||||
getRelaySets() {
|
getRelaySets() {
|
||||||
@@ -132,28 +104,6 @@ class LocalStorageService {
|
|||||||
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(this.relaySets))
|
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(this.relaySets))
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveRelaySetId() {
|
|
||||||
return this.activeRelaySetId
|
|
||||||
}
|
|
||||||
|
|
||||||
setActiveRelaySetId(id: string | null) {
|
|
||||||
this.activeRelaySetId = id
|
|
||||||
if (id) {
|
|
||||||
window.localStorage.setItem(StorageKey.ACTIVE_RELAY_SET_ID, id)
|
|
||||||
} else {
|
|
||||||
window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getFeedType() {
|
|
||||||
return this.feedType
|
|
||||||
}
|
|
||||||
|
|
||||||
setFeedType(feedType: TFeedType) {
|
|
||||||
this.feedType = feedType
|
|
||||||
window.localStorage.setItem(StorageKey.FEED_TYPE, this.feedType)
|
|
||||||
}
|
|
||||||
|
|
||||||
getThemeSetting() {
|
getThemeSetting() {
|
||||||
return this.themeSetting
|
return this.themeSetting
|
||||||
}
|
}
|
||||||
@@ -260,6 +210,18 @@ class LocalStorageService {
|
|||||||
JSON.stringify(this.lastReadNotificationTimeMap)
|
JSON.stringify(this.lastReadNotificationTimeMap)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFeedInfo(pubkey: string) {
|
||||||
|
return this.accountFeedInfoMap[pubkey]
|
||||||
|
}
|
||||||
|
|
||||||
|
setFeedInfo(info: TFeedInfo, pubkey?: string | null) {
|
||||||
|
this.accountFeedInfoMap[pubkey ?? 'default'] = info
|
||||||
|
window.localStorage.setItem(
|
||||||
|
StorageKey.ACCOUNT_FEED_INFO_MAP,
|
||||||
|
JSON.stringify(this.accountFeedInfoMap)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = new LocalStorageService()
|
const instance = new LocalStorageService()
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ export type TAccount = {
|
|||||||
|
|
||||||
export type TAccountPointer = Pick<TAccount, 'pubkey' | 'signerType'>
|
export type TAccountPointer = Pick<TAccount, 'pubkey' | 'signerType'>
|
||||||
|
|
||||||
export type TFeedType = 'following' | 'relays' | 'temporary'
|
export type TFeedType = 'following' | 'relays' | 'relay' | 'temporary'
|
||||||
|
export type TFeedInfo = { feedType: TFeedType; id?: string }
|
||||||
|
|
||||||
export type TLanguage = 'en' | 'zh' | 'pl'
|
export type TLanguage = 'en' | 'zh' | 'pl'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user