Main coves client
1// Community data models for Coves
2//
3// These models match the backend API structure from:
4// GET /xrpc/social.coves.community.list
5// POST /xrpc/social.coves.community.post.create
6
7/// Response from GET /xrpc/social.coves.community.list
8class CommunitiesResponse {
9 CommunitiesResponse({required this.communities, this.cursor});
10
11 factory CommunitiesResponse.fromJson(Map<String, dynamic> json) {
12 // Handle null communities array from backend
13 final communitiesData = json['communities'];
14 final List<CommunityView> communitiesList;
15
16 if (communitiesData == null) {
17 // Backend returned null, use empty list
18 communitiesList = [];
19 } else {
20 // Parse community items
21 communitiesList = (communitiesData as List<dynamic>)
22 .map(
23 (item) => CommunityView.fromJson(item as Map<String, dynamic>),
24 )
25 .toList();
26 }
27
28 return CommunitiesResponse(
29 communities: communitiesList,
30 cursor: json['cursor'] as String?,
31 );
32 }
33
34 final List<CommunityView> communities;
35 final String? cursor;
36}
37
38/// Full community view data
39class CommunityView {
40 CommunityView({
41 required this.did,
42 required this.name,
43 this.handle,
44 this.displayName,
45 this.description,
46 this.avatar,
47 this.visibility,
48 this.subscriberCount,
49 this.memberCount,
50 this.postCount,
51 this.viewer,
52 });
53
54 factory CommunityView.fromJson(Map<String, dynamic> json) {
55 return CommunityView(
56 did: json['did'] as String,
57 name: json['name'] as String,
58 handle: json['handle'] as String?,
59 displayName: json['displayName'] as String?,
60 description: json['description'] as String?,
61 avatar: json['avatar'] as String?,
62 visibility: json['visibility'] as String?,
63 subscriberCount: json['subscriberCount'] as int?,
64 memberCount: json['memberCount'] as int?,
65 postCount: json['postCount'] as int?,
66 viewer: json['viewer'] != null
67 ? CommunityViewerState.fromJson(
68 json['viewer'] as Map<String, dynamic>,
69 )
70 : null,
71 );
72 }
73
74 /// Community DID (decentralized identifier)
75 final String did;
76
77 /// Community name (unique identifier)
78 final String name;
79
80 /// Community handle
81 final String? handle;
82
83 /// Display name for UI
84 final String? displayName;
85
86 /// Community description
87 final String? description;
88
89 /// Avatar URL
90 final String? avatar;
91
92 /// Visibility setting (e.g., "public", "private")
93 final String? visibility;
94
95 /// Number of subscribers
96 final int? subscriberCount;
97
98 /// Number of members
99 final int? memberCount;
100
101 /// Number of posts
102 final int? postCount;
103
104 /// Current user's relationship with this community
105 final CommunityViewerState? viewer;
106}
107
108/// Current user's relationship with a community
109class CommunityViewerState {
110 CommunityViewerState({this.subscribed, this.member});
111
112 factory CommunityViewerState.fromJson(Map<String, dynamic> json) {
113 return CommunityViewerState(
114 subscribed: json['subscribed'] as bool?,
115 member: json['member'] as bool?,
116 );
117 }
118
119 /// Whether the user is subscribed to this community
120 final bool? subscribed;
121
122 /// Whether the user is a member of this community
123 final bool? member;
124}
125
126/// Request body for POST /xrpc/social.coves.community.post.create
127class CreatePostRequest {
128 CreatePostRequest({
129 required this.community,
130 this.title,
131 this.content,
132 this.embed,
133 this.langs,
134 this.labels,
135 });
136
137 Map<String, dynamic> toJson() {
138 final json = <String, dynamic>{
139 'community': community,
140 };
141
142 if (title != null) {
143 json['title'] = title;
144 }
145 if (content != null) {
146 json['content'] = content;
147 }
148 if (embed != null) {
149 json['embed'] = embed!.toJson();
150 }
151 if (langs != null && langs!.isNotEmpty) {
152 json['langs'] = langs;
153 }
154 if (labels != null) {
155 json['labels'] = labels!.toJson();
156 }
157
158 return json;
159 }
160
161 /// Community DID or handle
162 final String community;
163
164 /// Post title
165 final String? title;
166
167 /// Post content/text
168 final String? content;
169
170 /// External link embed
171 final ExternalEmbedInput? embed;
172
173 /// Language codes (e.g., ["en", "es"])
174 final List<String>? langs;
175
176 /// Self-applied content labels
177 final SelfLabels? labels;
178}
179
180/// Response from POST /xrpc/social.coves.community.post.create
181class CreatePostResponse {
182 const CreatePostResponse({required this.uri, required this.cid});
183
184 factory CreatePostResponse.fromJson(Map<String, dynamic> json) {
185 return CreatePostResponse(
186 uri: json['uri'] as String,
187 cid: json['cid'] as String,
188 );
189 }
190
191 /// AT-URI of the created post
192 final String uri;
193
194 /// Content identifier (CID) of the created post
195 final String cid;
196}
197
198/// External link embed input for creating posts
199class ExternalEmbedInput {
200 const ExternalEmbedInput({
201 required this.uri,
202 this.title,
203 this.description,
204 this.thumb,
205 });
206
207 Map<String, dynamic> toJson() {
208 final json = <String, dynamic>{
209 'uri': uri,
210 };
211
212 if (title != null) {
213 json['title'] = title;
214 }
215 if (description != null) {
216 json['description'] = description;
217 }
218 if (thumb != null) {
219 json['thumb'] = thumb;
220 }
221
222 return json;
223 }
224
225 /// URL of the external link
226 final String uri;
227
228 /// Title of the linked content
229 final String? title;
230
231 /// Description of the linked content
232 final String? description;
233
234 /// Thumbnail URL
235 final String? thumb;
236}
237
238/// Self-applied content labels
239class SelfLabels {
240 const SelfLabels({required this.values});
241
242 Map<String, dynamic> toJson() {
243 return {
244 'values': values.map((label) => label.toJson()).toList(),
245 };
246 }
247
248 /// List of self-applied labels
249 final List<SelfLabel> values;
250}
251
252/// Individual self-applied label
253class SelfLabel {
254 const SelfLabel({required this.val});
255
256 Map<String, dynamic> toJson() {
257 return {
258 'val': val,
259 };
260 }
261
262 /// Label value (e.g., "nsfw", "spoiler")
263 final String val;
264}