Line data Source code
1 : import 'package:flutter/foundation.dart';
2 : import 'package:flutter/material.dart';
3 : import 'package:rxdart/rxdart.dart';
4 : import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
5 :
6 : class ActivitiesControllers<A, Ob, T, Or> {
7 : final Map<String,
8 : BehaviorSubject<List<GenericEnrichedActivity<A, Ob, T, Or>>>>
9 : _controller = {};
10 :
11 1 : List<GenericEnrichedActivity<A, Ob, T, Or>>? getActivities(
12 : String feedGroup) =>
13 2 : _getController(feedGroup)?.valueOrNull;
14 :
15 2 : Stream<List<GenericEnrichedActivity<A, Ob, T, Or>>>? getStream(
16 : String feedGroup) =>
17 4 : _getController(feedGroup)?.stream;
18 :
19 6 : void init(String feedGroup) => _controller[feedGroup] =
20 2 : BehaviorSubject<List<GenericEnrichedActivity<A, Ob, T, Or>>>();
21 :
22 0 : void clearActivities(String feedGroup) {
23 0 : _getController(feedGroup)!.value = [];
24 : }
25 :
26 0 : void clearAllActivities(List<String> feedGroups) {
27 0 : feedGroups.forEach((feedGroups) => init(feedGroups));
28 : }
29 :
30 1 : void close() {
31 3 : _controller.forEach((key, value) {
32 1 : value.close();
33 : });
34 : }
35 :
36 : /// Check if controller is not empty.
37 2 : bool hasValue(String feedGroup) =>
38 4 : _getController(feedGroup)?.hasValue != null;
39 :
40 2 : void add(String feedGroup,
41 : List<GenericEnrichedActivity<A, Ob, T, Or>> activities) {
42 2 : if (hasValue(feedGroup)) {
43 4 : _getController(feedGroup)!.add(activities);
44 : } //TODO: handle null safety
45 : }
46 :
47 2 : BehaviorSubject<List<GenericEnrichedActivity<A, Ob, T, Or>>>? _getController(
48 : String feedGroup) =>
49 4 : _controller[feedGroup];
50 :
51 1 : void update(String feedGroup,
52 : List<GenericEnrichedActivity<A, Ob, T, Or>> activities) {
53 1 : if (hasValue(feedGroup)) {
54 2 : _getController(feedGroup)!.value = activities;
55 : }
56 : }
57 :
58 0 : void addError(String feedGroup, Object e, StackTrace stk) {
59 0 : if (hasValue(feedGroup)) {
60 0 : _getController(feedGroup)!.addError(e, stk);
61 : } //TODO: handle null safety
62 : }
63 : }
64 :
65 : class GenericFeedBloc<A, Ob, T, Or> {
66 4 : GenericFeedBloc({required this.client, this.analyticsClient});
67 :
68 : final StreamFeedClient client;
69 0 : StreamUser? get currentUser => client.currentUser;
70 :
71 : final StreamAnalytics? analyticsClient;
72 :
73 : @visibleForTesting
74 4 : late ReactionsControllers reactionsControllers = ReactionsControllers();
75 :
76 : @visibleForTesting
77 2 : late ActivitiesControllers<A, Ob, T, Or> activitiesController =
78 2 : ActivitiesControllers<A, Ob, T, Or>();
79 :
80 : /// The current activities list.
81 1 : List<GenericEnrichedActivity<A, Ob, T, Or>>? getActivities(
82 : String feedGroup) =>
83 2 : activitiesController.getActivities(feedGroup);
84 :
85 : /// The current reactions list.
86 2 : List<Reaction> getReactions(String activityId, [Reaction? reaction]) =>
87 4 : reactionsControllers.getReactions(activityId, reaction);
88 :
89 : /// The current activities list as a stream.
90 2 : Stream<List<GenericEnrichedActivity<A, Ob, T, Or>>>? getActivitiesStream(
91 : String feedGroup) =>
92 4 : activitiesController.getStream(feedGroup);
93 :
94 : /// The current reactions list as a stream.
95 2 : Stream<List<Reaction>>? getReactionsStream(
96 : //TODO: better name?
97 : String activityId,
98 : [String? kind]) {
99 4 : return reactionsControllers.getStream(activityId, kind);
100 : }
101 :
102 0 : void clearActivities(String feedGroup) =>
103 0 : activitiesController.clearActivities(feedGroup);
104 :
105 0 : void clearAllActivities(List<String> feedGroups) =>
106 0 : activitiesController.clearAllActivities(feedGroups);
107 :
108 : final _queryActivitiesLoadingController = BehaviorSubject.seeded(false);
109 :
110 : final Map<String, BehaviorSubject<bool>> _queryReactionsLoadingControllers =
111 : {};
112 :
113 : /// The stream notifying the state of queryReactions call.
114 0 : Stream<bool> queryReactionsLoadingFor(String activityId) =>
115 0 : _queryReactionsLoadingControllers[activityId]!;
116 :
117 : /// The stream notifying the state of queryActivities call.
118 0 : Stream<bool> get queryActivitiesLoading =>
119 0 : _queryActivitiesLoadingController.stream;
120 :
121 : /// Add an activity to the feed.
122 1 : Future<Activity> onAddActivity({
123 : required String feedGroup,
124 : Map<String, String>? data,
125 : required String verb,
126 : required String object,
127 : String? userId,
128 : List<FeedId>? to,
129 : }) async {
130 1 : final activity = Activity(
131 3 : actor: client.currentUser?.ref,
132 : verb: verb,
133 : object: object,
134 : extraData: data,
135 : to: to,
136 : );
137 :
138 2 : final flatFeed = client.flatFeed(feedGroup, userId);
139 :
140 2 : final addedActivity = await flatFeed.addActivity(activity);
141 :
142 : // TODO(Sacha): merge activity and enriched activity classes together
143 1 : final enrichedActivity = await flatFeed
144 2 : .getEnrichedActivityDetail<A, Ob, T, Or>(addedActivity.id!);
145 :
146 2 : final _activities = getActivities(feedGroup) ?? [];
147 :
148 : // ignore: cascade_invocations
149 1 : _activities.insert(0, enrichedActivity);
150 :
151 2 : activitiesController.add(feedGroup, _activities);
152 :
153 2 : await trackAnalytics(
154 : label: 'post',
155 1 : foreignId: activity.foreignId,
156 : feedGroup: feedGroup,
157 : ); //TODO: remove hardcoded value
158 : return addedActivity;
159 : }
160 :
161 : /// Remove child reaction.
162 1 : Future<void> onRemoveChildReaction({
163 : required String kind,
164 : required GenericEnrichedActivity activity,
165 : required Reaction childReaction,
166 : required Reaction parentReaction,
167 : }) async {
168 5 : await client.reactions.delete(childReaction.id!);
169 2 : final _reactions = getReactions(activity.id!, parentReaction);
170 :
171 1 : final reactionPath = _reactions.getReactionPath(parentReaction);
172 :
173 1 : final indexPath = _reactions.indexWhere(
174 4 : (r) => r.id! == parentReaction.id); //TODO: handle null safety
175 :
176 : final childrenCounts =
177 2 : reactionPath.childrenCounts.unshiftByKind(kind, ShiftType.decrement);
178 1 : final latestChildren = reactionPath.latestChildren
179 1 : .unshiftByKind(kind, childReaction, ShiftType.decrement);
180 1 : final ownChildren = reactionPath.ownChildren
181 1 : .unshiftByKind(kind, childReaction, ShiftType.decrement);
182 :
183 1 : final updatedReaction = reactionPath.copyWith(
184 : ownChildren: ownChildren,
185 : latestChildren: latestChildren,
186 : childrenCounts: childrenCounts,
187 : );
188 :
189 : // remove reaction from rxstream
190 1 : reactionsControllers
191 2 : ..unshiftById(activity.id!, childReaction, ShiftType.decrement)
192 3 : ..update(activity.id!, _reactions.updateIn(updatedReaction, indexPath));
193 : }
194 :
195 1 : Future<Reaction> onAddChildReaction({
196 : required String kind,
197 : required Reaction reaction,
198 : required GenericEnrichedActivity activity,
199 : Map<String, Object>? data,
200 : String? userId,
201 : List<FeedId>? targetFeeds,
202 : }) async {
203 5 : final childReaction = await client.reactions.addChild(kind, reaction.id!,
204 : data: data, userId: userId, targetFeeds: targetFeeds);
205 2 : final _reactions = getReactions(activity.id!, reaction);
206 1 : final reactionPath = _reactions.getReactionPath(reaction);
207 : final indexPath = _reactions
208 5 : .indexWhere((r) => r.id! == reaction.id); //TODO: handle null safety
209 :
210 2 : final childrenCounts = reactionPath.childrenCounts.unshiftByKind(kind);
211 : final latestChildren =
212 2 : reactionPath.latestChildren.unshiftByKind(kind, childReaction);
213 : final ownChildren =
214 2 : reactionPath.ownChildren.unshiftByKind(kind, childReaction);
215 :
216 1 : final updatedReaction = reactionPath.copyWith(
217 : ownChildren: ownChildren,
218 : latestChildren: latestChildren,
219 : childrenCounts: childrenCounts,
220 : );
221 :
222 : // adds reaction to the rxstream
223 1 : reactionsControllers
224 2 : ..unshiftById(activity.id!, childReaction)
225 3 : ..update(activity.id!, _reactions.updateIn(updatedReaction, indexPath));
226 : // return reaction;
227 : return childReaction;
228 : }
229 :
230 : /// Remove reaction from the feed.
231 1 : Future<void> onRemoveReaction({
232 : required String kind,
233 : required GenericEnrichedActivity<A, Ob, T, Or> activity,
234 : required Reaction reaction,
235 : required String feedGroup,
236 : }) async {
237 5 : await client.reactions.delete(reaction.id!);
238 2 : await trackAnalytics(
239 2 : label: 'un$kind', foreignId: activity.foreignId, feedGroup: feedGroup);
240 2 : final _activities = getActivities(feedGroup) ?? [activity];
241 1 : final activityPath = _activities.getEnrichedActivityPath(activity);
242 :
243 : final indexPath = _activities
244 5 : .indexWhere((a) => a.id! == activity.id); //TODO: handle null safety
245 :
246 : final reactionCounts =
247 2 : activityPath.reactionCounts.unshiftByKind(kind, ShiftType.decrement);
248 :
249 : // final reaction =
250 : // reactionsFor(activity.id!).firstWhere((reaction) => reaction.id == id);
251 1 : final latestReactions = activityPath.latestReactions
252 1 : .unshiftByKind(kind, reaction, ShiftType.decrement);
253 :
254 1 : final ownReactions = activityPath.ownReactions
255 1 : .unshiftByKind(kind, reaction, ShiftType.decrement);
256 :
257 1 : final updatedActivity = activityPath.copyWith(
258 : ownReactions: ownReactions,
259 : latestReactions: latestReactions,
260 : reactionCounts: reactionCounts,
261 : );
262 :
263 : // remove reaction from the stream
264 2 : reactionsControllers.unshiftById(
265 1 : activity.id!, reaction, ShiftType.decrement);
266 :
267 2 : activitiesController.update(
268 1 : feedGroup, _activities.updateIn(updatedActivity, indexPath));
269 : }
270 :
271 : /// Add a new reaction to the feed.
272 1 : Future<Reaction> onAddReaction({
273 : Map<String, Object>? data,
274 : required String kind,
275 : required GenericEnrichedActivity<A, Ob, T, Or> activity,
276 : List<FeedId>? targetFeeds,
277 : required String feedGroup,
278 : }) async {
279 3 : final reaction = await client.reactions
280 2 : .add(kind, activity.id!, targetFeeds: targetFeeds, data: data);
281 2 : await trackAnalytics(
282 1 : label: kind, foreignId: activity.foreignId, feedGroup: feedGroup);
283 2 : final _activities = getActivities(feedGroup) ?? [activity];
284 1 : final activityPath = _activities.getEnrichedActivityPath(activity);
285 : final indexPath = _activities
286 5 : .indexWhere((a) => a.id! == activity.id); //TODO: handle null safety
287 :
288 2 : final reactionCounts = activityPath.reactionCounts.unshiftByKind(kind);
289 : final latestReactions =
290 2 : activityPath.latestReactions.unshiftByKind(kind, reaction);
291 : final ownReactions =
292 2 : activityPath.ownReactions.unshiftByKind(kind, reaction);
293 :
294 1 : final updatedActivity = activityPath.copyWith(
295 : ownReactions: ownReactions,
296 : latestReactions: latestReactions,
297 : reactionCounts: reactionCounts,
298 : );
299 :
300 : // adds reaction to the stream
301 3 : reactionsControllers.unshiftById(activity.id!, reaction);
302 :
303 2 : activitiesController.update(
304 : feedGroup,
305 : _activities //TODO: handle null safety
306 1 : .updateIn(updatedActivity, indexPath));
307 : return reaction;
308 : }
309 :
310 : /// Track analytics.
311 1 : Future<void> trackAnalytics({
312 : required String label,
313 : String? foreignId,
314 : required String feedGroup,
315 : }) async {
316 1 : analyticsClient != null
317 0 : ? await analyticsClient!.trackEngagement(Engagement(
318 0 : content: Content(foreignId: FeedId.fromId(foreignId)),
319 : label: label,
320 0 : feedId: FeedId.fromId(feedGroup),
321 : ))
322 1 : : print('warning: analytics: not enabled'); //TODO:logger
323 : }
324 :
325 2 : Future<void> queryReactions(
326 : LookupAttribute lookupAttr,
327 : String lookupValue, {
328 : Filter? filter,
329 : int? limit,
330 : String? kind,
331 : EnrichmentFlags? flags,
332 : }) async {
333 4 : reactionsControllers.init(lookupValue);
334 4 : _queryReactionsLoadingControllers[lookupValue] =
335 2 : BehaviorSubject.seeded(false);
336 8 : if (_queryReactionsLoadingControllers[lookupValue]?.value == true) return;
337 :
338 4 : if (reactionsControllers.hasValue(lookupValue)) {
339 4 : _queryReactionsLoadingControllers[lookupValue]!
340 2 : .add(true); //TODO: fix null
341 : }
342 :
343 : try {
344 4 : final oldReactions = List<Reaction>.from(getReactions(lookupValue));
345 8 : final reactionsResponse = await client.reactions.filter(
346 : lookupAttr,
347 : lookupValue,
348 : filter: filter,
349 : flags: flags,
350 : limit: limit,
351 : kind: kind,
352 : );
353 2 : final temp = oldReactions + reactionsResponse;
354 4 : reactionsControllers.add(lookupValue, temp);
355 : } catch (e, stk) {
356 : // reset loading controller
357 0 : _queryReactionsLoadingControllers[lookupValue]?.add(false);
358 0 : if (reactionsControllers.hasValue(lookupValue)) {
359 0 : _queryReactionsLoadingControllers[lookupValue]?.addError(e, stk);
360 : } else {
361 0 : reactionsControllers.addError(lookupValue, e, stk);
362 : }
363 : }
364 : }
365 :
366 1 : Future<void> queryEnrichedActivities({
367 : required String feedGroup,
368 : int? limit,
369 : int? offset,
370 : String? session,
371 : Filter? filter,
372 : EnrichmentFlags? flags,
373 : String? ranking,
374 : String? userId,
375 :
376 : //TODO: no way to parameterized marker?
377 : }) async {
378 2 : activitiesController.init(feedGroup);
379 3 : if (_queryActivitiesLoadingController.value == true) return;
380 :
381 2 : if (activitiesController.hasValue(feedGroup)) {
382 2 : _queryActivitiesLoadingController.add(true);
383 : }
384 :
385 : try {
386 2 : final activitiesResponse = await client
387 1 : .flatFeed(feedGroup, userId)
388 1 : .getEnrichedActivities<A, Ob, T, Or>(
389 : limit: limit,
390 : offset: offset,
391 : session: session,
392 : filter: filter,
393 : flags: flags,
394 : ranking: ranking,
395 : );
396 :
397 2 : activitiesController.add(feedGroup, activitiesResponse);
398 2 : if (activitiesController.hasValue(feedGroup) &&
399 2 : _queryActivitiesLoadingController.value) {
400 3 : _queryActivitiesLoadingController.sink.add(false);
401 : }
402 : } catch (e, stk) {
403 : // reset loading controller
404 0 : _queryActivitiesLoadingController.add(false);
405 0 : if (activitiesController.hasValue(feedGroup)) {
406 0 : _queryActivitiesLoadingController.addError(e, stk);
407 : } else {
408 0 : activitiesController.addError(feedGroup, e, stk);
409 : }
410 : }
411 : }
412 :
413 : /// Follows the given [flatFeed].
414 1 : Future<void> followFlatFeed(
415 : String otherUser,
416 : ) async {
417 2 : final timeline = client.flatFeed('timeline');
418 2 : final user = client.flatFeed('user', otherUser);
419 2 : await timeline.follow(user);
420 : }
421 :
422 : /// Unfollows the given [actingFeed].
423 1 : Future<void> unfollowFlatFeed(
424 : String otherUser,
425 : ) async {
426 2 : final timeline = client.flatFeed('timeline');
427 2 : final user = client.flatFeed('user', otherUser);
428 2 : await timeline.unfollow(user);
429 : }
430 :
431 : /// Checks whether the current user is following a feed with the given
432 : /// [userId].
433 : ///
434 : /// It filters the request such that if the current user is in fact
435 : /// following the given user, one user will be returned that matches the
436 : /// current user, thus indicating that the current user does follow the given
437 : /// user. If no results are found, this means that the current user is not
438 : /// following the given user.
439 1 : Future<bool> isFollowingUser(String userId) async {
440 4 : final following = await client.flatFeed('timeline').following(
441 : limit: 1,
442 : offset: 0,
443 1 : filter: [
444 2 : FeedId.id('user:$userId'),
445 : ],
446 : );
447 1 : return following.isNotEmpty;
448 : }
449 :
450 1 : void dispose() {
451 2 : activitiesController.close();
452 2 : reactionsControllers.close();
453 2 : _queryActivitiesLoadingController.close();
454 3 : _queryReactionsLoadingControllers.forEach((key, value) {
455 1 : value.close();
456 : });
457 : }
458 :
459 0 : Future<void> onRemoveActivity({
460 : required String feedGroup,
461 : required String activityId,
462 : }) async {
463 0 : await client.flatFeed(feedGroup).removeActivityById(activityId);
464 : }
465 : }
466 :
467 : class GenericFeedProvider<A, Ob, T, Or> extends InheritedWidget {
468 3 : const GenericFeedProvider({
469 : Key? key,
470 : required this.bloc,
471 : required Widget child,
472 3 : }) : super(key: key, child: child);
473 :
474 3 : factory GenericFeedProvider.of(BuildContext context) {
475 3 : final result = context.dependOnInheritedWidgetOfExactType<
476 : GenericFeedProvider<A, Ob, T, Or>>();
477 1 : assert(result != null,
478 1 : 'No GenericFeedProvider<$A, $Ob, $T, $Or> found in context');
479 : return result!;
480 : }
481 : final GenericFeedBloc<A, Ob, T, Or> bloc;
482 :
483 0 : @override
484 0 : bool updateShouldNotify(GenericFeedProvider old) => bloc != old.bloc; //
485 :
486 0 : @override
487 : void debugFillProperties(DiagnosticPropertiesBuilder properties) {
488 0 : super.debugFillProperties(properties);
489 : properties
490 0 : .add(DiagnosticsProperty<GenericFeedBloc<A, Ob, T, Or>>('bloc', bloc));
491 : }
492 : }
493 :
494 : class ReactionsControllers {
495 : final Map<String, BehaviorSubject<List<Reaction>>> _controller = {};
496 :
497 : /// Init controller for given activityId.
498 2 : void init(String lookupValue) =>
499 6 : _controller[lookupValue] = BehaviorSubject<List<Reaction>>();
500 :
501 : /// Retrieve with activityId the corresponding StreamController from the map
502 : /// of controllers.
503 2 : BehaviorSubject<List<Reaction>>? _getController(String lookupValue) =>
504 4 : _controller[lookupValue]; //TODO: handle null safety
505 :
506 : ///Retrieve Stream of reactions with activityId and filter it if necessary
507 2 : Stream<List<Reaction>>? getStream(String lookupValue, [String? kind]) {
508 : final isFiltered = kind != null;
509 4 : final reactionStream = _getController(lookupValue)?.stream;
510 : return isFiltered
511 2 : ? reactionStream?.map((reactions) =>
512 5 : reactions.where((reaction) => reaction.kind == kind).toList())
513 : : reactionStream; //TODO: handle null safety
514 : }
515 :
516 : /// Convert the Stream of reactions to a List of reactions.
517 2 : List<Reaction> getReactions(String lookupValue, [Reaction? reaction]) =>
518 4 : _getController(lookupValue)?.valueOrNull ??
519 3 : (reaction != null ? [reaction] : <Reaction>[]);
520 :
521 : /// Check if controller is not empty.
522 2 : bool hasValue(String lookupValue) =>
523 4 : _getController(lookupValue)?.hasValue != null;
524 :
525 : /// Lookup latest Reactions by Id and inserts the given reaction to the
526 : /// beginning of the list.
527 1 : void unshiftById(String lookupValue, Reaction reaction,
528 : [ShiftType type = ShiftType.increment]) =>
529 2 : _controller.unshiftById(lookupValue, reaction, type);
530 :
531 : /// Close every stream controllers.
532 4 : void close() => _controller.forEach((key, value) {
533 1 : value.close();
534 : });
535 :
536 : /// Update controller value with given reactions.
537 1 : void update(String lookupValue, List<Reaction> reactions) {
538 1 : if (hasValue(lookupValue)) {
539 2 : _getController(lookupValue)!.value = reactions;
540 : }
541 : }
542 :
543 : /// Add given reactions to the correct controller.
544 2 : void add(String lookupValue, List<Reaction> temp) {
545 2 : if (hasValue(lookupValue)) {
546 4 : _getController(lookupValue)!.add(temp);
547 : } //TODO: handle null safety
548 : }
549 :
550 : /// Add error to the correct controller.
551 0 : void addError(String lookupValue, Object e, StackTrace stk) {
552 0 : if (hasValue(lookupValue)) {
553 0 : _getController(lookupValue)!.addError(e, stk);
554 : } //TODO: handle null safety
555 : }
556 : }
|