mobile bluesky app made with flutter
lazurite.stormlightlabs.org/
mobile
bluesky
flutter
1import 'dart:typed_data';
2
3import 'package:atproto_core/atproto_core.dart' show AtUri, BlobRef;
4import 'package:bluesky/app_bsky_actor_defs.dart';
5import 'package:bluesky/app_bsky_feed_defs.dart';
6import 'package:bluesky/app_bsky_graph_defs.dart';
7import 'package:bluesky/app_bsky_graph_getlists.dart';
8import 'package:bluesky/app_bsky_graph_getlistswithmembership.dart';
9import 'package:lazurite/core/network/app_view_request_context.dart';
10import 'package:lazurite/features/moderation/data/moderation_service.dart';
11
12class ListRepository {
13 ListRepository({
14 required dynamic bluesky,
15 ModerationService? moderationService,
16 String? appViewProvider,
17 String Function()? appViewProviderResolver,
18 }) : _bluesky = bluesky,
19 _moderationService = moderationService,
20 _appViewContext = AppViewRequestContext(
21 appViewProvider: appViewProvider,
22 appViewProviderResolver: appViewProviderResolver,
23 );
24
25 final dynamic _bluesky;
26 final ModerationService? _moderationService;
27 final AppViewRequestContext _appViewContext;
28
29 Future<ListsResult> getLists({
30 required String actor,
31 String? cursor,
32 int limit = 50,
33 bool includeReference = false,
34 }) async {
35 final response = await _bluesky.graph.getLists(
36 actor: actor,
37 cursor: cursor,
38 limit: limit,
39 purposes: includeReference ? null : _listPurposes,
40 $headers: _appViewContext.appBskyHeadersForEndpoint(
41 'app.bsky.graph.getLists',
42 await _moderationService?.headersForRequest(),
43 ),
44 );
45
46 return ListsResult(lists: _filterLists(response.data.lists), cursor: response.data.cursor);
47 }
48
49 Future<ListDetailResult> getList({required AtUri listUri, String? cursor, int limit = 50}) async {
50 final response = await _bluesky.graph.getList(
51 list: listUri,
52 cursor: cursor,
53 limit: limit,
54 $headers: _appViewContext.appBskyHeadersForEndpoint(
55 'app.bsky.graph.getList',
56 await _moderationService?.headersForRequest(),
57 ),
58 );
59
60 return ListDetailResult(
61 list: response.data.list,
62 items: _filterListItems(response.data.items),
63 cursor: response.data.cursor,
64 );
65 }
66
67 Future<ListFeedResult> getListFeed({required AtUri listUri, String? cursor, int limit = 50}) async {
68 final response = await _bluesky.feed.getListFeed(
69 list: listUri,
70 cursor: cursor,
71 limit: limit,
72 $headers: _appViewContext.appBskyHeadersForEndpoint(
73 'app.bsky.feed.getListFeed',
74 await _moderationService?.headersForRequest(),
75 ),
76 );
77
78 return ListFeedResult(posts: _filterFeedPosts(response.data.feed), cursor: response.data.cursor);
79 }
80
81 Future<ListsWithMembershipResult> getListsWithMembership({
82 required String actor,
83 String? cursor,
84 int limit = 50,
85 }) async {
86 final response = await _bluesky.graph.getListsWithMembership(
87 actor: actor,
88 cursor: cursor,
89 limit: limit,
90 purposes: _membershipPurposes,
91 $headers: _appViewContext.appBskyHeadersForEndpoint(
92 'app.bsky.graph.getListsWithMembership',
93 await _moderationService?.headersForRequest(),
94 ),
95 );
96
97 return ListsWithMembershipResult(
98 lists: response.data.listsWithMembership.where((entry) => !_shouldFilterList(entry.list)).toList(growable: false),
99 cursor: response.data.cursor,
100 );
101 }
102
103 Future<List<ProfileViewBasic>> searchActorsTypeahead({required String query, int limit = 10}) async {
104 final response = await _bluesky.actor.searchActorsTypeahead(
105 q: query,
106 limit: limit,
107 $headers: _appViewContext.appBskyHeadersForEndpoint(
108 'app.bsky.actor.searchActorsTypeahead',
109 await _moderationService?.headersForRequest(),
110 ),
111 );
112
113 return _filterProfiles(response.data.actors);
114 }
115
116 Future<String> addListItem({required AtUri listUri, required String subjectDid}) async {
117 final response = await _bluesky.graph.listitem.create(
118 list: listUri,
119 subject: subjectDid,
120 createdAt: DateTime.now(),
121 $headers: _appViewContext.appBskyHeadersWithoutProxy(await _moderationService?.headersForRequest()),
122 );
123
124 return response.data.uri.toString();
125 }
126
127 Future<void> removeListItem({required AtUri listItemUri}) async {
128 await _bluesky.graph.listitem.delete(
129 rkey: listItemUri.rkey,
130 $headers: _appViewContext.appBskyHeadersWithoutProxy(await _moderationService?.headersForRequest()),
131 );
132 }
133
134 Future<void> muteList({required AtUri listUri}) async {
135 await _bluesky.graph.muteActorList(
136 list: listUri,
137 $headers: _appViewContext.appBskyHeadersWithoutProxy(await _moderationService?.headersForRequest()),
138 );
139 }
140
141 Future<void> unmuteList({required AtUri listUri}) async {
142 await _bluesky.graph.unmuteActorList(
143 list: listUri,
144 $headers: _appViewContext.appBskyHeadersWithoutProxy(await _moderationService?.headersForRequest()),
145 );
146 }
147
148 Future<String> blockList({required AtUri listUri}) async {
149 final response = await _bluesky.graph.listblock.create(
150 subject: listUri,
151 createdAt: DateTime.now(),
152 $headers: _appViewContext.appBskyHeadersWithoutProxy(await _moderationService?.headersForRequest()),
153 );
154
155 return response.data.uri.toString();
156 }
157
158 Future<void> unblockList({required AtUri blockUri}) async {
159 await _bluesky.graph.listblock.delete(
160 rkey: blockUri.rkey,
161 $headers: _appViewContext.appBskyHeadersWithoutProxy(await _moderationService?.headersForRequest()),
162 );
163 }
164
165 Future<BlobRef?> uploadListAvatar({required List<int> bytes, String mimeType = 'image/jpeg'}) async {
166 final response = await _bluesky.atproto.repo.uploadBlob(
167 bytes: Uint8List.fromList(bytes),
168 $headers: {'Content-Type': mimeType},
169 );
170 return response.data.blob.ref;
171 }
172
173 Future<AtUri> createList({
174 required String userDid,
175 required String name,
176 required String purpose,
177 String? description,
178 BlobRef? avatarBlob,
179 }) async {
180 final record = <String, dynamic>{
181 r'$type': 'app.bsky.graph.list',
182 'purpose': purpose,
183 'name': name,
184 'createdAt': DateTime.now().toUtc().toIso8601String(),
185 };
186 if (description != null) record['description'] = description;
187 if (avatarBlob != null) record['avatar'] = avatarBlob.toJson();
188
189 final response = await _bluesky.atproto.repo.createRecord(
190 repo: userDid,
191 collection: 'app.bsky.graph.list',
192 record: record,
193 );
194 return response.data.uri;
195 }
196
197 Future<void> updateList({
198 required AtUri listUri,
199 required String userDid,
200 required String name,
201 required String purpose,
202 String? description,
203 BlobRef? avatarBlob,
204 }) async {
205 final record = <String, dynamic>{
206 r'$type': 'app.bsky.graph.list',
207 'purpose': purpose,
208 'name': name,
209 'createdAt': DateTime.now().toUtc().toIso8601String(),
210 };
211 if (description != null) record['description'] = description;
212 if (avatarBlob != null) record['avatar'] = avatarBlob.toJson();
213
214 await _bluesky.atproto.repo.putRecord(
215 repo: userDid,
216 collection: 'app.bsky.graph.list',
217 rkey: listUri.rkey,
218 record: record,
219 );
220 }
221
222 Future<void> deleteList({required AtUri listUri, required String userDid}) async {
223 await _bluesky.atproto.repo.deleteRecord(repo: userDid, collection: 'app.bsky.graph.list', rkey: listUri.rkey);
224 }
225
226 List<ListView> _filterLists(List<ListView> lists) {
227 return lists.where((list) => !_shouldFilterList(list)).toList(growable: false);
228 }
229
230 List<ListItemView> _filterListItems(List<ListItemView> items) {
231 final moderationService = _moderationService;
232 if (moderationService == null) {
233 return items;
234 }
235
236 return items.where((item) => !moderationService.shouldFilterProfileInList(item.subject)).toList(growable: false);
237 }
238
239 List<ProfileViewBasic> _filterProfiles(List<ProfileViewBasic> profiles) {
240 final moderationService = _moderationService;
241 if (moderationService == null) {
242 return profiles;
243 }
244
245 return profiles
246 .where((profile) => !moderationService.shouldFilterProfileBasicInList(profile))
247 .toList(growable: false);
248 }
249
250 List<FeedViewPost> _filterFeedPosts(List<FeedViewPost> posts) {
251 final moderationService = _moderationService;
252 if (moderationService == null) {
253 return posts;
254 }
255
256 return posts.where((post) => !moderationService.shouldFilterFeedViewPostInList(post)).toList(growable: false);
257 }
258
259 bool _shouldFilterList(ListView list) {
260 final moderationService = _moderationService;
261 if (moderationService == null) {
262 return false;
263 }
264
265 return moderationService.shouldFilterProfileInList(list.creator);
266 }
267
268 static const List<GraphGetListsPurposes> _listPurposes = [
269 GraphGetListsPurposes.knownValue(data: KnownGraphGetListsPurposes.curatelist),
270 GraphGetListsPurposes.knownValue(data: KnownGraphGetListsPurposes.modlist),
271 ];
272
273 static const List<GraphGetListsWithMembershipPurposes> _membershipPurposes = [
274 GraphGetListsWithMembershipPurposes.knownValue(data: KnownGraphGetListsWithMembershipPurposes.curatelist),
275 GraphGetListsWithMembershipPurposes.knownValue(data: KnownGraphGetListsWithMembershipPurposes.modlist),
276 ];
277}
278
279class ListsResult {
280 const ListsResult({required this.lists, this.cursor});
281
282 final List<ListView> lists;
283 final String? cursor;
284}
285
286class ListDetailResult {
287 const ListDetailResult({required this.list, required this.items, this.cursor});
288
289 final ListView list;
290 final List<ListItemView> items;
291 final String? cursor;
292}
293
294class ListFeedResult {
295 const ListFeedResult({required this.posts, this.cursor});
296
297 final List<FeedViewPost> posts;
298 final String? cursor;
299}
300
301class ListsWithMembershipResult {
302 const ListsWithMembershipResult({required this.lists, this.cursor});
303
304 final List<ListWithMembership> lists;
305 final String? cursor;
306}