Skip to content

Commit 6926cb7

Browse files
authored
feat(llc, samples): improve follow counts (#49)
1 parent 3a66b67 commit 6926cb7

File tree

15 files changed

+792
-214
lines changed

15 files changed

+792
-214
lines changed

packages/stream_feeds/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## unreleased
2+
- Update follower and following counts on the feed state when receiving follow websocket events.
3+
14
## 0.3.1
25
- Update API client with renaming `addReaction` to `addActivityReaction` and `deleteReaction` to `deleteActivityReaction`.
36
- Update `activity.currentFeed` capabilities when adding or updating activity from websocket events.

packages/stream_feeds/lib/src/models/feed_id.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,21 @@ class FeedId with _$FeedId {
1414
required this.id,
1515
}) : rawValue = '$group:$id';
1616

17+
/// The user's timeline feed containing posts from followed users
18+
const FeedId.timeline(String id) : this(group: 'timeline', id: id);
19+
20+
/// Notifications feed
21+
const FeedId.notification(String id) : this(group: 'notification', id: id);
22+
23+
/// The user's feed containing stories from followed users
24+
const FeedId.stories(String id) : this(group: 'stories', id: id);
25+
26+
/// The user's own stories
27+
const FeedId.story(String id) : this(group: 'story', id: id);
28+
29+
/// TThe user's own posts
30+
const FeedId.user(String id) : this(group: 'user', id: id);
31+
1732
/// Creates a feed identifier from a raw string value.
1833
///
1934
/// The string should be in the format `"group:id"`. If the string

packages/stream_feeds/lib/src/state/feed_state.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,21 +349,29 @@ class FeedStateNotifier extends StateNotifier<FeedState> {
349349
}
350350

351351
if (follow.isFollowingFeed(state.fid)) {
352+
final updatedCount = follow.sourceFeed.followingCount;
352353
final updatedFollowing = state.following.upsert(
353354
follow,
354355
key: (it) => it.id,
355356
);
356357

357-
return state.copyWith(following: updatedFollowing);
358+
return state.copyWith(
359+
following: updatedFollowing,
360+
feed: state.feed?.copyWith(followingCount: updatedCount),
361+
);
358362
}
359363

360364
if (follow.isFollowerOf(state.fid)) {
365+
final updatedCount = follow.targetFeed.followerCount;
361366
final updatedFollowers = state.followers.upsert(
362367
follow,
363368
key: (it) => it.id,
364369
);
365370

366-
return state.copyWith(followers: updatedFollowers);
371+
return state.copyWith(
372+
followers: updatedFollowers,
373+
feed: state.feed?.copyWith(followerCount: updatedCount),
374+
);
367375
}
368376

369377
// If the follow doesn't match any known categories,
@@ -372,6 +380,17 @@ class FeedStateNotifier extends StateNotifier<FeedState> {
372380
}
373381

374382
FeedState _removeFollow(FollowData follow, FeedState state) {
383+
var feed = state.feed;
384+
385+
if (follow.isFollowerOf(state.fid)) {
386+
final followerCount = follow.targetFeed.followerCount;
387+
feed = feed?.copyWith(followerCount: followerCount);
388+
}
389+
if (follow.isFollowingFeed(state.fid)) {
390+
final followingCount = follow.sourceFeed.followingCount;
391+
feed = feed?.copyWith(followingCount: followingCount);
392+
}
393+
375394
final updatedFollowing = state.following.where((it) {
376395
return it.id != follow.id;
377396
}).toList();
@@ -385,6 +404,7 @@ class FeedStateNotifier extends StateNotifier<FeedState> {
385404
}).toList();
386405

