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}