Skip to content

Commit 226a382

Browse files
authored
Add moderated communities to drawer (#1063)
1 parent d4a5ac1 commit 226a382

8 files changed

Lines changed: 194 additions & 80 deletions

File tree

lib/account/bloc/account_bloc.dart

Lines changed: 103 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -14,95 +14,123 @@ part 'account_event.dart';
1414
part 'account_state.dart';
1515

1616
const throttleDuration = Duration(seconds: 1);
17-
const timeout = Duration(seconds: 5);
1817

1918
EventTransformer<E> throttleDroppable<E>(Duration duration) {
2019
return (events, mapper) => droppable<E>().call(events.throttle(duration), mapper);
2120
}
2221

2322
class AccountBloc extends Bloc<AccountEvent, AccountState> {
2423
AccountBloc() : super(const AccountState()) {
25-
on<GetAccountInformation>((event, emit) async {
26-
int attemptCount = 0;
24+
on<RefreshAccountInformation>(
25+
_refreshAccountInformation,
26+
transformer: restartable(),
27+
);
28+
29+
on<GetAccountInformation>(
30+
_getAccountInformation,
31+
transformer: restartable(),
32+
);
33+
34+
on<GetAccountSubscriptions>(
35+
_getAccountSubscriptions,
36+
transformer: restartable(),
37+
);
38+
39+
on<GetFavoritedCommunities>(
40+
_getFavoritedCommunities,
41+
transformer: restartable(),
42+
);
43+
}
2744

28-
bool hasFetchedAllSubsciptions = false;
29-
int currentPage = 1;
45+
Future<void> _refreshAccountInformation(RefreshAccountInformation event, Emitter<AccountState> emit) async {
46+
add(GetAccountInformation());
47+
add(GetAccountSubscriptions());
48+
add(GetFavoritedCommunities());
49+
}
3050

31-
try {
32-
var exception;
33-
34-
Account? account = await fetchActiveProfileAccount();
35-
36-
while (attemptCount < 2) {
37-
try {
38-
LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3;
39-
emit(state.copyWith(status: AccountStatus.loading));
40-
41-
if (account == null || account.jwt == null) {
42-
return emit(state.copyWith(status: AccountStatus.success, subsciptions: [], personView: null));
43-
} else {
44-
emit(state.copyWith(status: AccountStatus.loading));
45-
}
46-
47-
List<CommunityView> subsciptions = [];
48-
List<CommunityView> favoritedCommunities = [];
49-
50-
while (!hasFetchedAllSubsciptions) {
51-
ListCommunitiesResponse listCommunitiesResponse = await lemmy.run(
52-
ListCommunities(
53-
auth: account.jwt,
54-
page: currentPage,
55-
type: ListingType.subscribed,
56-
limit: 50, // Temporarily increasing this to address issue of missing subscriptions
57-
),
58-
);
59-
60-
subsciptions.addAll(listCommunitiesResponse.communities);
61-
currentPage++;
62-
hasFetchedAllSubsciptions = listCommunitiesResponse.communities.isEmpty;
63-
}
64-
65-
// Sort subscriptions by their name
66-
subsciptions.sort((CommunityView a, CommunityView b) => a.community.title.toLowerCase().compareTo(b.community.title.toLowerCase()));
67-
68-
List<Favorite> favorites = await Favorite.favorites(account.id);
69-
favoritedCommunities = subsciptions.where((CommunityView communityView) => favorites.any((Favorite favorite) => favorite.communityId == communityView.community.id)).toList();
70-
71-
GetPersonDetailsResponse? getPersonDetailsResponse =
72-
await lemmy.run(GetPersonDetails(username: account.username, auth: account.jwt, sort: SortType.new_, page: 1)).timeout(timeout, onTimeout: () {
73-
throw Exception('Error: Timeout when attempting to fetch account details');
74-
});
75-
76-
// This eliminates an issue which has plagued me a lot which is that there's a race condition
77-
// with so many calls to GetAccountInformation, we can return success for the new and old account.
78-
if (getPersonDetailsResponse.personView.person.id == (await fetchActiveProfileAccount())?.userId) {
79-
return emit(state.copyWith(status: AccountStatus.success, subsciptions: subsciptions, favorites: favoritedCommunities, personView: getPersonDetailsResponse.personView));
80-
} else {
81-
return emit(state.copyWith(status: AccountStatus.success));
82-
}
83-
} catch (e) {
84-
exception = e;
85-
attemptCount++;
86-
}
87-
}
88-
emit(state.copyWith(status: AccountStatus.failure, errorMessage: exception.toString()));
89-
} catch (e) {
90-
emit(state.copyWith(status: AccountStatus.failure, errorMessage: e.toString()));
51+
/// Fetches the current account's information. This updates [personView] which holds moderated community information.
52+
Future<void> _getAccountInformation(GetAccountInformation event, Emitter<AccountState> emit) async {
53+
Account? account = await fetchActiveProfileAccount();
54+
55+
if (account == null || account.jwt == null) {
56+
return emit(state.copyWith(status: AccountStatus.success, personView: null, moderates: []));
57+
}
58+
59+
try {
60+
emit(state.copyWith(status: AccountStatus.loading));
61+
LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3;
62+
63+
GetPersonDetailsResponse? getPersonDetailsResponse = await lemmy.run(GetPersonDetails(
64+
username: account.username,
65+
auth: account.jwt,
66+
sort: SortType.new_,
67+
page: 1,
68+
));
69+
70+
// This eliminates an issue which has plagued me a lot which is that there's a race condition
71+
// with so many calls to GetAccountInformation, we can return success for the new and old account.
72+
if (getPersonDetailsResponse?.personView.person.id == account.userId) {
73+
return emit(state.copyWith(status: AccountStatus.success, personView: getPersonDetailsResponse?.personView, moderates: getPersonDetailsResponse?.moderates));
74+
} else {
75+
return emit(state.copyWith(status: AccountStatus.success, personView: null));
9176
}
92-
});
77+
} catch (e) {
78+
emit(state.copyWith(status: AccountStatus.failure, errorMessage: e.toString()));
79+
}
80+
}
81+
82+
/// Fetches the current account's subscriptions.
83+
Future<void> _getAccountSubscriptions(GetAccountSubscriptions event, Emitter<AccountState> emit) async {
84+
Account? account = await fetchActiveProfileAccount();
85+
86+
if (account == null || account.jwt == null) {
87+
return emit(state.copyWith(status: AccountStatus.success, subsciptions: [], personView: null));
88+
}
89+
90+
try {
91+
emit(state.copyWith(status: AccountStatus.loading));
9392

94-
on<GetFavoritedCommunities>((event, emit) async {
95-
Account? account = await fetchActiveProfileAccount();
93+
LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3;
94+
List<CommunityView> subscriptions = [];
9695

97-
if (account == null || account.jwt == null) {
98-
return emit(state.copyWith(status: AccountStatus.success));
96+
int currentPage = 1;
97+
bool hasFetchedAllSubsciptions = false;
98+
99+
while (!hasFetchedAllSubsciptions) {
100+
ListCommunitiesResponse listCommunitiesResponse = await lemmy.run(
101+
ListCommunities(
102+
auth: account.jwt,
103+
page: currentPage,
104+
type: ListingType.subscribed,
105+
limit: 50, // Temporarily increasing this to address issue of missing subscriptions
106+
),
107+
);
108+
109+
subscriptions.addAll(listCommunitiesResponse.communities);
110+
currentPage++;
111+
hasFetchedAllSubsciptions = listCommunitiesResponse.communities.isEmpty;
99112
}
100113

101-
List<Favorite> favorites = await Favorite.favorites(account.id);
102-
List<CommunityView> favoritedCommunities =
103-
state.subsciptions.where((CommunityView communityView) => favorites.any((Favorite favorite) => favorite.communityId == communityView.community.id)).toList();
114+
// Sort subscriptions by their name
115+
subscriptions.sort((CommunityView a, CommunityView b) => a.community.title.toLowerCase().compareTo(b.community.title.toLowerCase()));
116+
return emit(state.copyWith(status: AccountStatus.success, subsciptions: subscriptions));
117+
} catch (e) {
118+
emit(state.copyWith(status: AccountStatus.failure, errorMessage: e.toString()));
119+
}
120+
}
121+
122+
/// Fetches the current account's favorited communities.
123+
Future<void> _getFavoritedCommunities(GetFavoritedCommunities event, Emitter<AccountState> emit) async {
124+
Account? account = await fetchActiveProfileAccount();
125+
126+
if (account == null || account.jwt == null) {
127+
return emit(state.copyWith(status: AccountStatus.success));
128+
}
129+
130+
List<Favorite> favorites = await Favorite.favorites(account.id);
131+
List<CommunityView> favoritedCommunities =
132+
state.subsciptions.where((CommunityView communityView) => favorites.any((Favorite favorite) => favorite.communityId == communityView.community.id)).toList();
104133

105-
emit(state.copyWith(status: AccountStatus.success, favorites: favoritedCommunities));
106-
});
134+
return emit(state.copyWith(status: AccountStatus.success, favorites: favoritedCommunities));
107135
}
108136
}

lib/account/bloc/account_event.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ abstract class AccountEvent extends Equatable {
77
List<Object> get props => [];
88
}
99

10+
class RefreshAccountInformation extends AccountEvent {}
11+
1012
class GetAccountInformation extends AccountEvent {}
1113

14+
class GetAccountSubscriptions extends AccountEvent {}
15+
1216
class GetFavoritedCommunities extends AccountEvent {}

lib/account/bloc/account_state.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class AccountState extends Equatable {
77
this.status = AccountStatus.initial,
88
this.subsciptions = const [],
99
this.favorites = const [],
10+
this.moderates = const [],
1011
this.personView,
1112
this.errorMessage,
1213
});
@@ -20,25 +21,30 @@ class AccountState extends Equatable {
2021
/// The user's favorites if logged in
2122
final List<CommunityView> favorites;
2223

24+
/// The user's moderated communities
25+
final List<CommunityModeratorView> moderates;
26+
2327
/// The user's information
2428
final PersonView? personView;
2529

2630
AccountState copyWith({
2731
AccountStatus? status,
2832
List<CommunityView>? subsciptions,
2933
List<CommunityView>? favorites,
34+
List<CommunityModeratorView>? moderates,
3035
PersonView? personView,
3136
String? errorMessage,
3237
}) {
3338
return AccountState(
3439
status: status ?? this.status,
3540
subsciptions: subsciptions ?? this.subsciptions,
3641
favorites: favorites ?? this.favorites,
42+
moderates: moderates ?? this.moderates,
3743
personView: personView ?? this.personView,
3844
errorMessage: errorMessage ?? this.errorMessage,
3945
);
4046
}
4147

4248
@override
43-
List<Object?> get props => [status, subsciptions, favorites, errorMessage];
49+
List<Object?> get props => [status, subsciptions, favorites, moderates, personView, errorMessage];
4450
}

lib/community/widgets/community_drawer.dart

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ class CommunityDrawer extends StatefulWidget {
2929
}
3030

3131
class _CommunityDrawerState extends State<CommunityDrawer> {
32+
@override
33+
void initState() {
34+
super.initState();
35+
36+
context.read<AccountBloc>().add(GetAccountSubscriptions());
37+
context.read<AccountBloc>().add(GetFavoritedCommunities());
38+
}
39+
3240
@override
3341
Widget build(BuildContext context) {
3442
return Drawer(
@@ -45,6 +53,7 @@ class _CommunityDrawerState extends State<CommunityDrawer> {
4553
children: [
4654
FeedDrawerItems(),
4755
FavoriteCommunities(),
56+
ModeratedCommunities(),
4857
SubscribedCommunities(),
4958
],
5059
),
@@ -255,8 +264,11 @@ class SubscribedCommunities extends StatelessWidget {
255264

256265
if (isLoggedIn) {
257266
Set<int> favoriteCommunityIds = accountState.favorites.map((cv) => cv.community.id).toSet();
267+
Set<int> moderatedCommunityIds = accountState.moderates.map((cmv) => cmv.community.id).toSet();
258268

259-
List<CommunityView> filteredSubscriptions = accountState.subsciptions.where((CommunityView communityView) => !favoriteCommunityIds.contains(communityView.community.id)).toList();
269+
List<CommunityView> filteredSubscriptions = accountState.subsciptions
270+
.where((CommunityView communityView) => !favoriteCommunityIds.contains(communityView.community.id) && !moderatedCommunityIds.contains(communityView.community.id))
271+
.toList();
260272
subscriptions = filteredSubscriptions.map((CommunityView communityView) => communityView.community).toList();
261273
} else {
262274
subscriptions = subscriptionsBloc.state.subscriptions;
@@ -318,6 +330,67 @@ class SubscribedCommunities extends StatelessWidget {
318330
}
319331
}
320332

333+
class ModeratedCommunities extends StatelessWidget {
334+
const ModeratedCommunities({super.key});
335+
336+
@override
337+
Widget build(BuildContext context) {
338+
final theme = Theme.of(context);
339+
final l10n = AppLocalizations.of(context)!;
340+
341+
FeedState feedState = context.watch<FeedBloc>().state;
342+
AccountState accountState = context.watch<AccountBloc>().state;
343+
ThunderState thunderState = context.read<ThunderBloc>().state;
344+
345+
List<CommunityModeratorView> moderatedCommunities = accountState.moderates;
346+
347+
return Column(
348+
crossAxisAlignment: CrossAxisAlignment.start,
349+
children: [
350+
if (moderatedCommunities.isNotEmpty) ...[
351+
Padding(
352+
padding: const EdgeInsets.fromLTRB(28, 16, 16, 8.0),
353+
child: Text(l10n.moderatedCommunities, style: theme.textTheme.titleSmall),
354+
),
355+
Padding(
356+
padding: const EdgeInsets.symmetric(horizontal: 14.0),
357+
child: ListView.builder(
358+
shrinkWrap: true,
359+
physics: const NeverScrollableScrollPhysics(),
360+
itemCount: moderatedCommunities.length,
361+
itemBuilder: (context, index) {
362+
Community community = moderatedCommunities[index].community;
363+
364+
final bool isCommunitySelected = feedState.communityId == community.id;
365+
366+
return TextButton(
367+
style: TextButton.styleFrom(
368+
alignment: Alignment.centerLeft,
369+
minimumSize: const Size.fromHeight(50),
370+
backgroundColor: isCommunitySelected ? theme.colorScheme.primaryContainer.withOpacity(0.25) : Colors.transparent,
371+
),
372+
onPressed: () {
373+
Navigator.of(context).pop();
374+
context.read<FeedBloc>().add(
375+
FeedFetchedEvent(
376+
feedType: FeedType.community,
377+
sortType: thunderState.defaultSortType,
378+
communityId: community.id,
379+
reset: true,
380+
),
381+
);
382+
},
383+
child: CommunityItem(community: community, showFavoriteAction: false, isFavorite: false),
384+
);
385+
},
386+
),
387+
),
388+
],
389+
],
390+
);
391+
}
392+
}
393+
321394
class Destination {
322395
const Destination(this.label, this.listingType, this.icon);
323396

lib/feed/utils/utils.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ Future<void> navigateToFeedPage(BuildContext context, {required FeedType feedTyp
111111
Future<void> triggerRefresh(BuildContext context) async {
112112
FeedState state = context.read<FeedBloc>().state;
113113

114-
context.read<AccountBloc>().add(GetAccountInformation());
115114
context.read<FeedBloc>().add(
116115
FeedFetchedEvent(
117116
feedType: state.feedType,

lib/l10n/app_en.arb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,10 @@
649649
},
650650
"missingErrorMessage": "No error message available",
651651
"@missingErrorMessage": {},
652+
"moderatedCommunities": "Moderated Communities",
653+
"@moderatedCommunities": {
654+
"description": "Describes a list of communities that are moderated by the current user."
655+
},
652656
"mostComments": "Most Comments",
653657
"@mostComments": {},
654658
"mustBeLoggedInComment": "You need to be logged in to comment",

lib/search/pages/search_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ class _SearchPageState extends State<SearchPage> with AutomaticKeepAliveClientMi
743743
SubscribedType subscriptionStatus = _getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions);
744744
_onSubscribeIconPressed(isUserLoggedIn, context, communityView);
745745
showSnackbar(context, subscriptionStatus == SubscribedType.notSubscribed ? l10n.addedCommunityToSubscriptions : l10n.removedCommunityFromSubscriptions);
746-
context.read<AccountBloc>().add(GetAccountInformation());
746+
context.read<AccountBloc>().add(GetAccountSubscriptions());
747747
},
748748
icon: Icon(
749749
switch (_getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions)) {

lib/thunder/pages/thunder_page.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ class _ThunderState extends State<Thunder> {
483483
},
484484
buildWhen: (previous, current) => current.status != AuthStatus.failure && current.status != AuthStatus.loading,
485485
listener: (context, state) {
486-
context.read<AccountBloc>().add(GetAccountInformation());
486+
context.read<AccountBloc>().add(RefreshAccountInformation());
487487

488488
// Add a bit of artificial delay to allow preferences to set the proper active profile
489489
Future.delayed(const Duration(milliseconds: 500), () => context.read<InboxBloc>().add(const GetInboxEvent(reset: true)));

0 commit comments

Comments
 (0)