mirror of
https://github.com/block/buzz.git
synced 2026-11-12 20:31:12 +00:00
## Summary Follow-up batch on the Projects overview (continues merged #1677): - **Repository access restrictions** — repositories the viewer can't reach are surfaced with a reason instead of failing silently. Channel-ACL denials (which arrive as the same 404 as a missing repo, for anti-enumeration) are re-classified using the repository's channel binding and the viewer's memberships (`useRepositoryAccess.ts`, `projectRepoAvailability.ts`). - **Projects loads in seconds instead of minutes** — enumeration no longer crawls every kind:5 deletion event on the relay. It fetches project/repo announcements first, then queries deletions scoped to those coordinates via chunked `#a` filters (3 queries instead of hundreds on staging). - **Activity feed layout polish** — bare event-type glyph beside the headline (no badge circle), timeline spine runs through the avatars connecting consecutive cards, linkable actor/project names are bold in theme foreground, rounded hover state, alignment fixes. - **Create button pinned** — the "+" create menu is pinned to the pane's top-right corner (equal 16px insets) and no longer scrolls away with the page header. - **List controls as a table header** — the scope selector (left) and sort + layout toggle (right) render as the first row of the list container on the Projects/Repositories/PRs/Issues tabs; in card view the identical bar stands alone with the cards below (`ProjectsListHeaderBar.tsx`). - **Repository rows show the git location** — subtitle is `github.com/org/repo` for external repos or `owner/repo` (resolved profile name) for Buzz-hosted ones, instead of repeating the project name (`repositoryDisplayPath`). - **Uniform work-item row heights** — issue rows previously ran the author chip in inline flow, letting the 20px avatar grow the line box ~3px taller than PR rows; both lists now share the same flex subtitle. 📸 Screenshots: [feed layout / pinned button](https://github.com/block/buzz/pull/5073#issuecomment-5214114069) · [list header / repo subtitles / row heights](https://github.com/block/buzz/pull/5073#issuecomment-5217480712). Note: two empty `chore: retrigger CI` commits exist on the branch from working around the Aug 6 GitHub Actions incident; happy to drop them with a signoff rebase before undrafting if preferred. Latest `main` is merged in (`a0cc35220`). ## Test plan - [x] Desktop unit tests (4,493 pass after merging main), Biome, tsc - [x] New unit tests for scoped deletion enumeration and repo availability re-classification - [x] New unit tests for `repositoryDisplayPath` (external, Buzz-hosted, unresolvable) - [x] Screenshot verification of feed layout, connector spine, and pinned button (top + scrolled states) — posted to the PR - [x] Screenshot verification of the list header row (list + card), repo subtitles, and matching PR/issue row heights — posted to the PR - [ ] Manual pass against staging (projects list load time, restricted-repo states) --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com>
114 lines
3.2 KiB
TypeScript
114 lines
3.2 KiB
TypeScript
import { effectiveCloneUrls } from "./projectCloneUrl";
|
|
|
|
export type ProjectRepoHost =
|
|
| { kind: "buzz" }
|
|
| { kind: "external"; host: string }
|
|
| { kind: "unresolved" };
|
|
|
|
/**
|
|
* Classifies the canonical git remote using the same origin and path boundary
|
|
* enforced by the Tauri git commands. This is presentation/query gating only;
|
|
* Rust remains the security boundary for clone operations.
|
|
*/
|
|
export function projectRepoHost(
|
|
cloneUrl: string | null | undefined,
|
|
relayOrigin: string | null | undefined,
|
|
): ProjectRepoHost {
|
|
if (!cloneUrl || !relayOrigin) return { kind: "unresolved" };
|
|
|
|
try {
|
|
const clone = new URL(cloneUrl);
|
|
const relay = new URL(relayOrigin);
|
|
const isBuzzPath = /^\/git\/[0-9a-f]{64}\/[^/]+\/?$/i.test(clone.pathname);
|
|
|
|
if (clone.origin === relay.origin && isBuzzPath) {
|
|
return { kind: "buzz" };
|
|
}
|
|
|
|
return { kind: "external", host: clone.host };
|
|
} catch {
|
|
return { kind: "unresolved" };
|
|
}
|
|
}
|
|
|
|
type RepositoryHostInput = {
|
|
cloneUrls: string[];
|
|
dtag: string;
|
|
owner: string;
|
|
repoAddress?: string;
|
|
};
|
|
|
|
export function projectRepoHostForRepository(
|
|
repository: RepositoryHostInput | null | undefined,
|
|
relayOrigin: string | null | undefined,
|
|
): ProjectRepoHost {
|
|
if (!repository) return { kind: "unresolved" };
|
|
const cloneUrl = effectiveCloneUrls(
|
|
repository.cloneUrls,
|
|
relayOrigin,
|
|
repository.owner,
|
|
repository.dtag,
|
|
)[0];
|
|
return projectRepoHost(cloneUrl, relayOrigin);
|
|
}
|
|
|
|
/**
|
|
* Human-readable location of a repository's git data — "github.com/block/buzz"
|
|
* for external repos (host + path, `.git` stripped), or "owner/repo" for
|
|
* Buzz-hosted ones (the relay host and full owner pubkey carry no signal;
|
|
* `ownerLabel` should be the resolved profile name, falling back to a
|
|
* shortened pubkey). Returns `null` when no clone URL can be resolved.
|
|
*/
|
|
export function repositoryDisplayPath(
|
|
repository: RepositoryHostInput | null | undefined,
|
|
relayOrigin: string | null | undefined,
|
|
ownerLabel?: string | null,
|
|
): string | null {
|
|
if (!repository) return null;
|
|
const cloneUrl = effectiveCloneUrls(
|
|
repository.cloneUrls,
|
|
relayOrigin,
|
|
repository.owner,
|
|
repository.dtag,
|
|
)[0];
|
|
if (!cloneUrl) return null;
|
|
|
|
if (projectRepoHost(cloneUrl, relayOrigin).kind === "buzz") {
|
|
const owner = ownerLabel?.trim() || `${repository.owner.slice(0, 8)}…`;
|
|
return `${owner}/${repository.dtag}`;
|
|
}
|
|
|
|
try {
|
|
const url = new URL(cloneUrl);
|
|
const path = url.pathname.replace(/\.git$/, "").replace(/\/+$/, "");
|
|
return `${url.host}${path}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function projectRepoHostForProject(
|
|
project:
|
|
| RepositoryHostInput
|
|
| {
|
|
primaryRepositoryAddress: string | null;
|
|
repositories: RepositoryHostInput[];
|
|
}
|
|
| null
|
|
| undefined,
|
|
relayOrigin: string | null | undefined,
|
|
): ProjectRepoHost {
|
|
if (!project) return { kind: "unresolved" };
|
|
if (!("repositories" in project)) {
|
|
return projectRepoHostForRepository(project, relayOrigin);
|
|
}
|
|
|
|
const repository =
|
|
project.repositories.find(
|
|
(candidate) => candidate.repoAddress === project.primaryRepositoryAddress,
|
|
) ??
|
|
project.repositories[0] ??
|
|
null;
|
|
return projectRepoHostForRepository(repository, relayOrigin);
|
|
}
|