387406
return state.copyWith(
407+
feed: feed,
388408
following: updatedFollowing,
389409
followers: updatedFollowers,
390410
followRequests: updatedFollowRequests,

packages/stream_feeds/test/client/feeds_client_impl_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import 'package:stream_feeds/src/client/feeds_client_impl.dart';
22
import 'package:stream_feeds/stream_feeds.dart';
33
import 'package:test/test.dart';
44

5-
import '../mocks.dart';
5+
import '../test_utils.dart';
66

77
void main() {
88
test('Create a feeds client', () {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import 'package:stream_feeds/stream_feeds.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
test('feed id should be created with group and id', () {
6+
const feedId = FeedId(group: 'group', id: 'id');
7+
expect(feedId.group, 'group');
8+
expect(feedId.id, 'id');
9+
expect(feedId.rawValue, 'group:id');
10+
});
11+
12+
test('feed id should be created with timeline group and id', () {
13+
const feedId = FeedId.timeline('id');
14+
expect(feedId.group, 'timeline');
15+
expect(feedId.id, 'id');
16+
expect(feedId.rawValue, 'timeline:id');
17+
});
18+
19+
test('feed id should be created with notification group and id', () {
20+
const feedId = FeedId.notification('id');
21+
expect(feedId.group, 'notification');
22+
expect(feedId.id, 'id');
23+
expect(feedId.rawValue, 'notification:id');
24+
});
25+
26+
test('feed id should be created with stories group and id', () {
27+
const feedId = FeedId.stories('id');
28+
expect(feedId.group, 'stories');
29+
expect(feedId.id, 'id');
30+
expect(feedId.rawValue, 'stories:id');
31+
});
32+
33+
test('feed id should be created with story group and id', () {
34+
const feedId = FeedId.story('id');
35+
expect(feedId.group, 'story');
36+
expect(feedId.id, 'id');
37+
expect(feedId.rawValue, 'story:id');
38+
});
39+
40+
test('feed id should be created with user group and id', () {
41+
const feedId = FeedId.user('id');
42+
expect(feedId.group, 'user');
43+
expect(feedId.id, 'id');
44+
expect(feedId.rawValue, 'user:id');
45+
});
46+
}

packages/stream_feeds/test/state/activity_test.dart

Lines changed: 9 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// ignore_for_file: avoid_redundant_argument_values
2-
31
import 'dart:async';
42
import 'dart:convert';
53

@@ -9,8 +7,7 @@ import 'package:stream_feeds/src/state/activity_state.dart';
97
import 'package:stream_feeds/stream_feeds.dart';
108
import 'package:test/test.dart';
119

12-
import '../mocks.dart';
13-
import '../ws_test_helpers.dart';
10+
import '../test_utils.dart';
1411

1512
void main() {
1613
late StreamFeedsClientImpl client;
@@ -47,7 +44,7 @@ void main() {
4744
depth: 3,
4845
),
4946
).thenAnswer(
50-
(_) async => const Result.success(defaultCommentsResponse),
47+
(_) async => Result.success(createDefaultCommentsResponse()),
5148
);
5249

5350
final activity = client.activity(
@@ -102,7 +99,7 @@ void main() {
10299
depth: 3,
103100
),
104101
).thenAnswer(
105-
(_) async => const Result.success(defaultCommentsResponse),
102+
(_) async => Result.success(createDefaultCommentsResponse()),
106103
);
107104
}
108105

@@ -146,7 +143,7 @@ void main() {
146143
optionId: firstOptionId,
147144
pollId: pollId,
148145
),
149-
type: 'feeds.poll.vote_casted',
146+
type: EventTypes.pollVoteCasted,
150147
).toJson(),
151148
),
152149
);
@@ -191,7 +188,7 @@ void main() {
191188
optionId: 'optionId1',
192189
pollId: 'pollId1',
193190
),
194-
type: 'feeds.poll.vote_casted',
191+
type: EventTypes.pollVoteCasted,
195192
),
196193
),
197194
);
@@ -251,7 +248,7 @@ void main() {
251248
optionId: 'optionId1',
252249
pollId: 'pollId1',
253250
),
254-
type: 'feeds.poll.vote_removed',
251+
type: EventTypes.pollVoteRemoved,
255252
),
256253
),
257254
);
@@ -309,7 +306,7 @@ void main() {
309306
optionId: 'optionId1',
310307
pollId: pollId,
311308
),
312-
type: 'feeds.poll.vote_removed',
309+
type: EventTypes.pollVoteRemoved,
313310
),
314311
),
315312
);
@@ -344,7 +341,7 @@ void main() {
344341
custom: const {},
345342
fid: 'fid',
346343
poll: poll.copyWith(isClosed: true),
347-
type: 'feeds.poll.closed',
344+
type: EventTypes.pollClosed,
348345
),
349346
),
350347
);
@@ -378,106 +375,10 @@ void main() {
378375
custom: const {},
379376
fid: 'fid',
380377
poll: poll,
381-
type: 'feeds.poll.deleted',
378+
type: EventTypes.pollDeleted,
382379
),
383380
),
384381
);
385382
});
386383
});
387384
}
388-
389-
const defaultCommentsResponse = GetCommentsResponse(
390-
comments: [],
391-
next: null,
392-
prev: null,
393-
duration: 'duration',
394-
);
395-
396-
GetActivityResponse createDefaultActivityResponse({PollResponseData? poll}) =>
397-
GetActivityResponse(
398-
activity: ActivityResponse(
399-
id: 'id',
400-
attachments: const [],
401-
bookmarkCount: 0,
402-
commentCount: 0,
403-
comments: const [],
404-
createdAt: DateTime(2021, 1, 1),
405-
custom: const {},
406-
feeds: const [],
407-
filterTags: const [],
408-
interestTags: const [],
409-
latestReactions: const [],
410-
mentionedUsers: const [],
411-
moderation: null,
412-
notificationContext: null,
413-
ownBookmarks: const [],
414-
ownReactions: const [],
415-
parent: null,
416-
poll: poll,
417-
popularity: 0,
418-
reactionCount: 0,
419-
reactionGroups: const {},
420-
score: 0,
421-
searchData: const {},
422-
shareCount: 0,
423-
text: null,
424-
type: 'type',
425-
updatedAt: DateTime(2021, 2, 1),
426-
user: UserResponse(
427-
id: 'id',
428-
name: 'name',
429-
banned: false,
430-
blockedUserIds: const [],
431-
createdAt: DateTime(2021, 1, 1),
432-
custom: const {},
433-
language: 'language',
434-
online: false,
435-
role: 'role',
436-
teams: const [],
437-
updatedAt: DateTime(2021, 2, 1),
438-
),
439-
visibility: ActivityResponseVisibility.public,
440-
visibilityTag: null,
441-
),
442-
duration: 'duration',
443-
);
444-
445-
PollResponseData createDefaultPollResponseData({
446-
List<PollVoteResponseData> latestAnswers = const [],
447-
Map<String, List<PollVoteResponseData>> latestVotesByOption = const {},
448-
}) =>
449-
PollResponseData(
450-
id: 'id',
451-
name: 'name',
452-
allowAnswers: true,
453-
allowUserSuggestedOptions: true,
454-
answersCount: latestAnswers.length,
455-
createdAt: DateTime.now(),
456-
createdById: 'id',
457-
custom: const {},
458-
description: 'description',
459-
enforceUniqueVote: true,
460-
latestAnswers: latestAnswers,
461-
latestVotesByOption: latestVotesByOption,
462-
ownVotes: const [],
463-
updatedAt: DateTime.now(),
464-
voteCount: latestVotesByOption.values
465-
.map((e) => e.length)
466-
.fold(0, (v, e) => v + e),
467-
voteCountsByOption: latestVotesByOption.map(
468-
(k, e) => MapEntry(k, e.length),
469-
),
470-
votingVisibility: 'visibility',
471-
options: const [
472-
PollOptionResponseData(
473-
id: 'id1',
474-
text: 'text1',
475-
custom: {},
476-
),
477-
PollOptionResponseData(
478-
id: 'id2',
479-
text: 'text2',
480-
custom: {},
481-
),
482-
],
483-
);

0 commit comments

Comments
 (0)