@@ -38,6 +38,13 @@ export type PostData = {
3838 /** Hex of the quoted post's EventKey — same encoding as `PostData.id`. */
3939 quoteId ?: string ;
4040
41+ /** Identity that reposted this post, when this item represents a
42+ * repost. The rest of the fields are the *reposted* post's data. */
43+ repostedBy ?: string ;
44+ /** Hex of the repost event's own EventKey — used as the feed list
45+ * key so a repost is distinct from the original post. */
46+ repostId ?: string ;
47+
4148 signedEvent : v2 . SignedEvent ;
4249} ;
4350
@@ -102,6 +109,71 @@ export function decodePostBundle(bundle: v2.EventBundle): PostData | null {
102109 }
103110}
104111
112+ /** A repost event decoded into who reposted, the target post's id, and
113+ * the repost event's own id. `null` if the bundle isn't a repost. */
114+ function decodeRepostBundle ( bundle : v2 . EventBundle ) : {
115+ repostedBy : string ;
116+ targetId : string ;
117+ repostId : string ;
118+ } | null {
119+ try {
120+ if ( ! bundle . signedEvent ) return null ;
121+ const event = v2 . Event . fromBinary ( bundle . signedEvent . eventBytes ) ;
122+ const key = event . key ;
123+ if ( ! key ?. signedBy ?. key ) return null ;
124+ if ( ! bundle . serializedContent ?. contentBytes ) return null ;
125+ const content = v2 . Content . fromBinary (
126+ bundle . serializedContent . contentBytes ,
127+ ) ;
128+ if ( content . contentBody . oneofKind !== 'repost' ) return null ;
129+ const target = content . contentBody . repost . post ;
130+ if ( ! target ) return null ;
131+ return {
132+ repostedBy : key . identity ,
133+ targetId : bytesToHex ( v2 . EventKey . toBinary ( target ) ) ,
134+ repostId : bytesToHex ( v2 . EventKey . toBinary ( key ) ) ,
135+ } ;
136+ } catch {
137+ return null ;
138+ }
139+ }
140+
141+ /**
142+ * Decode a `GetFeedResponse` into renderable posts. Plain posts decode
143+ * directly; reposts resolve their target post from `event_hints` (the
144+ * server ships the reposted post alongside) and surface it tagged with
145+ * `repostedBy`. A repost whose target isn't in the hints is dropped.
146+ */
147+ export function decodeFeedItems ( response : v2 . GetFeedResponse ) : PostData [ ] {
148+ const hintPosts = new Map < string , PostData > ( ) ;
149+ for ( const hint of response . eventHints ) {
150+ if ( ! hint . eventBundle ) continue ;
151+ const post = decodePostBundle ( hint . eventBundle ) ;
152+ if ( post ) hintPosts . set ( post . id , post ) ;
153+ }
154+
155+ const items : PostData [ ] = [ ] ;
156+ for ( const bundle of response . eventBundles ) {
157+ const post = decodePostBundle ( bundle ) ;
158+ if ( post ) {
159+ items . push ( post ) ;
160+ continue ;
161+ }
162+ const repost = decodeRepostBundle ( bundle ) ;
163+ if ( repost ) {
164+ const target = hintPosts . get ( repost . targetId ) ;
165+ if ( target ) {
166+ items . push ( {
167+ ...target ,
168+ repostedBy : repost . repostedBy ,
169+ repostId : repost . repostId ,
170+ } ) ;
171+ }
172+ }
173+ }
174+ return items ;
175+ }
176+
105177/**
106178 * Dicebear identicon URL for a public key.
107179 */
0 commit comments