Files
radicle-desktop/src/components/InboxList.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

180 lines
4.7 KiB
Svelte

<script lang="ts">
import type { NotificationsByRepo } from "@bindings/cob/inbox/NotificationsByRepo";
import Button from "@app/components/Button.svelte";
import ConfirmClear from "@app/components/ConfirmClear.svelte";
import NotificationsByRepoComponent from "@app/components/NotificationsByRepo.svelte";
interface Props {
clearAll: () => Promise<void>;
clearByIds: (ids: string[]) => Promise<void>;
clearByRepo: (rid: string) => Promise<void>;
loadNew: () => Promise<void>;
notificationCount: number | undefined;
notificationsByRepo: NotificationsByRepo[];
showAll: (rid: string) => Promise<void>;
}
const {
clearAll,
clearByIds,
clearByRepo,
loadNew,
notificationCount,
notificationsByRepo,
showAll,
}: Props = $props();
let pinnedRepos: string[] = $state(loadPinnedRepos());
let hiddenRepos: string[] = $state(loadHiddenRepos());
function loadPinnedRepos(): string[] {
const storedPinnedRepos = localStorage
? localStorage.getItem("pinnedInboxRepos")
: null;
if (storedPinnedRepos === null) {
return [];
} else {
return JSON.parse(storedPinnedRepos);
}
}
function updatePinnedRepos(newRepos: string[]) {
pinnedRepos = newRepos;
localStorage.setItem("pinnedInboxRepos", JSON.stringify(newRepos));
}
function togglePin(rid: string) {
const repos = loadPinnedRepos();
if (repos.includes(rid)) {
updatePinnedRepos(repos.filter(r => r !== rid));
} else {
updatePinnedRepos([rid, ...repos]);
}
}
function loadHiddenRepos(): string[] {
const storedHiddenRepos = localStorage
? localStorage.getItem("hiddenInboxRepos")
: null;
if (storedHiddenRepos === null) {
return [];
} else {
return JSON.parse(storedHiddenRepos);
}
}
function updateHiddenRepos(newRepos: string[]) {
hiddenRepos = newRepos;
localStorage.setItem("hiddenInboxRepos", JSON.stringify(newRepos));
}
function toggleHide(rid: string) {
const repos = loadHiddenRepos();
if (repos.includes(rid)) {
updateHiddenRepos(repos.filter(r => r !== rid));
} else {
updateHiddenRepos([rid, ...repos]);
}
}
function sortedRepos(
allRepos: NotificationsByRepo[],
pinned: string[],
hidden: string[],
) {
// Preserve pinning order.
const pinnedRepos = pinned
.map(p => allRepos.find(r => r.rid === p))
.filter((repo): repo is NotificationsByRepo => repo !== undefined);
const sortedRepos = allRepos
.filter(r => !pinned.includes(r.rid) && !hidden.includes(r.rid))
.sort((a, b) => a.name.localeCompare(b.name));
const hiddenRepos = allRepos
.filter(r => hidden.includes(r.rid))
.sort((a, b) => a.name.localeCompare(b.name));
return [...pinnedRepos, ...sortedRepos, ...hiddenRepos];
}
function loadedNotificationCount() {
return notificationsByRepo.reduce((acc, repo) => {
return acc + repo.count;
}, 0);
}
</script>
<style>
.container {
width: 100%;
display: flex;
flex-direction: column;
min-height: 100%;
}
.header {
font: var(--txt-heading-m);
display: flex;
align-items: center;
min-height: 2rem;
}
.clear-inbox {
margin-left: auto;
display: none;
color: var(--color-text-tertiary);
}
.header:hover .clear-inbox {
display: flex;
}
.repo-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
</style>
<div class="container">
{#if notificationCount !== undefined && notificationCount > 0}
<div class="header">
<div class="global-flex">
Inbox
<span class="global-counter-badge">{notificationCount}</span>
</div>
{#if notificationCount > loadedNotificationCount()}
<div
class="txt-missing txt-body-m-regular global-flex"
style:margin-left="1rem">
<Button variant="naked" onclick={loadNew}>
See {notificationCount - loadedNotificationCount()} new
</Button>
</div>
{/if}
<div class="clear-inbox">
<ConfirmClear count={notificationCount} clear={clearAll} />
</div>
</div>
{/if}
{#if notificationCount !== undefined && notificationCount > 0}
<div class="repo-list">
{#each sortedRepos(notificationsByRepo, pinnedRepos, hiddenRepos) as repo}
<NotificationsByRepoComponent
count={repo.count}
groupedNotifications={repo.notifications}
hidden={hiddenRepos.includes(repo.rid)}
name={repo.name}
pinned={pinnedRepos.includes(repo.rid)}
rid={repo.rid}
{clearByIds}
{clearByRepo}
{showAll}
{toggleHide}
{togglePin} />
{/each}
</div>
{/if}
</div>