Main coves client
1// Post data models for Coves timeline feed
2//
3// These models match the backend response structure from:
4// /xrpc/social.coves.feed.getTimeline
5// /xrpc/social.coves.feed.getDiscover
6
7class TimelineResponse {
8 TimelineResponse({required this.feed, this.cursor});
9
10 factory TimelineResponse.fromJson(Map<String, dynamic> json) {
11 // Handle null feed array from backend
12 final feedData = json['feed'];
13 final List<FeedViewPost> feedList;
14
15 if (feedData == null) {
16 // Backend returned null, use empty list
17 feedList = [];
18 } else {
19 // Parse feed items
20 feedList =
21 (feedData as List<dynamic>)
22 .map(
23 (item) => FeedViewPost.fromJson(item as Map<String, dynamic>),
24 )
25 .toList();
26 }
27
28 return TimelineResponse(feed: feedList, cursor: json['cursor'] as String?);
29 }
30 final List<FeedViewPost> feed;
31 final String? cursor;
32}
33
34class FeedViewPost {
35 FeedViewPost({required this.post, this.reason});
36
37 factory FeedViewPost.fromJson(Map<String, dynamic> json) {
38 return FeedViewPost(
39 post: PostView.fromJson(json['post'] as Map<String, dynamic>),
40 reason:
41 json['reason'] != null
42 ? FeedReason.fromJson(json['reason'] as Map<String, dynamic>)
43 : null,
44 );
45 }
46 final PostView post;
47 final FeedReason? reason;
48}
49
50class PostView {
51 PostView({
52 required this.uri,
53 required this.cid,
54 required this.rkey,
55 required this.author,
56 required this.community,
57 required this.createdAt,
58 required this.indexedAt,
59 required this.text,
60 this.title,
61 required this.stats,
62 this.embed,
63 this.facets,
64 });
65
66 factory PostView.fromJson(Map<String, dynamic> json) {
67 return PostView(
68 uri: json['uri'] as String,
69 cid: json['cid'] as String,
70 rkey: json['rkey'] as String,
71 author: AuthorView.fromJson(json['author'] as Map<String, dynamic>),
72 community: CommunityRef.fromJson(
73 json['community'] as Map<String, dynamic>,
74 ),
75 createdAt: DateTime.parse(json['createdAt'] as String),
76 indexedAt: DateTime.parse(json['indexedAt'] as String),
77 text: json['text'] as String,
78 title: json['title'] as String?,
79 stats: PostStats.fromJson(json['stats'] as Map<String, dynamic>),
80 embed:
81 json['embed'] != null
82 ? PostEmbed.fromJson(json['embed'] as Map<String, dynamic>)
83 : null,
84 facets:
85 json['facets'] != null
86 ? (json['facets'] as List<dynamic>)
87 .map((f) => PostFacet.fromJson(f as Map<String, dynamic>))
88 .toList()
89 : null,
90 );
91 }
92 final String uri;
93 final String cid;
94 final String rkey;
95 final AuthorView author;
96 final CommunityRef community;
97 final DateTime createdAt;
98 final DateTime indexedAt;
99 final String text;
100 final String? title;
101 final PostStats stats;
102 final PostEmbed? embed;
103 final List<PostFacet>? facets;
104}
105
106class AuthorView {
107 AuthorView({
108 required this.did,
109 required this.handle,
110 this.displayName,
111 this.avatar,
112 });
113
114 factory AuthorView.fromJson(Map<String, dynamic> json) {
115 return AuthorView(
116 did: json['did'] as String,
117 handle: json['handle'] as String,
118 displayName: json['displayName'] as String?,
119 avatar: json['avatar'] as String?,
120 );
121 }
122 final String did;
123 final String handle;
124 final String? displayName;
125 final String? avatar;
126}
127
128class CommunityRef {
129 CommunityRef({
130 required this.did,
131 required this.name,
132 this.handle,
133 this.avatar,
134 });
135
136 factory CommunityRef.fromJson(Map<String, dynamic> json) {
137 return CommunityRef(
138 did: json['did'] as String,
139 name: json['name'] as String,
140 handle: json['handle'] as String?,
141 avatar: json['avatar'] as String?,
142 );
143 }
144 final String did;
145 final String name;
146 final String? handle;
147 final String? avatar;
148}
149
150class PostStats {
151 PostStats({
152 required this.upvotes,
153 required this.downvotes,
154 required this.score,
155 required this.commentCount,
156 });
157
158 factory PostStats.fromJson(Map<String, dynamic> json) {
159 return PostStats(
160 upvotes: json['upvotes'] as int,
161 downvotes: json['downvotes'] as int,
162 score: json['score'] as int,
163 commentCount: json['commentCount'] as int,
164 );
165 }
166 final int upvotes;
167 final int downvotes;
168 final int score;
169 final int commentCount;
170}
171
172class PostEmbed {
173 PostEmbed({required this.type, this.external, required this.data});
174
175 factory PostEmbed.fromJson(Map<String, dynamic> json) {
176 final embedType = json[r'$type'] as String? ?? 'unknown';
177 ExternalEmbed? externalEmbed;
178
179 if (embedType == 'social.coves.embed.external' &&
180 json['external'] != null) {
181 externalEmbed = ExternalEmbed.fromJson(
182 json['external'] as Map<String, dynamic>,
183 );
184 }
185
186 return PostEmbed(type: embedType, external: externalEmbed, data: json);
187 }
188 final String type;
189 final ExternalEmbed? external;
190 final Map<String, dynamic> data;
191}
192
193class ExternalEmbed {
194 ExternalEmbed({
195 required this.uri,
196 this.title,
197 this.description,
198 this.thumb,
199 this.domain,
200 this.embedType,
201 this.provider,
202 this.images,
203 this.totalCount,
204 });
205
206 factory ExternalEmbed.fromJson(Map<String, dynamic> json) {
207 // Thumb is always a string URL (backend transforms blob refs
208 // before sending)
209
210 // Handle images array if present
211 List<Map<String, dynamic>>? imagesList;
212 if (json['images'] != null && json['images'] is List) {
213 imagesList =
214 (json['images'] as List).whereType<Map<String, dynamic>>().toList();
215 }
216
217 return ExternalEmbed(
218 uri: json['uri'] as String,
219 title: json['title'] as String?,
220 description: json['description'] as String?,
221 thumb: json['thumb'] as String?,
222 domain: json['domain'] as String?,
223 embedType: json['embedType'] as String?,
224 provider: json['provider'] as String?,
225 images: imagesList,
226 totalCount: json['totalCount'] as int?,
227 );
228 }
229 final String uri;
230 final String? title;
231 final String? description;
232 final String? thumb;
233 final String? domain;
234 final String? embedType;
235 final String? provider;
236 final List<Map<String, dynamic>>? images;
237 final int? totalCount;
238}
239
240class PostFacet {
241 PostFacet({required this.data});
242
243 factory PostFacet.fromJson(Map<String, dynamic> json) {
244 return PostFacet(data: json);
245 }
246 final Map<String, dynamic> data;
247}
248
249class FeedReason {
250 FeedReason({required this.type, required this.data});
251
252 factory FeedReason.fromJson(Map<String, dynamic> json) {
253 return FeedReason(type: json[r'$type'] as String? ?? 'unknown', data: json);
254 }
255 final String type;
256 final Map<String, dynamic> data;
257}