Skip to content

Commit 6e15d38

Browse files
committed
improve following in the sample app
1 parent 3a66b67 commit 6e15d38

File tree

5 files changed

+218
-105
lines changed

5 files changed

+218
-105
lines changed

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,

sample_app/lib/screens/user_feed/profile/profile_section.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ class ProfileSection<T> extends StatelessWidget {
99
required this.items,
1010
required this.emptyMessage,
1111
required this.itemBuilder,
12+
this.count,
1213
});
1314

1415
final String title;
1516
final List<T> items;
1617
final String emptyMessage;
1718
final Widget Function(T item) itemBuilder;
19+
final int? count;
1820

1921
@override
2022
Widget build(BuildContext context) {
@@ -24,7 +26,7 @@ class ProfileSection<T> extends StatelessWidget {
2426
children: [
2527
_SectionHeader(
2628
title: title,
27-
count: items.length,
29+
count: count ?? items.length,
2830
),
2931
DecoratedBox(
3032
decoration: BoxDecoration(
Lines changed: 161 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:math' as math;
2+
13
import 'package:collection/collection.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
@@ -66,109 +68,172 @@ class _UserProfileState extends State<UserProfile> {
6668
Widget build(BuildContext context) {
6769
return StateNotifierBuilder(
6870
stateNotifier: widget.timelineFeed.notifier,
69-
builder: (context, state, child) {
70-
final feedMembers = state.members;
71-
final followRequests = state.followRequests;
72-
final following = state.following;
73-
final currentUser = client.user;
74-
75-
final followIncludesCurrentUser =
76-
following.any((it) => it.targetFeed.id == currentUser.id) ||
77-
(followSuggestions?.any((it) => it.fid.id == currentUser.id) ??
78-
false);
79-
80-
return SingleChildScrollView(
81-
controller: widget.scrollController,
82-
padding: const EdgeInsets.all(20),
83-
child: Column(
84-
crossAxisAlignment: CrossAxisAlignment.start,
85-
spacing: 24,
86-
children: [
87-
// Profile Header Section
88-
ProfileHeader(
89-
user: currentUser,
90-
membersCount: state.feed?.memberCount ?? 0,
91-
followingCount: state.feed?.followingCount ?? 0,
92-
followersCount: state.feed?.followerCount ?? 0,
71+
builder: (context, timelineState, child) {
72+
return StateNotifierBuilder(
73+
stateNotifier: widget.userFeed.notifier,
74+
builder: (context, userState, child) {
75+
return _UserProfileContent(
76+
client: client,
77+
timelineState: timelineState,
78+
userState: userState,
79+
followSuggestions: followSuggestions,
80+
scrollController: widget.scrollController,
81+
onAcceptFollow: (fid) => widget.userFeed.acceptFollow(
82+
sourceFid: fid,
9383
),
94-
95-
// Members Section
96-
ProfileSection<FeedMemberData>(
97-
title: 'Members',
98-
items: feedMembers,
99-
emptyMessage: 'No members yet',
100-
itemBuilder: (member) => MemberListItem(member: member),
84+
onRejectFollow: (fid) => widget.userFeed.rejectFollow(
85+
sourceFid: fid,
10186
),
102-
103-
// Follow Requests Section
104-
ProfileSection<FollowData>(
105-
title: 'Follow Requests',
106-
items: followRequests,
107-
emptyMessage: 'No pending requests',
108-
itemBuilder: (followRequest) => FollowRequestListItem(
109-
followRequest: followRequest,
110-
onAcceptPressed: () => widget.timelineFeed.acceptFollow(
111-
sourceFid: followRequest.sourceFeed.fid,
112-
),
113-
onRejectPressed: () => widget.timelineFeed.rejectFollow(
114-
sourceFid: followRequest.sourceFeed.fid,
87+
onFollow: (targetFeed) async {
88+
final result = await widget.timelineFeed.follow(
89+
targetFid: targetFeed.fid,
90+
createNotificationActivity: true,
91+
);
92+
93+
// Remove the followed user from suggestions
94+
result.onSuccess(
95+
(_) => _updateFollowSuggestions([
96+
...?followSuggestions?.where((it) => it != targetFeed),
97+
]),
98+
);
99+
},
100+
onUnfollow: (targetFeed) async {
101+
final result = await widget.timelineFeed.unfollow(
102+
targetFid: targetFeed.fid,
103+
);
104+
105+
// Add the unfollowed user back to suggestions
106+
result.onSuccess(
107+
(_) => _updateFollowSuggestions(
108+
[...?followSuggestions, targetFeed],
115109
),
116-
),
117-
),
110+
);
111+
},
112+
);
113+
},
114+
);
115+
},
116+
);
117+
}
118+
}
118119

119-
// Following Section
120-
ProfileSection<FollowData>(
121-
title: 'Following',
122-
items: following,
123-
emptyMessage: 'Not following anyone yet',
124-
itemBuilder: (follow) => FollowingListItem(
125-
follow: follow,
126-
onUnfollowPressed: () async {
127-
final result = await widget.timelineFeed.unfollow(
128-
targetFid: follow.targetFeed.fid,
129-
);
130-
131-
// Add the unfollowed user back to suggestions
132-
result.onSuccess(
133-
(_) => _updateFollowSuggestions(
134-
[...?followSuggestions, follow.targetFeed],
135-
),
136-
);
137-
},
138-
),
139-
),
120+
class _UserProfileContent extends StatelessWidget {
121+
const _UserProfileContent({
122+
required this.client,
123+
required this.timelineState,
124+
required this.userState,
125+
required this.followSuggestions,
126+
required this.scrollController,
127+
required this.onAcceptFollow,
128+
required this.onRejectFollow,
129+
required this.onFollow,
130+
required this.onUnfollow,
131+
});
140132

141-
// Follow Suggestions Section
142-
ProfileSection<FeedData>(
143-
title: 'Suggested',
144-
items: [
145-
if (!followIncludesCurrentUser &&
146-
widget.userFeed.state.feed != null)
147-
widget.userFeed.state.feed!,
148-
...(followSuggestions ?? []),
149-
],
150-
emptyMessage: 'No suggestions available',
151-
itemBuilder: (suggestion) => SuggestionListItem(
152-
suggestion: suggestion,
153-
onFollowPressed: () async {
154-
final result = await widget.timelineFeed.follow(
155-
targetFid: suggestion.fid,
156-
createNotificationActivity: true,
157-
);
158-
159-
// Remove the followed user from suggestions
160-
result.onSuccess(
161-
(_) => _updateFollowSuggestions([
162-
...?followSuggestions?.where((it) => it != suggestion),
163-
]),
164-
);
165-
},
166-
),
167-
),
133+
final StreamFeedsClient client;
134+
final FeedState timelineState;
135+
final FeedState userState;
136+
final List<FeedData>? followSuggestions;
137+
final ScrollController? scrollController;
138+
139+
final ValueSetter<FeedId> onAcceptFollow;
140+
final ValueSetter<FeedId> onRejectFollow;
141+
final ValueSetter<FeedData> onFollow;
142+
final ValueSetter<FeedData> onUnfollow;
143+
144+
@override
145+
Widget build(BuildContext context) {
146+
final currentUser = client.user;
147+
final feedMembers = timelineState.members;
148+
149+
// We always follow ourselves, so we don't need to show it in the following list
150+
final following = timelineState.following
151+
.where((it) => it.targetFeed.id != currentUser.id)
152+
.toList();
153+
final followingCount = math.max(
154+
0,
155+
(timelineState.feed?.followingCount ?? 0) - 1,
156+
);
157+
final followersCount = math.max(
158+
0,
159+
(userState.feed?.followerCount ?? 0) - 1,
160+
);
161+
162+
final followRequests = userState.followRequests;
163+
164+
final followIncludesCurrentUser = following
165+
.any((it) => it.targetFeed.id == currentUser.id) ||
166+
(followSuggestions?.any((it) => it.fid.id == currentUser.id) ?? false);
167+
168+
return SingleChildScrollView(
169+
controller: scrollController,
170+
padding: const EdgeInsets.all(20),
171+
child: Column(
172+
crossAxisAlignment: CrossAxisAlignment.start,
173+
spacing: 24,
174+
children: [
175+
// Profile Header Section
176+
ProfileHeader(
177+
user: currentUser,
178+
membersCount: timelineState.feed?.memberCount ?? 0,
179+
followingCount: followingCount,
180+
followersCount: followersCount,
181+
),
182+
183+
// Members Section
184+
ProfileSection<FeedMemberData>(
185+
title: 'Members',
186+
items: feedMembers,
187+
emptyMessage: 'No members yet',
188+
itemBuilder: (member) => MemberListItem(member: member),
189+
),
190+
191+
// Follow Requests Section
192+
ProfileSection<FollowData>(
193+
title: 'Follow Requests',
194+
items: followRequests,
195+
emptyMessage: 'No pending requests',
196+
itemBuilder: (followRequest) => FollowRequestListItem(
197+
followRequest: followRequest,
198+
onAcceptPressed: () =>
199+
onAcceptFollow(followRequest.sourceFeed.fid),
200+
onRejectPressed: () =>
201+
onRejectFollow(followRequest.sourceFeed.fid),
202+
),
203+
),
204+
205+
// Following Section
206+
ProfileSection<FollowData>(
207+
title: 'Following',
208+
count: followingCount,
209+
items: following,
210+
emptyMessage: 'Not following anyone yet',
211+
itemBuilder: (follow) => FollowingListItem(
212+
follow: follow,
213+
onUnfollowPressed: () {
214+
onUnfollow(follow.targetFeed);
215+
},
216+
),
217+
),
218+
219+
// Follow Suggestions Section
220+
ProfileSection<FeedData>(
221+
title: 'Suggested',
222+
items: [
223+
if (!followIncludesCurrentUser && userState.feed != null)
224+
userState.feed!,
225+
...(followSuggestions ?? []),
168226
],
227+
emptyMessage: 'No suggestions available',
228+
itemBuilder: (suggestion) => SuggestionListItem(
229+
suggestion: suggestion,
230+
onFollowPressed: () {
231+
onFollow(suggestion);
232+
},
233+
),
169234
),
170-
);
171-
},
235+
],
236+
),
172237
);
173238
}
174239
}

0 commit comments

Comments
 (0)