diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 0889141d82..f2a45acb7a 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -10,7 +10,7 @@ }, "supportWebsite": "SUPPORT_WEBSITE", "fallbackPreferredSeed": { - "__name": "FALLBACK_PREFERRED_SEED", + "__name": "PREFERRED_SEEDS", "__format": "json" } } diff --git a/config/default.json b/config/default.json index de63e09be5..ade744673a 100644 --- a/config/default.json +++ b/config/default.json @@ -9,9 +9,21 @@ "commitsPerPage": 30 }, "supportWebsite": "https://radicle.zulipchat.com", - "fallbackPreferredSeed": { - "hostname": "seed.radicle.garden", - "port": 443, - "scheme": "https" - } + "preferredSeeds": [ + { + "hostname": "ash.radicle.garden", + "port": 443, + "scheme": "https" + }, + { + "hostname": "seed.radicle.xyz", + "port": 443, + "scheme": "https" + }, + { + "hostname": "seed.radicle.garden", + "port": 443, + "scheme": "https" + } + ] } diff --git a/config/test.json b/config/test.json index 82f33342c1..d8439a6e7d 100644 --- a/config/test.json +++ b/config/test.json @@ -3,9 +3,11 @@ "defaultHttpdPort": 8081, "defaultHttpdScheme": "http" }, - "fallbackPreferredSeed": { - "hostname": "127.0.0.1", - "port": 8081, - "scheme": "http" - } + "preferredSeeds": [ + { + "hostname": "127.0.0.1", + "port": 8081, + "scheme": "http" + } + ] } diff --git a/module.d.ts b/module.d.ts index efe297db79..b1f4abcb6b 100644 --- a/module.d.ts +++ b/module.d.ts @@ -12,7 +12,7 @@ declare module "virtual:*" { }; reactions: string[]; supportWebsite: string; - fallbackPreferredSeed: BaseUrl; + preferredSeeds: BaseUrl[]; }; export default config; diff --git a/src/components/Icon.svelte b/src/components/Icon.svelte index 3504ce6a0e..45fd60cf69 100644 --- a/src/components/Icon.svelte +++ b/src/components/Icon.svelte @@ -7,6 +7,8 @@ | "arrow-box-up-right" | "arrow-reply" | "badge" + | "bookmark-off" + | "bookmark-on" | "branch" | "brush" | "chat" @@ -104,6 +106,14 @@ fill-rule="evenodd" clip-rule="evenodd" d="M8.70235 1.76514L9.06964 2.13244C9.25591 2.31871 9.50857 2.42335 9.77201 2.42335H11.0548C11.6034 2.42335 12.0481 2.86806 12.0481 3.41663V4.69945C12.0481 4.9629 12.1528 5.21555 12.3391 5.40182L12.8168 5.87958C13.2047 6.26748 13.2047 6.89641 12.8168 7.28428L12.3391 7.76207C12.1528 7.94834 12.0481 8.20099 12.0481 8.46441V9.43104C12.0481 9.78994 11.8577 10.1044 11.5725 10.2789L12.6676 12.9387C12.7421 13.1197 12.7034 13.3277 12.5686 13.4697C12.4338 13.6117 12.2281 13.6612 12.0435 13.5962L10.7832 13.1526L10.082 14.2898C9.98071 14.454 9.79344 14.5447 9.60178 14.5224C9.41011 14.5001 9.24873 14.3687 9.18792 14.1856L8.33941 11.6302C8.12056 11.7095 7.87942 11.7095 7.66057 11.6301L6.81206 14.1856C6.75125 14.3687 6.58987 14.5001 6.3982 14.5224C6.20654 14.5447 6.01927 14.454 5.91799 14.2898L5.21675 13.1526L3.95651 13.5962C3.77184 13.6612 3.5662 13.6117 3.4314 13.4697C3.2966 13.3277 3.25783 13.1197 3.33237 12.9387L4.44826 10.2286C4.20486 10.0475 4.04717 9.75769 4.04717 9.43104V8.55968C4.04717 8.29627 3.9425 8.04361 3.75623 7.85734L3.1832 7.28428C2.7953 6.89641 2.7953 6.26748 3.1832 5.87958L3.75623 5.30655C3.9425 5.12028 4.04717 4.86762 4.04717 4.60418V3.41663C4.04717 2.86806 4.49185 2.42335 5.04045 2.42335H6.228C6.49142 2.42335 6.74407 2.31871 6.93034 2.13244L7.29764 1.76514C7.68554 1.37724 8.31447 1.37724 8.70235 1.76514ZM8.00001 2.46748L8.3673 2.83478C8.73984 3.20732 9.24515 3.41663 9.77201 3.41663H11.0548V4.69945C11.0548 5.22634 11.2641 5.73162 11.6367 6.10416L12.1145 6.58194L11.6367 7.0597C11.2641 7.43227 11.0548 7.93755 11.0548 8.46441V9.43104H10.0882C9.56134 9.43104 9.05606 9.64032 8.68352 10.0129L8.00001 10.6964L7.31649 10.0129C6.94392 9.64032 6.43864 9.43104 5.91178 9.43104H5.04045V8.55968C5.04045 8.03282 4.83114 7.52754 4.4586 7.15497L3.88554 6.58194L4.4586 6.00889C4.83114 5.63635 5.04045 5.13107 5.04045 4.60418V3.41663H6.228C6.75486 3.41663 7.26014 3.20732 7.63271 2.83478L8.00001 2.46748ZM5.44185 10.4243L4.6733 12.2909L5.26446 12.0827C5.48483 12.0052 5.72948 12.0917 5.8521 12.2905L6.20478 12.8625L6.84207 10.9432L6.61412 10.7152C6.42785 10.529 6.17523 10.4243 5.91178 10.4243H5.44185ZM9.15791 10.9432L9.7952 12.8625L10.1479 12.2905C10.2705 12.0917 10.5152 12.0052 10.7355 12.0827L11.3267 12.2909L10.5581 10.4243H10.0882C9.82479 10.4243 9.57213 10.529 9.38586 10.7152L9.15791 10.9432Z" /> + {:else if name === "bookmark-on"} + + {:else if name === "bookmark-off"} + {:else if name === "branch"} {#if loading} -{:else} - +{:else if disabled}
+ +
+{:else} + +
+ class="button" + on:click + role="button" + style:padding={stylePadding} + tabindex="0" + {title}>
{/if} diff --git a/src/lib/seeds.ts b/src/lib/seeds.ts deleted file mode 100644 index 0d77e67c7a..0000000000 --- a/src/lib/seeds.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { BaseUrl } from "@http-client"; - -import storedWritable from "@efstajas/svelte-stored-writable"; -import unionBy from "lodash/unionBy"; -import { array, number, string, object } from "zod"; -import { derived } from "svelte/store"; - -import config from "virtual:config"; - -const preferredSeedSchema = object({ - hostname: string(), - port: number(), - scheme: string(), -}); - -export const configuredPreferredSeeds = storedWritable( - "configuredPreferredSeeds", - array(preferredSeedSchema), - [], - !localStorage, -); -const storedPreferredSeed = storedWritable( - "preferredSeed", - preferredSeedSchema, - undefined, - !localStorage, -); - -export function addSeedsToConfiguredSeeds(newSeeds: BaseUrl[]) { - configuredPreferredSeeds.update(seeds => - unionBy(seeds, newSeeds, "hostname"), - ); -} - -export function selectPreferredSeed(seed: BaseUrl) { - storedPreferredSeed.set(seed); -} - -export function removeSeedFromConfiguredSeeds(seedHostname: string) { - configuredPreferredSeeds.update( - seeds => seeds.filter(s => s.hostname !== seedHostname) ?? seeds, - ); -} - -export const preferredSeeds = derived( - [configuredPreferredSeeds, storedPreferredSeed], - ([configuredPreferredSeeds, storedPreferredSeed]) => { - // No configured preferred seeds - if (configuredPreferredSeeds.length === 0) - return { - selected: config.fallbackPreferredSeed, - seeds: [config.fallbackPreferredSeed], - }; - - // No stored preferred seed - if (!storedPreferredSeed) - return { - selected: configuredPreferredSeeds[0], - seeds: configuredPreferredSeeds, - }; - - // Stored preferred seed not configured - if ( - !configuredPreferredSeeds.some( - seed => seed.hostname === storedPreferredSeed.hostname, - ) - ) - return { - selected: configuredPreferredSeeds[0], - seeds: configuredPreferredSeeds, - }; - - return { - selected: storedPreferredSeed, - seeds: configuredPreferredSeeds, - }; - }, -); diff --git a/src/views/nodes/PreferredSeedDropdown.svelte b/src/views/nodes/PreferredSeedDropdown.svelte deleted file mode 100644 index 6a262f0ebd..0000000000 --- a/src/views/nodes/PreferredSeedDropdown.svelte +++ /dev/null @@ -1,213 +0,0 @@ - - - - - -
- - - - -
- - -
- { - submittingInput = true; - const customSeedBaseUrl = { - hostname: customSeed, - port: config.nodes.defaultHttpdPort, - scheme: config.nodes.defaultHttpdScheme, - }; - valid = await validateInput(customSeedBaseUrl); - if (valid) { - addSeedsToConfiguredSeeds( - $configuredPreferredSeeds.length === 0 - ? [customSeedBaseUrl, config.fallbackPreferredSeed] - : [customSeedBaseUrl], - ); - selectPreferredSeed(customSeedBaseUrl); - customSeed = ""; - closeFocused(); - void push({ - resource: "nodes", - params: { baseUrl: $selectedSeed, projectPageIndex: 0 }, - }); - } else { - submittingInput = false; - } - }} /> - {#if validationMessage} - {validationMessage} - {/if} -
-
- {#if stateOptions} - - { - selectPreferredSeed(item); - closeFocused(); - void push({ - resource: "nodes", - params: { baseUrl: $selectedSeed, projectPageIndex: 0 }, - }); - }} - slot="item" - selected={item.hostname === $selectedSeed.hostname}> - - - { - selectPreferredSeed(config.fallbackPreferredSeed); - closeFocused(); - void push({ - resource: "nodes", - params: { baseUrl: $selectedSeed, projectPageIndex: 0 }, - }); - }} - slot="empty" - selected> - - - - {/if} -
-
- - diff --git a/src/views/nodes/SeedSelector.svelte b/src/views/nodes/SeedSelector.svelte new file mode 100644 index 0000000000..53b24e7cb1 --- /dev/null +++ b/src/views/nodes/SeedSelector.svelte @@ -0,0 +1,180 @@ + + + + +
+ +
+
+ {baseUrl.hostname} +
+ + + +
+ + +
+ { + loading = true; + const customSeedBaseUrl = { + hostname: seedAddressInput.trim(), + port: config.nodes.defaultHttpdPort, + scheme: config.nodes.defaultHttpdScheme, + }; + validationMessage = await validateInput(customSeedBaseUrl); + if (validationMessage === undefined) { + seedAddressInput = ""; + closeFocused(); + void push({ + resource: "nodes", + params: { baseUrl: customSeedBaseUrl, projectPageIndex: 0 }, + }); + } + loading = false; + }} /> + {#if validationMessage} + {validationMessage} + {/if} +
+ + { + closeFocused(); + void push({ + resource: "nodes", + params: { baseUrl: item, projectPageIndex: 0 }, + }); + }} + slot="item" + selected={false} + let:item> + + + + + + { + closeFocused(); + void push({ + resource: "nodes", + params: { baseUrl: item, projectPageIndex: 0 }, + }); + }} + slot="item" + selected={false} + let:item> + + + +
+
+
+
+ + { + if (some($bookmarkedSeeds, baseUrl)) { + bookmarkedSeeds.update(previous => previous.filter(x => x !== baseUrl)); + } else { + bookmarkedSeeds.update(previous => [...previous, baseUrl]); + } + + closeFocused(); + void push({ + resource: "nodes", + params: { baseUrl: determineSeed(), projectPageIndex: 0 }, + }); + }} + disabled={some(config.preferredSeeds, baseUrl)} + title={some(config.preferredSeeds, baseUrl) + ? "Default seeds can not be removed" + : undefined}> + {#if some(config.preferredSeeds, baseUrl)} + + {:else if some($bookmarkedSeeds, baseUrl)} + + {:else} + + {/if} + +
diff --git a/src/views/nodes/SeedSelector.ts b/src/views/nodes/SeedSelector.ts new file mode 100644 index 0000000000..8c98fc11c8 --- /dev/null +++ b/src/views/nodes/SeedSelector.ts @@ -0,0 +1,35 @@ +import type { BaseUrl } from "@http-client"; + +import storedWritable from "@efstajas/svelte-stored-writable"; +import { array, number, string, object } from "zod"; +import { get } from "svelte/store"; + +import config from "virtual:config"; + +const seedSchema = object({ + hostname: string(), + port: number(), + scheme: string(), +}); + +// A list of seeds that the user has explicitly bookmarked. +export const bookmarkedSeeds = storedWritable( + "bookmarkedSeeds", + array(seedSchema), + [], + !window.localStorage, +); + +// First, try using a seed that was selected by the user previously, +// if that fails fall back to the first configured seed, +// if no seeds are configured, fall back to a hardcoded seed. +export function determineSeed() { + return ( + get(bookmarkedSeeds).slice(-1)[0] ?? + config.preferredSeeds[0] ?? { + schema: "https", + hostname: "seed.radicle.xyz", + port: 443, + } + ); +} diff --git a/src/views/nodes/View.svelte b/src/views/nodes/View.svelte index e257b6e1be..ccccf593d5 100644 --- a/src/views/nodes/View.svelte +++ b/src/views/nodes/View.svelte @@ -21,7 +21,7 @@ import ProjectCard from "@app/components/ProjectCard.svelte"; import PolicyExplainer from "./PolicyExplainer.svelte"; - import PreferredSeedDropdown from "./PreferredSeedDropdown.svelte"; + import SeedSelector from "./SeedSelector.svelte"; import Seeding from "./Seeding.svelte"; import UserAgent from "./UserAgent.svelte"; import NodeAddress from "./NodeAddress.svelte"; @@ -261,13 +261,9 @@ src={node.avatarUrl ? node.avatarUrl : "/images/default-seed-avatar.png"} /> -
+
- -
- {baseUrl.hostname} -
-
+
@@ -384,11 +380,7 @@ : "/images/default-seed-avatar.png"} />
- -
- {baseUrl.hostname} -
-
+
diff --git a/src/views/nodes/router.ts b/src/views/nodes/router.ts index 73dedc820a..d15d0e0b0e 100644 --- a/src/views/nodes/router.ts +++ b/src/views/nodes/router.ts @@ -7,8 +7,7 @@ import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher"; import { baseUrlToString, isLocal } from "@app/lib/utils"; import { handleError } from "@app/views/nodes/error"; import { unreachableError } from "@app/views/projects/error"; -import { preferredSeeds } from "@app/lib/seeds"; -import { get } from "svelte/store"; +import { determineSeed } from "./SeedSelector"; export type NodesRouteParams = | { @@ -49,7 +48,7 @@ export async function loadNodeRoute( if (params) { baseUrl = params.baseUrl; } else { - baseUrl = get(preferredSeeds).selected; + baseUrl = determineSeed(); } const api = new HttpdClient(baseUrl);