Files
radicle-explorer/src/App.svelte
T
Rūdolfs Ošiņš 010cac0377 Redesign app with new UI kit
Redesign the entire app with a new UI kit. Typography, colors, icons,
avatars, and border radii have all been updated for a more polished,
consistent appearance that matches our other products like
radicle-explorer.

- Replace blockies-based avatars with a new DID-based avatar system
- Move Settings and Create Issue into fullscreen modals
- Change the default theme to follow the system theme
- Remove window decorations on macOS for a cleaner, more native feel
- Update the app icon
- Fix popovers and tooltips

Navigation:

- Rewrite app navigation and layouts
- Add a sidebar that gives you access to all your repos
- Change the startup screen to open directly to the notification inbox
- Add a getting started guide shown to new users on first launch

Settings:

- Add a setting to disable the notification badge
- Add a code font setting

Bug fixes:

- Fix reactions and edits incorrectly targeting the root comment
  instead of the intended reply
- Restrict issue/patch state, labels and assignees to delegates only
2026-03-20 17:48:38 -06:00

214 lines
6.6 KiB
Svelte

<script lang="ts">
import type { Config } from "@bindings/config/Config";
import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import delay from "lodash/delay";
import { onDestroy, onMount } from "svelte";
import { get } from "svelte/store";
import {
decreaseFontSize,
fontSettings,
increaseFontSize,
resetFontSize,
} from "@app/lib/appearance.svelte";
import { checkAuth, startup } from "@app/lib/auth.svelte";
import { nodeRunning } from "@app/lib/events";
import { dynamicInterval } from "@app/lib/interval";
import { invoke } from "@app/lib/invoke";
import { hide } from "@app/lib/modal";
import * as router from "@app/lib/router";
import {
setUnlistenNodeEvents,
unlistenNodeEvents,
} from "@app/lib/startup.svelte";
import { isMac, unreachable } from "@app/lib/utils";
import { codeFont } from "@app/components/CodeFontSwitch.svelte";
import {
followSystemTheme,
loadTheme,
theme,
} from "@app/components/ThemeSwitch.svelte";
import GuideView from "@app/modals/Guide.svelte";
import Auth from "@app/views/auth/Auth.svelte";
import CreateIdentity from "@app/views/auth/CreateIdentity.svelte";
import InboxView from "@app/views/Inbox.svelte";
import Issue from "@app/views/repo/Issue.svelte";
import Issues from "@app/views/repo/Issues.svelte";
import Patch from "@app/views/repo/Patch.svelte";
import Patches from "@app/views/repo/Patches.svelte";
import RepoHome from "@app/views/repo/RepoHome.svelte";
import Command from "./components/Command.svelte";
import ExternalLink from "./components/ExternalLink.svelte";
import FullscreenModalPortal from "./components/FullscreenModalPortal.svelte";
import FullWindowError from "./components/FullWindowError.svelte";
import Spinner from "./components/Spinner.svelte";
const activeRouteStore = router.activeRouteStore;
const DRAG_REGION_HEIGHT = 32;
const INTERACTIVE_TAGS = new Set([
"a",
"button",
"input",
"select",
"textarea",
]);
function isDraggableArea(e: MouseEvent): boolean {
if (e.clientY > DRAG_REGION_HEIGHT) return false;
let el = e.target as HTMLElement | null;
while (el && el !== document.body) {
if (INTERACTIVE_TAGS.has(el.tagName.toLowerCase())) return false;
if (el.getAttribute("role") === "button") return false;
if (el.classList.contains("txt-selectable")) return false;
el = el.parentElement;
}
return true;
}
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", ({ matches }) => {
if (get(followSystemTheme)) {
theme.set(matches ? "dark" : "light");
}
});
if (get(followSystemTheme)) {
theme.set(loadTheme());
}
let profile = $state<Config>();
let showSpinner = $state(false);
delay(() => (showSpinner = true), 1000);
onMount(async () => {
try {
profile = await invoke<Config>("startup");
} catch (err) {
startup.error = err as ErrorWrapper;
return;
}
if (window.__TAURI_INTERNALS__) {
setUnlistenNodeEvents(
await listen<boolean>("node_running", event => {
nodeRunning.set(event.payload);
}),
);
}
try {
await invoke("authenticate");
void router.loadFromLocation();
dynamicInterval(
"auth",
checkAuth,
import.meta.env.VITE_AUTH_LONG_DELAY || 30_000,
);
} catch (err) {
startup.error = err as ErrorWrapper;
void router.push({ resource: "booting" });
dynamicInterval("auth", checkAuth, 5_000);
}
});
onDestroy(() => {
if (unlistenNodeEvents) {
unlistenNodeEvents();
}
});
$effect(() =>
document.documentElement.style.setProperty(
"--font-size",
`${fontSettings.size}px`,
),
);
$effect(() => document.documentElement.setAttribute("data-theme", $theme));
$effect(() =>
document.documentElement.setAttribute("data-codefont", $codeFont),
);
</script>
<style>
.spinner {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
</style>
<svelte:document
onmousedown={e => {
if (window.__TAURI_INTERNALS__ && isDraggableArea(e)) {
void getCurrentWindow().startDragging();
}
}}
onkeydown={e => {
const auxiliarKey = isMac() ? e.metaKey : e.ctrlKey;
// Handles the position of the plus key on different keyboard layouts.
const plusKey = e.key === "1" || e.key === "=";
if (e.key === "Escape") {
hide();
} else if (auxiliarKey && (e.key === "+" || plusKey)) {
increaseFontSize();
} else if (auxiliarKey && e.key === "-") {
decreaseFontSize();
} else if (auxiliarKey && e.key.toLowerCase() === "0") {
resetFontSize();
}
}} />
<FullscreenModalPortal />
{#if $activeRouteStore.resource === "booting"}
{#if startup.error?.code === "IdentityError.MissingProfile"}
<CreateIdentity />
{:else if startup.error?.code === "PassphraseError.InvalidPassphrase" && profile}
<Auth profile={{ did: profile.publicKey, alias: profile.alias }} />
{:else if startup.error}
<FullWindowError title="An error occurred" error={startup.error}>
We were unable to load your Radicle identity, or your Radicle installation
is outdated.
<br />
If you have an existing Radicle installation, make sure you have
<ExternalLink href="https://radicle.xyz/download">
the latest version.
</ExternalLink>
<br />
<br />
<div
style="display: flex; flex-direction: column; align-items: center; gap: 0.5rem;">
To migrate your Radicle storage, make sure to restart your radicle-node
or run:
<Command styleWidth="30rem" command="rad cob migrate" />
</div>
</FullWindowError>
{:else if showSpinner}
<div class="spinner"><Spinner /></div>
{/if}
{:else if $activeRouteStore.resource === "inbox"}
<InboxView {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "guide"}
<GuideView {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.home"}
<RepoHome {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issue"}
<Issue {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issues"}
<Issues {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.patch"}
<Patch {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.patches"}
<Patches {...$activeRouteStore.params} />
{:else}
{unreachable($activeRouteStore)}
{/if}