diff --git a/src/renderer/src/components/RelaySettings/RelayGroup.tsx b/src/renderer/src/components/RelaySettings/RelayGroup.tsx index 40f3143a..0a27fa54 100644 --- a/src/renderer/src/components/RelaySettings/RelayGroup.tsx +++ b/src/renderer/src/components/RelaySettings/RelayGroup.tsx @@ -15,7 +15,9 @@ import { TRelayGroup } from './types' export default function RelayGroup({ group }: { group: TRelayGroup }) { const { expandedRelayGroup } = useRelaySettingsComponent() - const { groupName, isActive, relayUrls } = group + const { temporaryRelayUrls } = useRelaySettings() + const { groupName, relayUrls } = group + const isActive = temporaryRelayUrls.length === 0 && group.isActive return (
- + 0} + />
{relayUrls.length} relays - +
{expandedRelayGroup === groupName && } @@ -38,20 +44,25 @@ export default function RelayGroup({ group }: { group: TRelayGroup }) { ) } -function RelayGroupActiveToggle({ groupName }: { groupName: string }) { - const { relayGroups, switchRelayGroup } = useRelaySettings() - - const isActive = relayGroups.find((group) => group.groupName === groupName)?.isActive - const hasRelayUrls = relayGroups.find((group) => group.groupName === groupName)?.relayUrls.length +function RelayGroupActiveToggle({ + groupName, + isActive, + canActive +}: { + groupName: string + isActive: boolean + canActive: boolean +}) { + const { switchRelayGroup } = useRelaySettings() return isActive ? ( ) : ( { - if (hasRelayUrls) { + if (canActive) { switchRelayGroup(groupName) } }} @@ -68,6 +79,9 @@ function RelayGroupName({ groupName }: { groupName: string }) { const hasRelayUrls = relayGroups.find((group) => group.groupName === groupName)?.relayUrls.length const saveNewGroupName = () => { + if (relayGroups.find((group) => group.groupName === newGroupName)) { + return setNewNameError('already exists') + } const errMsg = renameRelayGroup(groupName, newGroupName) if (errMsg) { setNewNameError(errMsg) @@ -138,10 +152,9 @@ function RelayUrlsExpandToggle({ ) } -function RelayGroupOptions({ groupName }: { groupName: string }) { - const { relayGroups, deleteRelayGroup } = useRelaySettings() +function RelayGroupOptions({ group }: { group: TRelayGroup }) { + const { deleteRelayGroup } = useRelaySettings() const { setRenamingGroup } = useRelaySettingsComponent() - const isActive = relayGroups.find((group) => group.groupName === groupName)?.isActive return ( @@ -151,11 +164,21 @@ function RelayGroupOptions({ groupName }: { groupName: string }) { - setRenamingGroup(groupName)}>Rename + setRenamingGroup(group.groupName)}> + Rename + + { + navigator.clipboard.writeText( + `https://jumble.social/?${group.relayUrls.map((url) => 'r=' + url).join('&')}` + ) + }} + > + Copy share link + deleteRelayGroup(groupName)} + onClick={() => deleteRelayGroup(group.groupName)} > Delete diff --git a/src/renderer/src/components/RelaySettings/TemporaryRelayGroup.tsx b/src/renderer/src/components/RelaySettings/TemporaryRelayGroup.tsx new file mode 100644 index 00000000..ba2530fb --- /dev/null +++ b/src/renderer/src/components/RelaySettings/TemporaryRelayGroup.tsx @@ -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 ( +
+
+
Temporary
+ +
+ {relays.map((relay, index) => ( +
+
+ {relay.isConnected ? ( +
+ ) : ( +
+ )} +
{relay.url}
+
+
+ ))} +
+ ) +} diff --git a/src/renderer/src/components/RelaySettings/index.tsx b/src/renderer/src/components/RelaySettings/index.tsx index f69600d7..6b583f5d 100644 --- a/src/renderer/src/components/RelaySettings/index.tsx +++ b/src/renderer/src/components/RelaySettings/index.tsx @@ -5,6 +5,7 @@ import { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' import { useEffect, useRef, useState } from 'react' import { RelaySettingsComponentProvider } from './provider' import RelayGroup from './RelayGroup' +import TemporaryRelayGroup from './TemporaryRelayGroup' export default function RelaySettings() { const { relayGroups, addRelayGroup } = useRelaySettings() @@ -19,6 +20,9 @@ export default function RelaySettings() { }, []) const saveRelayGroup = () => { + if (relayGroups.find((group) => group.groupName === newGroupName)) { + return setNewNameError('already exists') + } const errMsg = addRelayGroup(newGroupName) if (errMsg) { return setNewNameError(errMsg) @@ -43,6 +47,7 @@ export default function RelaySettings() {
Relay Settings
+ {relayGroups.map((group, index) => ( ))} @@ -63,7 +68,7 @@ export default function RelaySettings() { onKeyDown={handleNewGroupNameKeyDown} onBlur={saveRelayGroup} /> - +
{newNameError &&
{newNameError}
}
diff --git a/src/renderer/src/layouts/PrimaryPageLayout/index.tsx b/src/renderer/src/layouts/PrimaryPageLayout/index.tsx index 96d62ebc..eae817ef 100644 --- a/src/renderer/src/layouts/PrimaryPageLayout/index.tsx +++ b/src/renderer/src/layouts/PrimaryPageLayout/index.tsx @@ -10,7 +10,10 @@ import { forwardRef, useImperativeHandle, useRef } from 'react' const PrimaryPageLayout = forwardRef( ( - { children, titlebarContent }: { children: React.ReactNode; titlebarContent?: React.ReactNode }, + { + children, + titlebarContent + }: { children?: React.ReactNode; titlebarContent?: React.ReactNode }, ref ) => { const scrollAreaRef = useRef(null) diff --git a/src/renderer/src/pages/primary/NoteListPage/index.tsx b/src/renderer/src/pages/primary/NoteListPage/index.tsx index 33bbdb61..40ef6afa 100644 --- a/src/renderer/src/pages/primary/NoteListPage/index.tsx +++ b/src/renderer/src/pages/primary/NoteListPage/index.tsx @@ -1,10 +1,36 @@ 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 { useRelaySettings } from '@renderer/providers/RelaySettingsProvider' export default function NoteListPage() { const { relayUrls } = useRelaySettings() - if (!relayUrls.length) return null + + if (!relayUrls.length) { + return ( + +
+ + + + + + +
+ +
+
+
+
+
+
+ ) + } return ( diff --git a/src/renderer/src/providers/RelaySettingsProvider.tsx b/src/renderer/src/providers/RelaySettingsProvider.tsx index 8c44dec6..25073a21 100644 --- a/src/renderer/src/providers/RelaySettingsProvider.tsx +++ b/src/renderer/src/providers/RelaySettingsProvider.tsx @@ -1,14 +1,16 @@ import { TRelayGroup } from '@common/types' +import { isWebsocketUrl, normalizeUrl } from '@renderer/lib/url' import storage from '@renderer/services/storage.service' import { createContext, useContext, useEffect, useState } from 'react' type TRelaySettingsContext = { relayGroups: TRelayGroup[] + temporaryRelayUrls: string[] relayUrls: string[] switchRelayGroup: (groupName: string) => void renameRelayGroup: (oldGroupName: string, newGroupName: string) => string | null deleteRelayGroup: (groupName: string) => void - addRelayGroup: (groupName: string) => string | null + addRelayGroup: (groupName: string, relayUrls?: string[]) => string | null updateRelayGroupRelayUrls: (groupName: string, relayUrls: string[]) => void } @@ -24,12 +26,31 @@ export const useRelaySettings = () => { export function RelaySettingsProvider({ children }: { children: React.ReactNode }) { const [relayGroups, setRelayGroups] = useState([]) + const [temporaryRelayUrls, setTemporaryRelayUrls] = useState([]) const [relayUrls, setRelayUrls] = useState( - relayGroups.find((group) => group.isActive)?.relayUrls ?? [] + temporaryRelayUrls.length + ? temporaryRelayUrls + : (relayGroups.find((group) => group.isActive)?.relayUrls ?? []) ) useEffect(() => { 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() setRelayGroups(storedGroups) } @@ -38,30 +59,39 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode }, []) useEffect(() => { - setRelayUrls(relayGroups.find((group) => group.isActive)?.relayUrls ?? []) - }, [relayGroups]) + setRelayUrls( + temporaryRelayUrls.length + ? temporaryRelayUrls + : (relayGroups.find((group) => group.isActive)?.relayUrls ?? []) + ) + }, [relayGroups, temporaryRelayUrls]) - const updateGroups = async (newGroups: TRelayGroup[]) => { - setRelayGroups(newGroups) + const updateGroups = async (fn: (pre: TRelayGroup[]) => TRelayGroup[]) => { + let newGroups = relayGroups + setRelayGroups((pre) => { + newGroups = fn(pre) + return newGroups + }) await storage.setRelayGroups(newGroups) } const switchRelayGroup = (groupName: string) => { - updateGroups( - relayGroups.map((group) => ({ + updateGroups((pre) => + pre.map((group) => ({ ...group, isActive: group.groupName === groupName })) ) + setTemporaryRelayUrls([]) } 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[]) => { - updateGroups( - relayGroups.map((group) => ({ + updateGroups((pre) => + pre.map((group) => ({ ...group, relayUrls: group.groupName === groupName ? relayUrls : group.relayUrls })) @@ -75,33 +105,38 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode if (oldGroupName === newGroupName) { return null } - if (relayGroups.some((group) => group.groupName === newGroupName)) { - return 'already exists' - } - updateGroups( - relayGroups.map((group) => ({ + updateGroups((pre) => { + if (pre.some((group) => group.groupName === newGroupName)) { + return pre + } + return pre.map((group) => ({ ...group, groupName: group.groupName === oldGroupName ? newGroupName : group.groupName })) - ) + }) return null } - const addRelayGroup = (groupName: string) => { + const addRelayGroup = (groupName: string, relayUrls: string[] = []) => { if (groupName === '') { return null } - if (relayGroups.some((group) => group.groupName === groupName)) { - return 'already exists' - } - updateGroups([ - ...relayGroups, - { - groupName, - relayUrls: [], - isActive: false + const normalizedUrls = relayUrls + .filter((url) => isWebsocketUrl(url)) + .map((url) => normalizeUrl(url)) + updateGroups((pre) => { + if (pre.some((group) => group.groupName === groupName)) { + return pre } - ]) + return [ + ...pre, + { + groupName, + relayUrls: normalizedUrls, + isActive: false + } + ] + }) return null } @@ -109,6 +144,7 @@ export function RelaySettingsProvider({ children }: { children: React.ReactNode