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}