Monorepo for Tangled tangled.org
766
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

authored by

eti and committed by
Tangled
bfbc936c 1824bcbf

+108 -25
+15 -11
appview/issues/opengraph.go
··· 68 68 } 69 69 70 70 ownerHandle := rp.pages.DisplayHandle(r.Context(), f.Did) 71 + authorHandle := rp.pages.DisplayHandle(r.Context(), issue.Did) 71 72 72 73 avatarUrl := rp.pages.AvatarUrl(f.Did, "256") 74 + authorAvatarUrl := rp.pages.AvatarUrl(issue.Did, "256") 73 75 74 76 status := "closed" 75 77 if issue.Open { ··· 81 83 reactionCount, _ := db.GetReactionCount(rp.db, issue.AtUri()) 82 84 83 85 payload := ogre.IssueCardPayload{ 84 - Type: "issue", 85 - RepoName: f.Name, 86 - OwnerHandle: ownerHandle, 87 - AvatarUrl: avatarUrl, 88 - Title: issue.Title, 89 - IssueNumber: issue.IssueId, 90 - Status: status, 91 - Labels: labels, 92 - CommentCount: commentCount, 93 - ReactionCount: reactionCount, 94 - CreatedAt: issue.Created.Format(time.RFC3339), 86 + Type: "issue", 87 + RepoName: f.Name, 88 + OwnerHandle: ownerHandle, 89 + AuthorHandle: authorHandle, 90 + AvatarUrl: avatarUrl, 91 + AuthorAvatarUrl: authorAvatarUrl, 92 + Title: issue.Title, 93 + IssueNumber: issue.IssueId, 94 + Status: status, 95 + Labels: labels, 96 + CommentCount: commentCount, 97 + ReactionCount: reactionCount, 98 + CreatedAt: issue.Created.Format(time.RFC3339), 95 99 } 96 100 97 101 imageBytes, err := rp.ogreClient.RenderIssueCard(r.Context(), payload)
+4
appview/pulls/opengraph.go
··· 26 26 } 27 27 28 28 ownerHandle := s.pages.DisplayHandle(r.Context(), f.Did) 29 + authorHandle := s.pages.DisplayHandle(r.Context(), pull.OwnerDid) 29 30 30 31 avatarUrl := s.pages.AvatarUrl(f.Did, "256") 32 + authorAvatarUrl := s.pages.AvatarUrl(pull.OwnerDid, "256") 31 33 32 34 var status string 33 35 if pull.State.IsOpen() { ··· 60 62 Type: "pullRequest", 61 63 RepoName: f.Name, 62 64 OwnerHandle: ownerHandle, 65 + AuthorHandle: authorHandle, 63 66 AvatarUrl: avatarUrl, 67 + AuthorAvatarUrl: authorAvatarUrl, 64 68 Title: pull.Title, 65 69 PullRequestNumber: pull.PullId, 66 70 Status: status,
+15 -11
ogre/client.go
··· 47 47 } 48 48 49 49 type IssueCardPayload struct { 50 - Type string `json:"type"` 51 - RepoName string `json:"repoName"` 52 - OwnerHandle string `json:"ownerHandle"` 53 - AvatarUrl string `json:"avatarUrl"` 54 - Title string `json:"title"` 55 - IssueNumber int `json:"issueNumber"` 56 - Status string `json:"status"` 57 - Labels []LabelData `json:"labels"` 58 - CommentCount int `json:"commentCount"` 59 - ReactionCount int `json:"reactionCount"` 60 - CreatedAt string `json:"createdAt"` 50 + Type string `json:"type"` 51 + RepoName string `json:"repoName"` 52 + OwnerHandle string `json:"ownerHandle"` 53 + AuthorHandle string `json:"authorHandle"` 54 + AvatarUrl string `json:"avatarUrl"` 55 + AuthorAvatarUrl string `json:"authorAvatarUrl"` 56 + Title string `json:"title"` 57 + IssueNumber int `json:"issueNumber"` 58 + Status string `json:"status"` 59 + Labels []LabelData `json:"labels"` 60 + CommentCount int `json:"commentCount"` 61 + ReactionCount int `json:"reactionCount"` 62 + CreatedAt string `json:"createdAt"` 61 63 } 62 64 63 65 type PullRequestCardPayload struct { 64 66 Type string `json:"type"` 65 67 RepoName string `json:"repoName"` 66 68 OwnerHandle string `json:"ownerHandle"` 69 + AuthorHandle string `json:"authorHandle"` 67 70 AvatarUrl string `json:"avatarUrl"` 71 + AuthorAvatarUrl string `json:"authorAvatarUrl"` 68 72 Title string `json:"title"` 69 73 PullRequestNumber int `json:"pullRequestNumber"` 70 74 Status string `json:"status"`
+4
ogre/src/__tests__/fixtures.ts
··· 32 32 type: "issue", 33 33 repoName: "core", 34 34 ownerHandle: "tangled.org", 35 + authorHandle: "oppi.li", 35 36 avatarUrl, 37 + authorAvatarUrl: avatarUrl, 36 38 title: "feature request: sync fork button", 37 39 issueNumber: 8, 38 40 status: "open", ··· 54 56 type: "pullRequest", 55 57 repoName: "core", 56 58 ownerHandle: "tangled.org", 59 + authorHandle: "oppi.li", 57 60 avatarUrl, 61 + authorAvatarUrl: avatarUrl, 58 62 title: "add author description to README.md", 59 63 pullRequestNumber: 1, 60 64 status: "open",
+22
ogre/src/__tests__/render.test.ts
··· 86 86 const validated = issueCardSchema.parse(data); 87 87 await renderAndSave(h(IssueCard, validated), "issue-card-long-title.png"); 88 88 }); 89 + 90 + test("renders issue with long author handle (reactions hidden)", async () => { 91 + const data = createIssueData(avatarDataUri, { 92 + authorHandle: "extremely-long-handle.example.com", 93 + }); 94 + const validated = issueCardSchema.parse(data); 95 + await renderAndSave( 96 + h(IssueCard, validated), 97 + "issue-card-long-handle.png", 98 + ); 99 + }); 89 100 }); 90 101 91 102 describe("pull request cards", () => { ··· 132 143 await renderAndSave( 133 144 h(PullRequestCard, validated), 134 145 "pull-request-card-long-title.png", 146 + ); 147 + }); 148 + 149 + test("renders pull request with long author handle (reactions hidden)", async () => { 150 + const data = createPullRequestData(avatarDataUri, { 151 + authorHandle: "extremely-long-handle.example.com", 152 + }); 153 + const validated = pullRequestCardSchema.parse(data); 154 + await renderAndSave( 155 + h(PullRequestCard, validated), 156 + "pull-request-card-long-handle.png", 135 157 ); 136 158 }); 137 159 });
+2
ogre/src/components/cards/issue.tsx
··· 42 42 }}> 43 43 <FooterStats 44 44 createdAt={data.createdAt} 45 + authorHandle={data.authorHandle} 46 + authorAvatarUrl={data.authorAvatarUrl} 45 47 reactionCount={data.reactionCount} 46 48 commentCount={data.commentCount} 47 49 />
+2
ogre/src/components/cards/pull-request.tsx
··· 127 127 }}> 128 128 <FooterStats 129 129 createdAt={data.createdAt} 130 + authorHandle={data.authorHandle} 131 + authorAvatarUrl={data.authorAvatarUrl} 130 132 reactionCount={data.reactionCount} 131 133 commentCount={data.commentCount} 132 134 />
+40 -3
ogre/src/components/shared/footer-stats.tsx
··· 1 1 import { Row } from "./layout"; 2 2 import { Calendar, MessageSquare, SmilePlus } from "../../icons/lucide"; 3 3 import { StatItem } from "./stat-item"; 4 + import { Avatar } from "./avatar"; 5 + import { TYPOGRAPHY } from "./constants"; 6 + 7 + // Handles longer than this cause the footer to overflow when combined with 8 + // other stats, so we drop the less-important reaction count first, then the 9 + // comment count once the handle grows longer still. 10 + const LONG_HANDLE_THRESHOLD = 20; 11 + const VERY_LONG_HANDLE_THRESHOLD = 28; 4 12 5 13 interface FooterStatsProps { 6 14 createdAt: string; 15 + authorHandle?: string; 16 + authorAvatarUrl?: string; 7 17 reactionCount?: number; 8 18 commentCount?: number; 9 19 } 10 20 11 21 export function FooterStats({ 12 22 createdAt, 23 + authorHandle, 24 + authorAvatarUrl, 13 25 reactionCount, 14 26 commentCount, 15 27 }: FooterStatsProps) { ··· 19 31 year: "numeric", 20 32 }).format(new Date(createdAt)); 21 33 34 + const handleLength = authorHandle?.length ?? 0; 35 + // Long handles crowd the footer. Drop reactions first; drop comments too 36 + // for extremely long handles to prevent overflow past the tangled logo. 37 + const isLongHandle = handleLength > LONG_HANDLE_THRESHOLD; 38 + const isVeryLongHandle = handleLength > VERY_LONG_HANDLE_THRESHOLD; 39 + const gap = isLongHandle ? 40 : 64; 40 + const hideReactions = isLongHandle; 41 + const hideComments = isVeryLongHandle; 42 + 22 43 return ( 23 - <Row style={{ gap: 64 }}> 44 + <Row style={{ gap }}> 45 + {authorHandle && authorAvatarUrl ? ( 46 + <Row style={{ gap: 16, alignItems: "center" }}> 47 + <Avatar src={authorAvatarUrl} size={40} /> 48 + <span 49 + style={{ 50 + ...TYPOGRAPHY.body, 51 + color: "#404040", 52 + maxWidth: 480, 53 + overflow: "hidden", 54 + textOverflow: "ellipsis", 55 + whiteSpace: "nowrap", 56 + }}> 57 + {authorHandle} 58 + </span> 59 + </Row> 60 + ) : null} 24 61 <StatItem Icon={Calendar} value={formattedDate} /> 25 - {reactionCount ? ( 62 + {reactionCount && !hideReactions ? ( 26 63 <StatItem Icon={SmilePlus} value={reactionCount} /> 27 64 ) : null} 28 - {commentCount ? ( 65 + {commentCount && !hideComments ? ( 29 66 <StatItem Icon={MessageSquare} value={commentCount} /> 30 67 ) : null} 31 68 </Row>
+4
ogre/src/validation.ts
··· 23 23 type: z.literal("issue"), 24 24 repoName: z.string().min(1).max(100), 25 25 ownerHandle: z.string().min(1).max(100), 26 + authorHandle: z.string().min(1).max(100), 26 27 avatarUrl: z.string().url(), 28 + authorAvatarUrl: z.string().url(), 27 29 title: z.string().min(1).max(500), 28 30 issueNumber: z.number().int().positive(), 29 31 status: z.enum(["open", "closed"]), ··· 44 46 type: z.literal("pullRequest"), 45 47 repoName: z.string().min(1).max(100), 46 48 ownerHandle: z.string().min(1).max(100), 49 + authorHandle: z.string().min(1).max(100), 47 50 avatarUrl: z.string().url(), 51 + authorAvatarUrl: z.string().url(), 48 52 title: z.string().min(1).max(500), 49 53 pullRequestNumber: z.number().int().positive(), 50 54 status: z.enum(["open", "closed", "merged"]),