From a03c413343295f3bcb0f237331b59f0fa9b27de1 Mon Sep 17 00:00:00 2001 From: Brandon Oxendine Date: Thu, 6 Aug 2026 17:52:45 +0200 Subject: [PATCH] Add an author hover card --- src/components/NodeId.svelte | 420 ++++++++++++++++++++++++++++++-- src/lib/invoke.ts | 17 ++ src/lib/patchActivityContext.ts | 32 +++ 3 files changed, 454 insertions(+), 15 deletions(-) create mode 100644 src/lib/patchActivityContext.ts diff --git a/src/components/NodeId.svelte b/src/components/NodeId.svelte index 3235aedfc..07d73c9bb 100644 --- a/src/components/NodeId.svelte +++ b/src/components/NodeId.svelte @@ -1,8 +1,22 @@ -
-
- -
- {#if alias} - - {alias} - - {:else} - - {truncateId(publicKey)} - - {/if} -
+ + {#snippet toggle(_onclick)} + +
+
+ +
+ {#if effectiveAlias} + + {effectiveAlias} + + {:else} + + {truncateId(publicKey)} + + {/if} +
+ {/snippet} + {#snippet popover()} + +
+
+
+ +
+
+ {#if effectiveAlias} +
+ {effectiveAlias} +
+ {:else} +
+ {truncateId(publicKey)} +
+ {/if} + {#if activity?.isAuthor || activity?.isDelegate} +
+ {#if activity?.isAuthor} +
+ + Patch author +
+ {/if} + {#if activity?.isDelegate} +
+ + Delegate +
+ {/if} +
+ {/if} + +
+
+ {#if activity} + {@const patchFacts = ( + [ + activity.revisionCount > 0 + ? { + icon: "revision" as IconName, + label: `${activity.revisionCount} ${pluralize("revision", activity.revisionCount)}`, + } + : undefined, + activity.commitCount > 0 + ? { + icon: "commit" as IconName, + label: `${activity.commitCount} ${pluralize("commit", activity.commitCount)}`, + } + : undefined, + activity.reviewCount > 0 + ? { + icon: "comment" as IconName, + label: `${activity.reviewCount} ${pluralize("review", activity.reviewCount)}`, + } + : undefined, + ] as ({ icon: IconName; label: string } | undefined)[] + ).filter( + (x): x is { icon: IconName; label: string } => x !== undefined, + )} + {@const repoFacts = ( + [ + activity.patchesAuthored > 0 + ? { + icon: "patch" as IconName, + label: `${activity.patchesAuthored} ${pluralize("patch", activity.patchesAuthored)}`, + } + : undefined, + activity.issuesAuthored > 0 + ? { + icon: "issue" as IconName, + label: `${activity.issuesAuthored} ${pluralize("issue", activity.issuesAuthored)}`, + } + : undefined, + ] as ({ icon: IconName; label: string } | undefined)[] + ).filter( + (x): x is { icon: IconName; label: string } => x !== undefined, + )} + {#if patchFacts.length > 0 || repoFacts.length > 0} +
+ {#if patchFacts.length > 0} +
+
+ On this patch +
+ {#each patchFacts as item (item.label)} +
+ + {item.label} +
+ {/each} +
+ {/if} + {#if patchFacts.length > 0 && repoFacts.length > 0} +
+ {/if} + {#if repoFacts.length > 0} +
+
+ In this repo +
+ {#each repoFacts as item (item.label)} +
+ + {item.label} +
+ {/each} +
+ {/if} +
+ {/if} + {/if} + +
+ {/snippet} +
diff --git a/src/lib/invoke.ts b/src/lib/invoke.ts index 5690dce1c..5d2a2c9d6 100644 --- a/src/lib/invoke.ts +++ b/src/lib/invoke.ts @@ -1,4 +1,5 @@ import type { DiffOptions } from "@bindings/cob/DiffOptions"; +import type { Config } from "@bindings/config/Config"; import type { Diff } from "@bindings/diff/Diff"; import type { Stats } from "@bindings/diff/Stats"; import type { Commit } from "@bindings/repo/Commit"; @@ -139,6 +140,22 @@ export const cachedListJobs = cached( { max: 200, ttl: 30_000 }, ); +async function alias(nid: string): Promise { + return withTestBackend(tauri.invoke, "alias", { nid }); +} + +export const cachedAlias = cached(alias, (...[nid]) => `alias:${nid}`, { + max: 5_000, +}); + +async function config(): Promise { + return withTestBackend(tauri.invoke, "config", {}); +} + +// The node config is static for the session, so a single-entry cache collapses +// the many callers (e.g. every NodeId hover card) into one in-flight request. +export const cachedConfig = cached(config, () => "config", { max: 1 }); + async function diffStats( rid: string, base: string, diff --git a/src/lib/patchActivityContext.ts b/src/lib/patchActivityContext.ts new file mode 100644 index 000000000..678d3cfbf --- /dev/null +++ b/src/lib/patchActivityContext.ts @@ -0,0 +1,32 @@ +import { getContext, setContext } from "svelte"; + +export interface PatchUserActivity { + isAuthor: boolean; + isDelegate: boolean; + revisionCount: number; + commitCount: number; + reviewCount: number; + patchesAuthored: number; + issuesAuthored: number; +} + +export interface PatchActivitySource { + /// Pure lookup: derives the counts from data already in hand. Safe to call + /// from a `$derived`. + resolve: (publicKey: string) => PatchUserActivity | undefined; + /// Loads the repo-wide figures the lookup needs for `patchesAuthored` and + /// `issuesAuthored`. Separate from `resolve` because it fetches, and a + /// `$derived` is no place to start one; call it from an effect when a card + /// is actually opened. + prefetch: () => void; +} + +const KEY = Symbol("patch-activity-context"); + +export function setPatchActivitySource(source: PatchActivitySource) { + setContext(KEY, source); +} + +export function getPatchActivitySource(): PatchActivitySource | undefined { + return getContext(KEY); +}