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 8f19aba79..377efe941 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 f99a8d992..5843c6083 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 b6ea71283..c55e46afc 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 dce1f6755..eae8a2b41 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 4213abcbf..19bfa699f 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 15bba5948..e6d160f10 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 a723ec4af..a8a7dda2c 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 0f03aa8e4..d7bb49788 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 01f67f4e3..34b6b074d 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