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 ViewerState {
51 ViewerState({
52 this.vote,
53 this.voteUri,
54 this.saved = false,
55 this.savedUri,
56 this.tags,
57 });
58
59 factory ViewerState.fromJson(Map<String, dynamic> json) {
60 return ViewerState(
61 vote: json['vote'] as String?,
62 voteUri: json['voteUri'] as String?,
63 saved: json['saved'] as bool? ?? false,
64 savedUri: json['savedUri'] as String?,
65 tags: (json['tags'] as List<dynamic>?)?.cast<String>(),
66 );
67 }
68
69 /// Vote direction: "up", "down", or null if not voted
70 final String? vote;
71
72 /// AT-URI of the vote record
73 final String? voteUri;
74
75 /// Whether the post is saved/bookmarked
76 final bool saved;
77
78 /// AT-URI of the saved record
79 final String? savedUri;
80
81 /// User-applied tags
82 final List<String>? tags;
83}
84
85class PostView {
86 PostView({
87 required this.uri,
88 required this.cid,
89 required this.rkey,
90 required this.author,
91 required this.community,
92 required this.createdAt,
93 required this.indexedAt,
94 required this.text,
95 this.title,
96 required this.stats,
97 this.embed,
98 this.facets,
99 this.viewer,
100 });
101
102 factory PostView.fromJson(Map<String, dynamic> json) {
103 return PostView(
104 uri: json['uri'] as String,
105 cid: json['cid'] as String,
106 rkey: json['rkey'] as String,
107 author: AuthorView.fromJson(json['author'] as Map<String, dynamic>),
108 community: CommunityRef.fromJson(
109 json['community'] as Map<String, dynamic>,
110 ),
111 createdAt: DateTime.parse(json['createdAt'] as String),
112 indexedAt: DateTime.parse(json['indexedAt'] as String),
113 text: json['text'] as String,
114 title: json['title'] as String?,
115 stats: PostStats.fromJson(json['stats'] as Map<String, dynamic>),
116 embed:
117 json['embed'] != null
118 ? PostEmbed.fromJson(json['embed'] as Map<String, dynamic>)
119 : null,
120 facets:
121 json['facets'] != null
122 ? (json['facets'] as List<dynamic>)
123 .map((f) => PostFacet.fromJson(f as Map<String, dynamic>))
124 .toList()
125 : null,
126 viewer:
127 json['viewer'] != null
128 ? ViewerState.fromJson(json['viewer'] as Map<String, dynamic>)
129 : null,
130 );
131 }
132 final String uri;
133 final String cid;
134 final String rkey;
135 final AuthorView author;
136 final CommunityRef community;
137 final DateTime createdAt;
138 final DateTime indexedAt;
139 final String text;
140 final String? title;
141 final PostStats stats;
142 final PostEmbed? embed;
143 final List<PostFacet>? facets;
144 final ViewerState? viewer;
145}
146
147class AuthorView {
148 AuthorView({
149 required this.did,
150 required this.handle,
151 this.displayName,
152 this.avatar,
153 });
154
155 factory AuthorView.fromJson(Map<String, dynamic> json) {
156 return AuthorView(
157 did: json['did'] as String,
158 handle: json['handle'] as String,
159 displayName: json['displayName'] as String?,
160 avatar: json['avatar'] as String?,
161 );
162 }
163 final String did;
164 final String handle;
165 final String? displayName;
166 final String? avatar;
167}
168
169class CommunityRef {
170 CommunityRef({
171 required this.did,
172 required this.name,
173 this.handle,
174 this.avatar,
175 });
176
177 factory CommunityRef.fromJson(Map<String, dynamic> json) {
178 return CommunityRef(
179 did: json['did'] as String,
180 name: json['name'] as String,
181 handle: json['handle'] as String?,
182 avatar: json['avatar'] as String?,
183 );
184 }
185 final String did;
186 final String name;
187 final String? handle;
188 final String? avatar;
189}
190
191class PostStats {
192 PostStats({
193 required this.upvotes,
194 required this.downvotes,
195 required this.score,
196 required this.commentCount,
197 });
198
199 factory PostStats.fromJson(Map<String, dynamic> json) {
200 return PostStats(
201 upvotes: json['upvotes'] as int,
202 downvotes: json['downvotes'] as int,
203 score: json['score'] as int,
204 commentCount: json['commentCount'] as int,
205 );
206 }
207 final int upvotes;
208 final int downvotes;
209 final int score;
210 final int commentCount;
211}
212
213class PostEmbed {
214 PostEmbed({required this.type, this.external, required this.data});
215
216 factory PostEmbed.fromJson(Map<String, dynamic> json) {
217 final embedType = json[r'$type'] as String? ?? 'unknown';
218 ExternalEmbed? externalEmbed;
219
220 if (embedType == 'social.coves.embed.external' &&
221 json['external'] != null) {
222 externalEmbed = ExternalEmbed.fromJson(
223 json['external'] as Map<String, dynamic>,
224 );
225 }
226
227 return PostEmbed(type: embedType, external: externalEmbed, data: json);
228 }
229 final String type;
230 final ExternalEmbed? external;
231 final Map<String, dynamic> data;
232}
233
234class ExternalEmbed {
235 ExternalEmbed({
236 required this.uri,
237 this.title,
238 this.description,
239 this.thumb,
240 this.domain,
241 this.embedType,
242 this.provider,
243 this.images,
244 this.totalCount,
245 });
246
247 factory ExternalEmbed.fromJson(Map<String, dynamic> json) {
248 // Thumb is always a string URL (backend transforms blob refs
249 // before sending)
250
251 // Handle images array if present
252 List<Map<String, dynamic>>? imagesList;
253 if (json['images'] != null && json['images'] is List) {
254 imagesList =
255 (json['images'] as List).whereType<Map<String, dynamic>>().toList();
256 }
257
258 return ExternalEmbed(
259 uri: json['uri'] as String,
260 title: json['title'] as String?,
261 description: json['description'] as String?,
262 thumb: json['thumb'] as String?,
263 domain: json['domain'] as String?,
264 embedType: json['embedType'] as String?,
265 provider: json['provider'] as String?,
266 images: imagesList,
267 totalCount: json['totalCount'] as int?,
268 );
269 }
270 final String uri;
271 final String? title;
272 final String? description;
273 final String? thumb;
274 final String? domain;
275 final String? embedType;
276 final String? provider;
277 final List<Map<String, dynamic>>? images;
278 final int? totalCount;
279}
280
281class PostFacet {
282 PostFacet({required this.data});
283
284 factory PostFacet.fromJson(Map<String, dynamic> json) {
285 return PostFacet(data: json);
286 }
287 final Map<String, dynamic> data;
288}
289
290class FeedReason {
291 FeedReason({required this.type, required this.data});
292
293 factory FeedReason.fromJson(Map<String, dynamic> json) {
294 return FeedReason(type: json[r'$type'] as String? ?? 'unknown', data: json);
295 }
296 final String type;
297 final Map<String, dynamic> data;
298}