From bfbc936c865482d3badf43e5f7533ecaa62e1787 Mon Sep 17 00:00:00 2001 From: eti Date: Tue, 21 Apr 2026 10:24:57 +0000 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 --- ogre/client.go | 26 +++++++++++++++----------- appview/issues/opengraph.go | 26 +++++++++++++++----------- appview/pulls/opengraph.go | 4 ++++ ogre/src/validation.ts | 4 ++++ 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 ++++++++++++++++++++++++++++++++++++++++--- 9 file(s) changed, 108 insertion(s)(+), 25 deletion(s)(-) diff --git a/ogre/client.go b/ogre/client.go --- a/ogre/client.go +++ b/ogre/client.go @@ -47,24 +47,28 @@ } 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/appview/issues/opengraph.go b/appview/issues/opengraph.go --- a/appview/issues/opengraph.go +++ b/appview/issues/opengraph.go @@ -68,8 +68,10 @@ } 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 @@ 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 --- a/appview/pulls/opengraph.go +++ b/appview/pulls/opengraph.go @@ -26,8 +26,10 @@ } 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 @@ 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/src/validation.ts b/ogre/src/validation.ts --- a/ogre/src/validation.ts +++ b/ogre/src/validation.ts @@ -23,7 +23,9 @@ 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 @@ 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"]), diff --git a/ogre/src/__tests__/fixtures.ts b/ogre/src/__tests__/fixtures.ts --- a/ogre/src/__tests__/fixtures.ts +++ b/ogre/src/__tests__/fixtures.ts @@ -32,7 +32,9 @@ 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 @@ 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 --- a/ogre/src/__tests__/render.test.ts +++ b/ogre/src/__tests__/render.test.ts @@ -86,6 +86,17 @@ 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", () => { @@ -132,6 +143,17 @@ await renderAndSave( h(PullRequestCard, validated), "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 --- a/ogre/src/components/cards/issue.tsx +++ b/ogre/src/components/cards/issue.tsx @@ -42,6 +42,8 @@ }}> diff --git a/ogre/src/components/cards/pull-request.tsx b/ogre/src/components/cards/pull-request.tsx --- a/ogre/src/components/cards/pull-request.tsx +++ b/ogre/src/components/cards/pull-request.tsx @@ -127,6 +127,8 @@ }}> diff --git a/ogre/src/components/shared/footer-stats.tsx b/ogre/src/components/shared/footer-stats.tsx --- 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 @@ 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} -- tangled.sh