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({required this.did, required this.name, this.avatar});
130
131 factory CommunityRef.fromJson(Map<String, dynamic> json) {
132 return CommunityRef(
133 did: json['did'] as String,
134 name: json['name'] as String,
135 avatar: json['avatar'] as String?,
136 );
137 }
138 final String did;
139 final String name;
140 final String? avatar;
141}
142
143class PostStats {
144 PostStats({
145 required this.upvotes,
146 required this.downvotes,
147 required this.score,
148 required this.commentCount,
149 });
150
151 factory PostStats.fromJson(Map<String, dynamic> json) {
152 return PostStats(
153 upvotes: json['upvotes'] as int,
154 downvotes: json['downvotes'] as int,
155 score: json['score'] as int,
156 commentCount: json['commentCount'] as int,
157 );
158 }
159 final int upvotes;
160 final int downvotes;
161 final int score;
162 final int commentCount;
163}
164
165class PostEmbed {
166 PostEmbed({required this.type, this.external, required this.data});
167
168 factory PostEmbed.fromJson(Map<String, dynamic> json) {
169 final embedType = json[r'$type'] as String? ?? 'unknown';
170 ExternalEmbed? externalEmbed;
171
172 if (embedType == 'social.coves.embed.external' &&
173 json['external'] != null) {
174 externalEmbed = ExternalEmbed.fromJson(
175 json['external'] as Map<String, dynamic>,
176 );
177 }
178
179 return PostEmbed(type: embedType, external: externalEmbed, data: json);
180 }
181 final String type;
182 final ExternalEmbed? external;
183 final Map<String, dynamic> data;
184}
185
186class ExternalEmbed {
187 ExternalEmbed({
188 required this.uri,
189 this.title,
190 this.description,
191 this.thumb,
192 this.domain,
193 });
194
195 factory ExternalEmbed.fromJson(Map<String, dynamic> json) {
196 return ExternalEmbed(
197 uri: json['uri'] as String,
198 title: json['title'] as String?,
199 description: json['description'] as String?,
200 thumb: json['thumb'] as String?,
201 domain: json['domain'] as String?,
202 );
203 }
204 final String uri;
205 final String? title;
206 final String? description;
207 final String? thumb;
208 final String? domain;
209}
210
211class PostFacet {
212 PostFacet({required this.data});
213
214 factory PostFacet.fromJson(Map<String, dynamic> json) {
215 return PostFacet(data: json);
216 }
217 final Map<String, dynamic> data;
218}
219
220class FeedReason {
221 FeedReason({required this.type, required this.data});
222
223 factory FeedReason.fromJson(Map<String, dynamic> json) {
224 return FeedReason(type: json[r'$type'] as String? ?? 'unknown', data: json);
225 }
226 final String type;
227 final Map<String, dynamic> data;
228}