···1package views
23import (
4+ "fmt"
5+6 "github.com/bluesky-social/indigo/api/bsky"
7 "github.com/bluesky-social/indigo/lex/util"
8 "github.com/whyrusleeping/konbini/hydration"
···42 }
43 }
4445+ // Add embed handling
46+ if post.Post.Embed != nil {
47+ view.Embed = formatEmbed(post.Post.Embed, post.Author)
48+ }
4950 return view
51}
···58}
5960// ThreadViewPost builds a thread view post (app.bsky.feed.defs#threadViewPost)
61+func ThreadViewPost(post *hydration.PostInfo, author *hydration.ActorInfo, parent, replies any) *bsky.FeedDefs_ThreadViewPost {
62 view := &bsky.FeedDefs_ThreadViewPost{
63 LexiconTypeID: "app.bsky.feed.defs#threadViewPost",
64 Post: PostView(post, author),
···6970 return view
71}
72+73+func formatEmbed(embed *bsky.FeedPost_Embed, authorDID string) *bsky.FeedDefs_PostView_Embed {
74+ if embed == nil {
75+ return nil
76+ }
77+78+ result := &bsky.FeedDefs_PostView_Embed{}
79+80+ // Handle images
81+ if embed.EmbedImages != nil {
82+ viewImages := make([]*bsky.EmbedImages_ViewImage, len(embed.EmbedImages.Images))
83+ for i, img := range embed.EmbedImages.Images {
84+ // Convert blob to CDN URLs
85+ fullsize := ""
86+ thumb := ""
87+ if img.Image != nil {
88+ // CDN URL format for feed images
89+ cid := img.Image.Ref.String()
90+ fullsize = fmt.Sprintf("https://cdn.bsky.app/img/feed_fullsize/plain/%s/%s@jpeg", authorDID, cid)
91+ thumb = fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s@jpeg", authorDID, cid)
92+ }
93+94+ viewImages[i] = &bsky.EmbedImages_ViewImage{
95+ Alt: img.Alt,
96+ AspectRatio: img.AspectRatio,
97+ Fullsize: fullsize,
98+ Thumb: thumb,
99+ }
100+ }
101+ result.EmbedImages_View = &bsky.EmbedImages_View{
102+ LexiconTypeID: "app.bsky.embed.images#view",
103+ Images: viewImages,
104+ }
105+ return result
106+ }
107+108+ // Handle external links
109+ if embed.EmbedExternal != nil && embed.EmbedExternal.External != nil {
110+ // Convert blob thumb to CDN URL if present
111+ var thumbURL *string
112+ if embed.EmbedExternal.External.Thumb != nil {
113+ // CDN URL for external link thumbnails
114+ cid := embed.EmbedExternal.External.Thumb.Ref.String()
115+ url := fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s@jpeg", authorDID, cid)
116+ thumbURL = &url
117+ }
118+119+ result.EmbedExternal_View = &bsky.EmbedExternal_View{
120+ LexiconTypeID: "app.bsky.embed.external#view",
121+ External: &bsky.EmbedExternal_ViewExternal{
122+ Uri: embed.EmbedExternal.External.Uri,
123+ Title: embed.EmbedExternal.External.Title,
124+ Description: embed.EmbedExternal.External.Description,
125+ Thumb: thumbURL,
126+ },
127+ }
128+ return result
129+ }
130+131+ // Handle video
132+ if embed.EmbedVideo != nil {
133+ // TODO: Implement video embed view
134+ // This would require converting video blob to CDN URLs and playlist URLs
135+ return nil
136+ }
137+138+ // Handle record (quote posts, etc.)
139+ if embed.EmbedRecord != nil {
140+ // TODO: Implement record embed view
141+ // This requires hydrating the embedded record, which is complex
142+ // For now, return nil to skip these embeds
143+ return nil
144+ }
145+146+ // Handle record with media (quote post with images/external)
147+ if embed.EmbedRecordWithMedia != nil {
148+ // TODO: Implement record with media embed view
149+ // This combines record hydration with media conversion
150+ return nil
151+ }
152+153+ return nil
154+}
155+156+// GeneratorView builds a feed generator view (app.bsky.feed.defs#generatorView)
157+func GeneratorView(uri, cid string, record *bsky.FeedGenerator, creator *hydration.ActorInfo, likeCount int64, viewerLike string, indexedAt string) *bsky.FeedDefs_GeneratorView {
158+ view := &bsky.FeedDefs_GeneratorView{
159+ LexiconTypeID: "app.bsky.feed.defs#generatorView",
160+ Uri: uri,
161+ Cid: cid,
162+ Did: record.Did,
163+ Creator: ProfileView(creator),
164+ DisplayName: record.DisplayName,
165+ Description: record.Description,
166+ IndexedAt: indexedAt,
167+ }
168+169+ // Add optional fields
170+ if record.Avatar != nil {
171+ avatarURL := fmt.Sprintf("https://cdn.bsky.app/img/avatar/plain/%s/%s@jpeg", creator.DID, record.Avatar.Ref.String())
172+ view.Avatar = &avatarURL
173+ }
174+175+ if record.DescriptionFacets != nil && len(record.DescriptionFacets) > 0 {
176+ view.DescriptionFacets = record.DescriptionFacets
177+ }
178+179+ if record.AcceptsInteractions != nil {
180+ view.AcceptsInteractions = record.AcceptsInteractions
181+ }
182+183+ if record.ContentMode != nil {
184+ view.ContentMode = record.ContentMode
185+ }
186+187+ // Add like count if present
188+ if likeCount > 0 {
189+ view.LikeCount = &likeCount
190+ }
191+192+ // Add viewer state if viewer has liked
193+ if viewerLike != "" {
194+ view.Viewer = &bsky.FeedDefs_GeneratorViewerState{
195+ Like: &viewerLike,
196+ }
197+ }
198+199+ return view
200+}
+2-15
xrpc/actor/getProfiles.go
···3import (
4 "net/http"
506 "github.com/labstack/echo/v4"
7 "github.com/whyrusleeping/konbini/hydration"
8 "github.com/whyrusleeping/konbini/views"
···28 ctx := c.Request().Context()
2930 // Resolve all actors to DIDs and hydrate profiles
31- profiles := make([]interface{}, 0)
32 for _, actor := range actors {
33 // Resolve actor to DID
34 did, err := hydrator.ResolveDID(ctx, actor)
···43 // Skip actors that can't be hydrated
44 continue
45 }
46-47- // Get counts for the profile
48- type counts struct {
49- Followers int
50- Follows int
51- Posts int
52- }
53- var c counts
54- db.Raw(`
55- SELECT
56- (SELECT COUNT(*) FROM follows WHERE subject = (SELECT id FROM repos WHERE did = ?)) as followers,
57- (SELECT COUNT(*) FROM follows WHERE author = (SELECT id FROM repos WHERE did = ?)) as follows,
58- (SELECT COUNT(*) FROM posts WHERE author = (SELECT id FROM repos WHERE did = ?)) as posts
59- `, did, did, did).Scan(&c)
6061 profiles = append(profiles, views.ProfileViewDetailed(actorInfo))
62 }
···3import (
4 "net/http"
56+ "github.com/bluesky-social/indigo/api/bsky"
7 "github.com/labstack/echo/v4"
8 "github.com/whyrusleeping/konbini/hydration"
9 "github.com/whyrusleeping/konbini/views"
···29 ctx := c.Request().Context()
3031 // Resolve all actors to DIDs and hydrate profiles
32+ profiles := make([]*bsky.ActorDefs_ProfileViewDetailed, 0, len(actors))
33 for _, actor := range actors {
34 // Resolve actor to DID
35 did, err := hydrator.ResolveDID(ctx, actor)
···44 // Skip actors that can't be hydrated
45 continue
46 }
000000000000004748 profiles = append(profiles, views.ProfileViewDetailed(actorInfo))
49 }