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}