mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-31 01:00:47 +08:00
Use configured explorer and seed for links
explorerUrl() hardcoded both the seed ("iris.radicle.network") and
the explorer base ("https://radicle.network"), so every "Copy Link"
and "Open in …" action ignored the user's config and could point at
a host that does not replicate the repo.
Expose publicExplorer and preferredSeeds on the Config type (both
already carried by radicle::profile::Config) and thread config
through to every explorerUrl() call site. URLs are now built from
the publicExplorer template, filling $host from the first preferred
seed (falling back to rosa only when none are configured), and link
labels show the configured explorer's host.
Co-authored-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
co-authored by
Rūdolfs Ošiņš
parent
7fe26dc4e5
commit
74aaa6ef14
@@ -18,4 +18,13 @@ export type Config = {
|
||||
seedingPolicy: { default: "allow"; scope: "followed" | "all" } | {
|
||||
default: "block";
|
||||
};
|
||||
/**
|
||||
* Public explorer URL template, e.g. `https://radicle.network/nodes/$host/$rid$path`.
|
||||
*/
|
||||
publicExplorer: string;
|
||||
/**
|
||||
* Preferred seed addresses, in priority order. Used to pick a host for
|
||||
* explorer links.
|
||||
*/
|
||||
preferredSeeds: Array<string>;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use radicle::crypto::PublicKey;
|
||||
use radicle::explorer::Explorer;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
use radicle::node::Alias;
|
||||
use radicle::node::config::DefaultSeedingPolicy;
|
||||
use radicle::node::config::{ConnectAddress, DefaultSeedingPolicy};
|
||||
|
||||
/// Service configuration.
|
||||
#[derive(Debug, TS, Serialize, PartialEq)]
|
||||
@@ -21,6 +22,13 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
#[ts(type = "{ default: 'allow', scope: 'followed' | 'all' } | { default: 'block' }")]
|
||||
pub seeding_policy: DefaultSeedingPolicy,
|
||||
/// Public explorer URL template, e.g. `https://radicle.network/nodes/$host/$rid$path`.
|
||||
#[ts(as = "String")]
|
||||
pub public_explorer: Explorer,
|
||||
/// Preferred seed addresses, in priority order. Used to pick a host for
|
||||
/// explorer links.
|
||||
#[ts(as = "Vec<String>")]
|
||||
pub preferred_seeds: Vec<ConnectAddress>,
|
||||
}
|
||||
|
||||
pub struct Version {
|
||||
|
||||
@@ -19,6 +19,8 @@ pub trait Profile {
|
||||
public_key: p.public_key,
|
||||
alias: p.config.node.alias.clone(),
|
||||
seeding_policy: p.config.node.seeding_policy,
|
||||
public_explorer: p.config.public_explorer.clone(),
|
||||
preferred_seeds: p.config.preferred_seeds.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +57,8 @@ mod test {
|
||||
public_key: *signer.public_key(),
|
||||
alias: Alias::from_str("seed").unwrap(),
|
||||
seeding_policy: config::DefaultSeedingPolicy::Block,
|
||||
public_explorer: state.profile.config.public_explorer.clone(),
|
||||
preferred_seeds: state.profile.config.preferred_seeds.clone(),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
<SidebarRepoList
|
||||
initialRepos={sidebarData.repos}
|
||||
initialSeededNotReplicated={sidebarData.seededNotReplicated}
|
||||
config={sidebarData.config}
|
||||
{activeRepo} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
import { writeToClipboard } from "@app/lib/invoke";
|
||||
import { didFromPublicKey, explorerUrl, truncateDid } from "@app/lib/utils";
|
||||
import {
|
||||
didFromPublicKey,
|
||||
explorerHost,
|
||||
explorerUrl,
|
||||
truncateDid,
|
||||
} from "@app/lib/utils";
|
||||
|
||||
import Button from "@app/components/Button.svelte";
|
||||
import DropdownListItem from "@app/components/DropdownListItem.svelte";
|
||||
@@ -60,11 +65,14 @@
|
||||
style:text-decoration="none"
|
||||
style:width="100%"
|
||||
onclick={closeFocused}
|
||||
href={explorerUrl(`users/${didFromPublicKey(config.publicKey)}`)}
|
||||
href={explorerUrl(
|
||||
`users/${didFromPublicKey(config.publicKey)}`,
|
||||
config,
|
||||
)}
|
||||
target="_blank">
|
||||
<DropdownListItem styleGap="0.5rem" styleWidth="100%" selected={false}>
|
||||
<Icon name="seed" />
|
||||
view on iris.radicle.network
|
||||
Open in {explorerHost(config)}
|
||||
<span style:margin-left="auto"><Icon name="open-external" /></span>
|
||||
</DropdownListItem>
|
||||
</a>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
|
||||
import debounce from "lodash/debounce";
|
||||
@@ -15,9 +16,10 @@
|
||||
|
||||
interface Props {
|
||||
repo: RepoInfo;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const { repo }: Props = $props();
|
||||
const { repo, config }: Props = $props();
|
||||
|
||||
const project = $derived(repo.payloads["xyz.radicle.project"]!);
|
||||
|
||||
@@ -27,7 +29,7 @@
|
||||
}, 1000);
|
||||
|
||||
async function copyLink() {
|
||||
await writeToClipboard(explorerUrl(repo.rid));
|
||||
await writeToClipboard(explorerUrl(repo.rid, config));
|
||||
copyIcon = "checkmark";
|
||||
restoreCopyIcon();
|
||||
}
|
||||
@@ -131,7 +133,7 @@
|
||||
style:white-space="nowrap"
|
||||
style:text-decoration="none"
|
||||
style:width="100%"
|
||||
href={explorerUrl(`users/${delegate.did}`)}
|
||||
href={explorerUrl(`users/${delegate.did}`, config)}
|
||||
target="_blank">
|
||||
{#if delegate.alias}
|
||||
<span class="txt-overflow alias">
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
||||
import type { RepoSummary } from "@bindings/repo/RepoSummary";
|
||||
|
||||
@@ -53,7 +54,11 @@
|
||||
writeToClipboard,
|
||||
} from "@app/lib/invoke";
|
||||
import * as router from "@app/lib/router";
|
||||
import { explorerUrl, formatRepositoryId } from "@app/lib/utils";
|
||||
import {
|
||||
explorerHost,
|
||||
explorerUrl,
|
||||
formatRepositoryId,
|
||||
} from "@app/lib/utils";
|
||||
|
||||
import AddRepoButton from "@app/components/AddRepoButton.svelte";
|
||||
import ContextMenu from "@app/components/ContextMenu.svelte";
|
||||
@@ -69,12 +74,14 @@
|
||||
initialRepos: RepoSummary[];
|
||||
initialSeededNotReplicated: string[];
|
||||
activeRepo?: RepoInfo;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const {
|
||||
initialRepos,
|
||||
initialSeededNotReplicated,
|
||||
activeRepo = undefined,
|
||||
config,
|
||||
}: Props = $props();
|
||||
|
||||
let repos: RepoSummary[] = $derived(initialRepos);
|
||||
@@ -716,7 +723,7 @@
|
||||
|
||||
{#if contextMenu}
|
||||
{@const repo = contextMenu.repo}
|
||||
{@const url = explorerUrl(repo.rid)}
|
||||
{@const url = explorerUrl(repo.rid, config)}
|
||||
<ContextMenu
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
@@ -741,7 +748,7 @@
|
||||
role="menuitem"
|
||||
onclick={() => writeToClipboard(url)}>
|
||||
<Icon name="link" />
|
||||
Copy link to radicle.network
|
||||
Copy link to {explorerHost(config)}
|
||||
</button>
|
||||
<a
|
||||
class="menu-item"
|
||||
@@ -750,7 +757,7 @@
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">
|
||||
<Icon name="open-external" />
|
||||
Open on radicle.network
|
||||
Open in {explorerHost(config)}
|
||||
</a>
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
|
||||
+34
-6
@@ -2,6 +2,7 @@ import type { Author } from "@bindings/cob/Author";
|
||||
import type { Issue } from "@bindings/cob/issue/Issue";
|
||||
import type { Patch } from "@bindings/cob/patch/Patch";
|
||||
import type { Review } from "@bindings/cob/patch/Review";
|
||||
import type { Config } from "@bindings/config/Config";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
import bs58 from "bs58";
|
||||
@@ -288,10 +289,37 @@ export function safeHttpUrl(url: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function explorerUrl(
|
||||
path: string,
|
||||
seed = "iris.radicle.network",
|
||||
explorer = "https://radicle.network",
|
||||
) {
|
||||
return `${explorer}/nodes/${seed}/${path}`;
|
||||
// Seed to fall back to when `config.preferredSeeds` is empty.
|
||||
const DEFAULT_SEED = "rosa.radicle.network";
|
||||
|
||||
// The host of the first entry in `config.preferredSeeds`, if any, parsed from
|
||||
// its "<NID>@<host>:<port>" address form. Used to pick a seed that's actually
|
||||
// known to replicate the user's repos, instead of a hardcoded default.
|
||||
function preferredSeedHost(config: Config): string | undefined {
|
||||
const address = config.preferredSeeds[0];
|
||||
if (!address) {
|
||||
return undefined;
|
||||
}
|
||||
return address.split("@")[1]?.split(":")[0] ?? address;
|
||||
}
|
||||
|
||||
// The hostname of the configured public explorer, e.g. "radicle.network".
|
||||
// Used to label links with the site they actually open.
|
||||
export function explorerHost(config: Config): string {
|
||||
try {
|
||||
return new URL(config.publicExplorer).host;
|
||||
} catch {
|
||||
return config.publicExplorer;
|
||||
}
|
||||
}
|
||||
|
||||
// Build an explorer URL, honouring the user's configured `publicExplorer`
|
||||
// template and preferred seed. The template mirrors radicle's `Explorer`
|
||||
// type: "<base>/nodes/$host/$rid$path". `path` is everything after the seed
|
||||
// host, e.g. `<rid>/issues/<id>` or `users/<did>`.
|
||||
export function explorerUrl(path: string, config: Config): string {
|
||||
const seed = preferredSeedHost(config) ?? DEFAULT_SEED;
|
||||
return config.publicExplorer
|
||||
.replace("$host", seed)
|
||||
.replace("$rid$path", path);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import * as roles from "@app/lib/roles";
|
||||
import * as router from "@app/lib/router";
|
||||
import {
|
||||
explorerHost,
|
||||
explorerUrl,
|
||||
issueStatusBackgroundColor,
|
||||
issueStatusColor,
|
||||
@@ -324,8 +325,8 @@
|
||||
<Icon name="chevron-right" />
|
||||
<Id id={issue.id} clipboard={issue.id} placement="bottom-start" />
|
||||
<ExternalLink
|
||||
href={explorerUrl(`${repo.rid}/issues/${issue.id}`)}
|
||||
title="Open in radicle.network" />
|
||||
href={explorerUrl(`${repo.rid}/issues/${issue.id}`, config)}
|
||||
title={`Open in ${explorerHost(config)}`} />
|
||||
</div>
|
||||
<div style:margin-left="auto">
|
||||
<Button
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import * as router from "@app/lib/router";
|
||||
import {
|
||||
didFromPublicKey,
|
||||
explorerHost,
|
||||
explorerUrl,
|
||||
patchStatusBackgroundColor,
|
||||
patchStatusColor,
|
||||
@@ -361,8 +362,8 @@
|
||||
<Icon name="chevron-right" />
|
||||
<Id id={patch.id} clipboard={patch.id} placement="bottom-start" />
|
||||
<ExternalLink
|
||||
href={explorerUrl(`${repo.rid}/patches/${patch.id}`)}
|
||||
title="Open in radicle.network" />
|
||||
href={explorerUrl(`${repo.rid}/patches/${patch.id}`, config)}
|
||||
title={`Open in ${explorerHost(config)}`} />
|
||||
</div>
|
||||
<div style:margin-left="auto">
|
||||
<NewPatchButton rid={repo.rid} ghost />
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
import { diffOptions } from "@app/lib/diffOptions.svelte";
|
||||
import { getDiffText, invoke } from "@app/lib/invoke";
|
||||
import * as router from "@app/lib/router";
|
||||
import type { SidebarData } from "@app/lib/router/definitions";
|
||||
import {
|
||||
absoluteTimestamp,
|
||||
explorerHost,
|
||||
explorerUrl,
|
||||
formatOid,
|
||||
formatTimestamp,
|
||||
@@ -43,9 +45,10 @@
|
||||
// Unified patch text feeding the rendered diff (Pierre renders from this,
|
||||
// not from the structured `diff`).
|
||||
patch: string;
|
||||
sidebarData: SidebarData;
|
||||
}
|
||||
|
||||
const { repo, commit, diff, patch }: Props = $props();
|
||||
const { repo, commit, diff, patch, sidebarData }: Props = $props();
|
||||
|
||||
let diffView = $state<{
|
||||
scrollToFile: (path: string) => void;
|
||||
@@ -373,8 +376,11 @@
|
||||
<Icon name="chevron-right" />
|
||||
<Id id={commit.id} clipboard={commit.id} placement="bottom-start" />
|
||||
<ExternalLink
|
||||
href={explorerUrl(`${repo.rid}/commits/${commit.id}`)}
|
||||
title="Open in app.radicle.xyz" />
|
||||
href={explorerUrl(
|
||||
`${repo.rid}/commits/${commit.id}`,
|
||||
sidebarData.config,
|
||||
)}
|
||||
title={`Open in ${explorerHost(sidebarData.config)}`} />
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<Button
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { invoke } from "@app/lib/invoke";
|
||||
import { createPaginatedList } from "@app/lib/paginatedList.svelte";
|
||||
import * as router from "@app/lib/router";
|
||||
import type { SidebarData } from "@app/lib/router/definitions";
|
||||
import { modifierKey } from "@app/lib/utils";
|
||||
|
||||
import CobCommitTeaser from "@app/components/CobCommitTeaser.svelte";
|
||||
@@ -27,9 +28,11 @@
|
||||
oid: string;
|
||||
commit: Commit;
|
||||
commits: PaginatedQuery<Commit[]>;
|
||||
sidebarData: SidebarData;
|
||||
}
|
||||
|
||||
const { repo, peer, revision, oid, commit, commits }: Props = $props();
|
||||
const { repo, peer, revision, oid, commit, commits, sidebarData }: Props =
|
||||
$props();
|
||||
|
||||
const baseRoute = $derived({
|
||||
resource: "repo.commits" as const,
|
||||
@@ -209,7 +212,7 @@
|
||||
|
||||
<Layout selfScroll>
|
||||
<div class="page">
|
||||
<RepoHeader {repo} />
|
||||
<RepoHeader {repo} config={sidebarData.config} />
|
||||
<SourceHeader
|
||||
{repo}
|
||||
{peer}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { useOverlayScrollbars } from "overlayscrollbars-svelte";
|
||||
|
||||
import { invoke, InvokeError } from "@app/lib/invoke";
|
||||
import type { SidebarData } from "@app/lib/router/definitions";
|
||||
import { highlight } from "@app/lib/syntax";
|
||||
|
||||
import FileBlock from "@app/components/FileBlock.svelte";
|
||||
@@ -32,10 +33,12 @@
|
||||
oid: string;
|
||||
commit: Commit;
|
||||
readme: Readme | null;
|
||||
sidebarData: SidebarData;
|
||||
}
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
let { tree, readme, repo, peer, revision, oid, commit }: Props = $props();
|
||||
let { tree, readme, repo, peer, revision, oid, commit, sidebarData }: Props =
|
||||
$props();
|
||||
/* eslint-enable prefer-const */
|
||||
|
||||
const baseRoute = $derived({
|
||||
@@ -141,7 +144,7 @@
|
||||
style:display="flex"
|
||||
style:flex-direction="column"
|
||||
style:height="100%">
|
||||
<RepoHeader {repo} />
|
||||
<RepoHeader {repo} config={sidebarData.config} />
|
||||
<SourceHeader
|
||||
{repo}
|
||||
{peer}
|
||||
|
||||
Reference in New Issue
Block a user