feat: share relays

This commit is contained in:
codytseng
2024-11-16 19:59:46 +08:00
parent e7f021d03b
commit 8d4b5985db
6 changed files with 212 additions and 47 deletions

View File

@@ -15,7 +15,9 @@ import { TRelayGroup } from './types'
export default function RelayGroup({ group }: { group: TRelayGroup }) { export default function RelayGroup({ group }: { group: TRelayGroup }) {
const { expandedRelayGroup } = useRelaySettingsComponent() const { expandedRelayGroup } = useRelaySettingsComponent()
const { groupName, isActive, relayUrls } = group const { temporaryRelayUrls } = useRelaySettings()
const { groupName, relayUrls } = group
const isActive = temporaryRelayUrls.length === 0 && group.isActive
return ( return (
<div <div
@@ -23,14 +25,18 @@ export default function RelayGroup({ group }: { group: TRelayGroup }) {
> >
<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 space-x-2 items-center">
<RelayGroupActiveToggle groupName={groupName} /> <RelayGroupActiveToggle
groupName={groupName}
isActive={isActive}
canActive={relayUrls.length > 0}
/>
<RelayGroupName groupName={groupName} /> <RelayGroupName groupName={groupName} />
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
<RelayUrlsExpandToggle groupName={groupName}> <RelayUrlsExpandToggle groupName={groupName}>
{relayUrls.length} relays {relayUrls.length} relays
</RelayUrlsExpandToggle> </RelayUrlsExpandToggle>
<RelayGroupOptions groupName={groupName} /> <RelayGroupOptions group={group} />
</div> </div>
</div> </div>
{expandedRelayGroup === groupName && <RelayUrls groupName={groupName} />} {expandedRelayGroup === groupName && <RelayUrls groupName={groupName} />}
@@ -38,20 +44,25 @@ export default function RelayGroup({ group }: { group: TRelayGroup }) {
) )
} }
function RelayGroupActiveToggle({ groupName }: { groupName: string }) { function RelayGroupActiveToggle({
const { relayGroups, switchRelayGroup } = useRelaySettings() groupName,
isActive,
const isActive = relayGroups.find((group) => group.groupName === groupName)?.isActive canActive
const hasRelayUrls = relayGroups.find((group) => group.groupName === groupName)?.relayUrls.length }: {
groupName: string
isActive: boolean
canActive: boolean
}) {
const { switchRelayGroup } = useRelaySettings()
return isActive ? ( return isActive ? (
<CircleCheck size={18} className="text-highlight shrink-0" /> <CircleCheck size={18} className="text-highlight shrink-0" />
) : ( ) : (
<Circle <Circle
size={18} size={18}
className={`text-muted-foreground shrink-0 ${hasRelayUrls ? 'cursor-pointer hover:text-foreground ' : ''}`} className={`text-muted-foreground shrink-0 ${canActive ? 'cursor-pointer hover:text-foreground ' : ''}`}
onClick={() => { onClick={() => {
if (hasRelayUrls) { if (canActive) {
switchRelayGroup(groupName) switchRelayGroup(groupName)
} }
}} }}
@@ -68,6 +79,9 @@ function RelayGroupName({ groupName }: { groupName: string }) {
const hasRelayUrls = relayGroups.find((group) => group.groupName === groupName)?.relayUrls.length const hasRelayUrls = relayGroups.find((group) => group.groupName === groupName)?.relayUrls.length
const saveNewGroupName = () => { const saveNewGroupName = () => {
if (relayGroups.find((group) => group.groupName === newGroupName)) {
return setNewNameError('already exists')
}
const errMsg = renameRelayGroup(groupName, newGroupName) const errMsg = renameRelayGroup(groupName, newGroupName)
if (errMsg) { if (errMsg) {
setNewNameError(errMsg) setNewNameError(errMsg)
@@ -138,10 +152,9 @@ function RelayUrlsExpandToggle({
) )
} }
function RelayGroupOptions({ groupName }: { groupName: string }) { function RelayGroupOptions({ group }: { group: TRelayGroup }) {
const { relayGroups, deleteRelayGroup } = useRelaySettings() const { deleteRelayGroup } = useRelaySettings()
const { setRenamingGroup } = useRelaySettingsComponent() const { setRenamingGroup } = useRelaySettingsComponent()
const isActive = relayGroups.find((group) => group.groupName === groupName)?.isActive
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -151,11 +164,21 @@ function RelayGroupOptions({ groupName }: { groupName: string }) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem onClick={() => setRenamingGroup(groupName)}>Rename</DropdownMenuItem> <DropdownMenuItem onClick={() => setRenamingGroup(group.groupName)}>
Rename
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(
`https://jumble.social/?${group.relayUrls.map((url) => 'r=' + url).join('&')}`
)
}}
>
Copy share link
</DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className="text-destructive focus:text-destructive" className="text-destructive focus:text-destructive"
disabled={isActive} onClick={() => deleteRelayGroup(group.groupName)}
onClick={() => deleteRelayGroup(groupName)}
> >
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -0,0 +1,72 @@
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
import client from '@renderer/services/client.service'
import { Save } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Button } from '../ui/button'
export default function TemporaryRelayGroup() {
const { temporaryRelayUrls, relayGroups, addRelayGroup, switchRelayGroup } = useRelaySettings()
const [relays, setRelays] = useState<
{
url: string
isConnected: boolean
}[]
>(temporaryRelayUrls.map((url) => ({ url, isConnected: false })))
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
}
const handleSave = () => {
const existingTemporaryIndexes = relayGroups
.filter((group) => /^Temporary \d+$/.test(group.groupName))
.map((group) => group.groupName.split(' ')[1])
.map(Number)
.filter((index) => !isNaN(index))
const nextIndex = Math.max(...existingTemporaryIndexes, 0) + 1
const groupName = `Temporary ${nextIndex}`
addRelayGroup(groupName, temporaryRelayUrls)
switchRelayGroup(groupName)
}
return (
<div className={`w-full border border-dashed rounded-lg p-4 border-highlight bg-highlight/5`}>
<div className="flex justify-between items-center">
<div className="h-8 font-semibold">Temporary</div>
<Button title="save" size="icon" variant="ghost" onClick={handleSave}>
<Save />
</Button>
</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>
</div>
</div>
))}
</div>
)
}

View File

@@ -5,6 +5,7 @@ import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { RelaySettingsComponentProvider } from './provider' import { RelaySettingsComponentProvider } from './provider'
import RelayGroup from './RelayGroup' import RelayGroup from './RelayGroup'
import TemporaryRelayGroup from './TemporaryRelayGroup'
export default function RelaySettings() { export default function RelaySettings() {
const { relayGroups, addRelayGroup } = useRelaySettings() const { relayGroups, addRelayGroup } = useRelaySettings()
@@ -19,6 +20,9 @@ export default function RelaySettings() {
}, []) }, [])
const saveRelayGroup = () => { const saveRelayGroup = () => {
if (relayGroups.find((group) => group.groupName === newGroupName)) {
return setNewNameError('already exists')
}
const errMsg = addRelayGroup(newGroupName) const errMsg = addRelayGroup(newGroupName)
if (errMsg) { if (errMsg) {
return setNewNameError(errMsg) return setNewNameError(errMsg)
@@ -43,6 +47,7 @@ export default function RelaySettings() {
<div ref={dummyRef} tabIndex={-1} style={{ position: 'absolute', opacity: 0 }}></div> <div ref={dummyRef} tabIndex={-1} style={{ position: 'absolute', opacity: 0 }}></div>
<div className="text-lg font-semibold mb-4">Relay Settings</div> <div className="text-lg font-semibold mb-4">Relay Settings</div>
<div className="space-y-2"> <div className="space-y-2">
<TemporaryRelayGroup />
{relayGroups.map((group, index) => ( {relayGroups.map((group, index) => (
<RelayGroup key={index} group={group} /> <RelayGroup key={index} group={group} />
))} ))}
@@ -63,7 +68,7 @@ export default function RelaySettings() {
onKeyDown={handleNewGroupNameKeyDown} onKeyDown={handleNewGroupNameKeyDown}
onBlur={saveRelayGroup} onBlur={saveRelayGroup}
/> />
<Button>Add</Button> <Button onClick={saveRelayGroup}>Add</Button>
</div> </div>
{newNameError && <div className="text-xs text-destructive mt-1">{newNameError}</div>} {newNameError && <div className="text-xs text-destructive mt-1">{newNameError}</div>}
</div> </div>

View File

@@ -10,7 +10,10 @@ import { forwardRef, useImperativeHandle, useRef } from 'react'
const PrimaryPageLayout = forwardRef( const PrimaryPageLayout = forwardRef(
( (
{ children, titlebarContent }: { children: React.ReactNode; titlebarContent?: React.ReactNode }, {
children,
titlebarContent
}: { children?: React.ReactNode; titlebarContent?: React.ReactNode },
ref ref
) => { ) => {
const scrollAreaRef = useRef<HTMLDivElement>(null) const scrollAreaRef = useRef<HTMLDivElement>(null)

View File

@@ -1,10 +1,36 @@
import NoteList from '@renderer/components/NoteList' import NoteList from '@renderer/components/NoteList'
import RelaySettings from '@renderer/components/RelaySettings'
import { Button } from '@renderer/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'
import { ScrollArea } from '@renderer/components/ui/scroll-area'
import PrimaryPageLayout from '@renderer/layouts/PrimaryPageLayout' import PrimaryPageLayout from '@renderer/layouts/PrimaryPageLayout'
import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider'
export default function NoteListPage() { export default function NoteListPage() {
const { relayUrls } = useRelaySettings() const { relayUrls } = useRelaySettings()
if (!relayUrls.length) return null
if (!relayUrls.length) {
return (
<PrimaryPageLayout>
<div className="w-full text-center">
<Popover>
<PopoverTrigger asChild>
<Button title="relay settings" size="lg">
Choose a relay group
</Button>
</PopoverTrigger>
<PopoverContent className="w-96 h-[450px] p-0">
<ScrollArea className="h-full">
<div className="p-4">
<RelaySettings />
</div>
</ScrollArea>
</PopoverContent>
</Popover>
</div>
</PrimaryPageLayout>
)
}
return ( return (
<PrimaryPageLayout> <PrimaryPageLayout>

View File

@@ -1,14 +1,16 @@
import { TRelayGroup } from '@common/types' import { TRelayGroup } from '@common/types'
import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url'
import storage from '@renderer/services/storage.service' import storage from '@renderer/services/storage.service'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
type TRelaySettingsContext = { type TRelaySettingsContext = {
relayGroups: TRelayGroup[] relayGroups: TRelayGroup[]
temporaryRelayUrls: string[]
relayUrls: string[] relayUrls: string[]
switchRelayGroup: (groupName: string) => void switchRelayGroup: (groupName: string) => void
renameRelayGroup: (oldGroupName: string, newGroupName: string) => string | null renameRelayGroup: (oldGroupName: string, newGroupName: string) => string | null
deleteRelayGroup: (groupName: string) => void deleteRelayGroup: (groupName: string) => void
addRelayGroup: (groupName: string) => string | null addRelayGroup: (groupName: string, relayUrls?: string[]) => string | null
updateRelayGroupRelayUrls: (groupName: string, relayUrls: string[]) => void updateRelayGroupRelayUrls: (groupName: string, relayUrls: string[]) => void
} }
@@ -24,12 +26,31 @@ export const useRelaySettings = () => {
export function RelaySettingsProvider({ children }: { children: React.ReactNode }) { export function RelaySettingsProvider({ children }: { children: React.ReactNode }) {
const [relayGroups, setRelayGroups] = useState<TRelayGroup[]>([]) const [relayGroups, setRelayGroups] = useState<TRelayGroup[]>([])
const [temporaryRelayUrls, setTemporaryRelayUrls] = useState<string[]>([])
const [relayUrls, setRelayUrls] = useState<string[]>( const [relayUrls, setRelayUrls] = useState<string[]>(
relayGroups.find((group) => group.isActive)?.relayUrls ?? [] temporaryRelayUrls.length
? temporaryRelayUrls
: (relayGroups.find((group) => group.isActive)?.relayUrls ?? [])
) )
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
const searchParams = new URLSearchParams(window.location.search)
const tempRelays = searchParams
.getAll('r')
.filter((url) => isWebsocketUrl(url))
.map((url) => normalizeUrl(url))
if (tempRelays.length) {
setTemporaryRelayUrls(tempRelays)
// remove relay urls from query string
searchParams.delete('r')
const newSearch = searchParams.toString()
window.history.replaceState(
{},
'',
`${window.location.pathname}${newSearch.length ? `?${newSearch}` : ''}`
)
}
const storedGroups = await storage.getRelayGroups() const storedGroups = await storage.getRelayGroups()
setRelayGroups(storedGroups) setRelayGroups(storedGroups)
} }
@@ -38,30 +59,39 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode
}, []) }, [])
useEffect(() => { useEffect(() => {
setRelayUrls(relayGroups.find((group) => group.isActive)?.relayUrls ?? []) setRelayUrls(
}, [relayGroups]) temporaryRelayUrls.length
? temporaryRelayUrls
: (relayGroups.find((group) => group.isActive)?.relayUrls ?? [])
)
}, [relayGroups, temporaryRelayUrls])
const updateGroups = async (newGroups: TRelayGroup[]) => { const updateGroups = async (fn: (pre: TRelayGroup[]) => TRelayGroup[]) => {
setRelayGroups(newGroups) let newGroups = relayGroups
setRelayGroups((pre) => {
newGroups = fn(pre)
return newGroups
})
await storage.setRelayGroups(newGroups) await storage.setRelayGroups(newGroups)
} }
const switchRelayGroup = (groupName: string) => { const switchRelayGroup = (groupName: string) => {
updateGroups( updateGroups((pre) =>
relayGroups.map((group) => ({ pre.map((group) => ({
...group, ...group,
isActive: group.groupName === groupName isActive: group.groupName === groupName
})) }))
) )
setTemporaryRelayUrls([])
} }
const deleteRelayGroup = (groupName: string) => { const deleteRelayGroup = (groupName: string) => {
updateGroups(relayGroups.filter((group) => group.groupName !== groupName || group.isActive)) updateGroups((pre) => pre.filter((group) => group.groupName !== groupName))
} }
const updateRelayGroupRelayUrls = (groupName: string, relayUrls: string[]) => { const updateRelayGroupRelayUrls = (groupName: string, relayUrls: string[]) => {
updateGroups( updateGroups((pre) =>
relayGroups.map((group) => ({ pre.map((group) => ({
...group, ...group,
relayUrls: group.groupName === groupName ? relayUrls : group.relayUrls relayUrls: group.groupName === groupName ? relayUrls : group.relayUrls
})) }))
@@ -75,33 +105,38 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode
if (oldGroupName === newGroupName) { if (oldGroupName === newGroupName) {
return null return null
} }
if (relayGroups.some((group) => group.groupName === newGroupName)) { updateGroups((pre) => {
return 'already exists' if (pre.some((group) => group.groupName === newGroupName)) {
return pre
} }
updateGroups( return pre.map((group) => ({
relayGroups.map((group) => ({
...group, ...group,
groupName: group.groupName === oldGroupName ? newGroupName : group.groupName groupName: group.groupName === oldGroupName ? newGroupName : group.groupName
})) }))
) })
return null return null
} }
const addRelayGroup = (groupName: string) => { const addRelayGroup = (groupName: string, relayUrls: string[] = []) => {
if (groupName === '') { if (groupName === '') {
return null return null
} }
if (relayGroups.some((group) => group.groupName === groupName)) { const normalizedUrls = relayUrls
return 'already exists' .filter((url) => isWebsocketUrl(url))
.map((url) => normalizeUrl(url))
updateGroups((pre) => {
if (pre.some((group) => group.groupName === groupName)) {
return pre
} }
updateGroups([ return [
...relayGroups, ...pre,
{ {
groupName, groupName,
relayUrls: [], relayUrls: normalizedUrls,
isActive: false isActive: false
} }
]) ]
})
return null return null
} }
@@ -109,6 +144,7 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode
<RelaySettingsContext.Provider <RelaySettingsContext.Provider
value={{ value={{
relayGroups, relayGroups,
temporaryRelayUrls,
relayUrls, relayUrls,
switchRelayGroup, switchRelayGroup,
renameRelayGroup, renameRelayGroup,