From bfbc936c865482d3badf43e5f7533ecaa62e1787 Mon Sep 17 00:00:00 2001 From: eti Date: Tue, 21 Apr 2026 12:24:57 +0200 Subject: [PATCH] ogre: show issue/pull author in card footer Replace the footer's leading slot with the author's avatar and handle for issue and pull request cards. For long handles, ellipsize and drop the reaction and comment counts to keep the tangled logo visible. appview/{issues,pulls}: send author info to ogre Resolve the author's handle and avatar URL from the issue/pull owner DID and pass them in the payload so ogre can render the author in the card footer. Signed-off-by: eti eti@eti.tf --- appview/issues/opengraph.go | 26 +++++++------ appview/pulls/opengraph.go | 4 ++ ogre/client.go | 26 +++++++------ ogre/src/__tests__/fixtures.ts | 4 ++ ogre/src/__tests__/render.test.ts | 22 +++++++++++ ogre/src/components/cards/issue.tsx | 2 + ogre/src/components/cards/pull-request.tsx | 2 + ogre/src/components/shared/footer-stats.tsx | 43 +++++++++++++++++++-- ogre/src/validation.ts | 4 ++ 9 files changed, 108 insertions(+), 25 deletions(-) diff --git a/appview/issues/opengraph.go b/appview/issues/opengraph.go index 8f19aba7..377efe94 100644 --- a/appview/issues/opengraph.go +++ b/appview/issues/opengraph.go @@ -68,8 +68,10 @@ func (rp *Issues) IssueOpenGraphSummary(w http.ResponseWriter, r *http.Request) } ownerHandle := rp.pages.DisplayHandle(r.Context(), f.Did) + authorHandle := rp.pages.DisplayHandle(r.Context(), issue.Did) avatarUrl := rp.pages.AvatarUrl(f.Did, "256") + authorAvatarUrl := rp.pages.AvatarUrl(issue.Did, "256") status := "closed" if issue.Open { @@ -81,17 +83,19 @@ func (rp *Issues) IssueOpenGraphSummary(w http.ResponseWriter, r *http.Request) reactionCount, _ := db.GetReactionCount(rp.db, issue.AtUri()) payload := ogre.IssueCardPayload{ - Type: "issue", - RepoName: f.Name, - OwnerHandle: ownerHandle, - AvatarUrl: avatarUrl, - Title: issue.Title, - IssueNumber: issue.IssueId, - Status: status, - Labels: labels, - CommentCount: commentCount, - ReactionCount: reactionCount, - CreatedAt: issue.Created.Format(time.RFC3339), + Type: "issue", + RepoName: f.Name, + OwnerHandle: ownerHandle, + AuthorHandle: authorHandle, + AvatarUrl: avatarUrl, + AuthorAvatarUrl: authorAvatarUrl, + Title: issue.Title, + IssueNumber: issue.IssueId, + Status: status, + Labels: labels, + CommentCount: commentCount, + ReactionCount: reactionCount, + CreatedAt: issue.Created.Format(time.RFC3339), } imageBytes, err := rp.ogreClient.RenderIssueCard(r.Context(), payload) diff --git a/appview/pulls/opengraph.go b/appview/pulls/opengraph.go index f99a8d99..5843c608 100644 --- a/appview/pulls/opengraph.go +++ b/appview/pulls/opengraph.go @@ -26,8 +26,10 @@ func (s *Pulls) PullOpenGraphSummary(w http.ResponseWriter, r *http.Request) { } ownerHandle := s.pages.DisplayHandle(r.Context(), f.Did) + authorHandle := s.pages.DisplayHandle(r.Context(), pull.OwnerDid) avatarUrl := s.pages.AvatarUrl(f.Did, "256") + authorAvatarUrl := s.pages.AvatarUrl(pull.OwnerDid, "256") var status string if pull.State.IsOpen() { @@ -60,7 +62,9 @@ func (s *Pulls) PullOpenGraphSummary(w http.ResponseWriter, r *http.Request) { Type: "pullRequest", RepoName: f.Name, OwnerHandle: ownerHandle, + AuthorHandle: authorHandle, AvatarUrl: avatarUrl, + AuthorAvatarUrl: authorAvatarUrl, Title: pull.Title, PullRequestNumber: pull.PullId, Status: status, diff --git a/ogre/client.go b/ogre/client.go index b6ea7128..c55e46af 100644 --- a/ogre/client.go +++ b/ogre/client.go @@ -47,24 +47,28 @@ type RepositoryCardPayload struct { } type IssueCardPayload struct { - Type string `json:"type"` - RepoName string `json:"repoName"` - OwnerHandle string `json:"ownerHandle"` - AvatarUrl string `json:"avatarUrl"` - Title string `json:"title"` - IssueNumber int `json:"issueNumber"` - Status string `json:"status"` - Labels []LabelData `json:"labels"` - CommentCount int `json:"commentCount"` - ReactionCount int `json:"reactionCount"` - CreatedAt string `json:"createdAt"` + Type string `json:"type"` + RepoName string `json:"repoName"` + OwnerHandle string `json:"ownerHandle"` + AuthorHandle string `json:"authorHandle"` + AvatarUrl string `json:"avatarUrl"` + AuthorAvatarUrl string `json:"authorAvatarUrl"` + Title string `json:"title"` + IssueNumber int `json:"issueNumber"` + Status string `json:"status"` + Labels []LabelData `json:"labels"` + CommentCount int `json:"commentCount"` + ReactionCount int `json:"reactionCount"` + CreatedAt string `json:"createdAt"` } type PullRequestCardPayload struct { Type string `json:"type"` RepoName string `json:"repoName"` OwnerHandle string `json:"ownerHandle"` + AuthorHandle string `json:"authorHandle"` AvatarUrl string `json:"avatarUrl"` + AuthorAvatarUrl string `json:"authorAvatarUrl"` Title string `json:"title"` PullRequestNumber int `json:"pullRequestNumber"` Status string `json:"status"` diff --git a/ogre/src/__tests__/fixtures.ts b/ogre/src/__tests__/fixtures.ts index dce1f675..eae8a2b4 100644 --- a/ogre/src/__tests__/fixtures.ts +++ b/ogre/src/__tests__/fixtures.ts @@ -32,7 +32,9 @@ export const createIssueData = ( type: "issue", repoName: "core", ownerHandle: "tangled.org", + authorHandle: "oppi.li", avatarUrl, + authorAvatarUrl: avatarUrl, title: "feature request: sync fork button", issueNumber: 8, status: "open", @@ -54,7 +56,9 @@ export const createPullRequestData = ( type: "pullRequest", repoName: "core", ownerHandle: "tangled.org", + authorHandle: "oppi.li", avatarUrl, + authorAvatarUrl: avatarUrl, title: "add author description to README.md", pullRequestNumber: 1, status: "open", diff --git a/ogre/src/__tests__/render.test.ts b/ogre/src/__tests__/render.test.ts index 4213abcb..19bfa699 100644 --- a/ogre/src/__tests__/render.test.ts +++ b/ogre/src/__tests__/render.test.ts @@ -86,6 +86,17 @@ describe("issue cards", () => { const validated = issueCardSchema.parse(data); await renderAndSave(h(IssueCard, validated), "issue-card-long-title.png"); }); + + test("renders issue with long author handle (reactions hidden)", async () => { + const data = createIssueData(avatarDataUri, { + authorHandle: "extremely-long-handle.example.com", + }); + const validated = issueCardSchema.parse(data); + await renderAndSave( + h(IssueCard, validated), + "issue-card-long-handle.png", + ); + }); }); describe("pull request cards", () => { @@ -134,4 +145,15 @@ describe("pull request cards", () => { "pull-request-card-long-title.png", ); }); + + test("renders pull request with long author handle (reactions hidden)", async () => { + const data = createPullRequestData(avatarDataUri, { + authorHandle: "extremely-long-handle.example.com", + }); + const validated = pullRequestCardSchema.parse(data); + await renderAndSave( + h(PullRequestCard, validated), + "pull-request-card-long-handle.png", + ); + }); }); diff --git a/ogre/src/components/cards/issue.tsx b/ogre/src/components/cards/issue.tsx index 15bba594..e6d160f1 100644 --- a/ogre/src/components/cards/issue.tsx +++ b/ogre/src/components/cards/issue.tsx @@ -42,6 +42,8 @@ export function IssueCard(data: IssueCardData) { }}> diff --git a/ogre/src/components/cards/pull-request.tsx b/ogre/src/components/cards/pull-request.tsx index a723ec4a..a8a7dda2 100644 --- a/ogre/src/components/cards/pull-request.tsx +++ b/ogre/src/components/cards/pull-request.tsx @@ -127,6 +127,8 @@ export function PullRequestCard(data: PullRequestCardData) { }}> diff --git a/ogre/src/components/shared/footer-stats.tsx b/ogre/src/components/shared/footer-stats.tsx index 0f03aa8e..d7bb4978 100644 --- a/ogre/src/components/shared/footer-stats.tsx +++ b/ogre/src/components/shared/footer-stats.tsx @@ -1,15 +1,27 @@ import { Row } from "./layout"; import { Calendar, MessageSquare, SmilePlus } from "../../icons/lucide"; import { StatItem } from "./stat-item"; +import { Avatar } from "./avatar"; +import { TYPOGRAPHY } from "./constants"; + +// Handles longer than this cause the footer to overflow when combined with +// other stats, so we drop the less-important reaction count first, then the +// comment count once the handle grows longer still. +const LONG_HANDLE_THRESHOLD = 20; +const VERY_LONG_HANDLE_THRESHOLD = 28; interface FooterStatsProps { createdAt: string; + authorHandle?: string; + authorAvatarUrl?: string; reactionCount?: number; commentCount?: number; } export function FooterStats({ createdAt, + authorHandle, + authorAvatarUrl, reactionCount, commentCount, }: FooterStatsProps) { @@ -19,13 +31,38 @@ export function FooterStats({ year: "numeric", }).format(new Date(createdAt)); + const handleLength = authorHandle?.length ?? 0; + // Long handles crowd the footer. Drop reactions first; drop comments too + // for extremely long handles to prevent overflow past the tangled logo. + const isLongHandle = handleLength > LONG_HANDLE_THRESHOLD; + const isVeryLongHandle = handleLength > VERY_LONG_HANDLE_THRESHOLD; + const gap = isLongHandle ? 40 : 64; + const hideReactions = isLongHandle; + const hideComments = isVeryLongHandle; + return ( - + + {authorHandle && authorAvatarUrl ? ( + + + + {authorHandle} + + + ) : null} - {reactionCount ? ( + {reactionCount && !hideReactions ? ( ) : null} - {commentCount ? ( + {commentCount && !hideComments ? ( ) : null} diff --git a/ogre/src/validation.ts b/ogre/src/validation.ts index 01f67f4e..34b6b074 100644 --- a/ogre/src/validation.ts +++ b/ogre/src/validation.ts @@ -23,7 +23,9 @@ export const issueCardSchema = z.object({ type: z.literal("issue"), repoName: z.string().min(1).max(100), ownerHandle: z.string().min(1).max(100), + authorHandle: z.string().min(1).max(100), avatarUrl: z.string().url(), + authorAvatarUrl: z.string().url(), title: z.string().min(1).max(500), issueNumber: z.number().int().positive(), status: z.enum(["open", "closed"]), @@ -44,7 +46,9 @@ export const pullRequestCardSchema = z.object({ type: z.literal("pullRequest"), repoName: z.string().min(1).max(100), ownerHandle: z.string().min(1).max(100), + authorHandle: z.string().min(1).max(100), avatarUrl: z.string().url(), + authorAvatarUrl: z.string().url(), title: z.string().min(1).max(500), pullRequestNumber: z.number().int().positive(), status: z.enum(["open", "closed", "merged"]), -- 2.51.2