From 226a382d25713ef796a5cdfb32618696f4e85239 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su <30667958+hjiangsu@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:11:37 -0800 Subject: [PATCH] Add moderated communities to drawer (#1063) --- lib/account/bloc/account_bloc.dart | 178 +++++++++++--------- lib/account/bloc/account_event.dart | 4 + lib/account/bloc/account_state.dart | 8 +- lib/community/widgets/community_drawer.dart | 75 ++++++++- lib/feed/utils/utils.dart | 1 - lib/l10n/app_en.arb | 4 + lib/search/pages/search_page.dart | 2 +- lib/thunder/pages/thunder_page.dart | 2 +- 8 files changed, 194 insertions(+), 80 deletions(-) diff --git a/lib/account/bloc/account_bloc.dart b/lib/account/bloc/account_bloc.dart index 0539059b7..dfd871a1a 100644 --- a/lib/account/bloc/account_bloc.dart +++ b/lib/account/bloc/account_bloc.dart @@ -14,7 +14,6 @@ part 'account_event.dart'; part 'account_state.dart'; const throttleDuration = Duration(seconds: 1); -const timeout = Duration(seconds: 5); EventTransformer throttleDroppable(Duration duration) { return (events, mapper) => droppable().call(events.throttle(duration), mapper); @@ -22,87 +21,116 @@ EventTransformer throttleDroppable(Duration duration) { class AccountBloc extends Bloc { AccountBloc() : super(const AccountState()) { - on((event, emit) async { - int attemptCount = 0; + on( + _refreshAccountInformation, + transformer: restartable(), + ); + + on( + _getAccountInformation, + transformer: restartable(), + ); + + on( + _getAccountSubscriptions, + transformer: restartable(), + ); + + on( + _getFavoritedCommunities, + transformer: restartable(), + ); + } - bool hasFetchedAllSubsciptions = false; - int currentPage = 1; + Future _refreshAccountInformation(RefreshAccountInformation event, Emitter emit) async { + add(GetAccountInformation()); + add(GetAccountSubscriptions()); + add(GetFavoritedCommunities()); + } - try { - var exception; - - Account? account = await fetchActiveProfileAccount(); - - while (attemptCount < 2) { - try { - LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; - emit(state.copyWith(status: AccountStatus.loading)); - - if (account == null || account.jwt == null) { - return emit(state.copyWith(status: AccountStatus.success, subsciptions: [], personView: null)); - } else { - emit(state.copyWith(status: AccountStatus.loading)); - } - - List subsciptions = []; - List favoritedCommunities = []; - - while (!hasFetchedAllSubsciptions) { - ListCommunitiesResponse listCommunitiesResponse = await lemmy.run( - ListCommunities( - auth: account.jwt, - page: currentPage, - type: ListingType.subscribed, - limit: 50, // Temporarily increasing this to address issue of missing subscriptions - ), - ); - - subsciptions.addAll(listCommunitiesResponse.communities); - currentPage++; - hasFetchedAllSubsciptions = listCommunitiesResponse.communities.isEmpty; - } - - // Sort subscriptions by their name - subsciptions.sort((CommunityView a, CommunityView b) => a.community.title.toLowerCase().compareTo(b.community.title.toLowerCase())); - - List favorites = await Favorite.favorites(account.id); - favoritedCommunities = subsciptions.where((CommunityView communityView) => favorites.any((Favorite favorite) => favorite.communityId == communityView.community.id)).toList(); - - GetPersonDetailsResponse? getPersonDetailsResponse = - await lemmy.run(GetPersonDetails(username: account.username, auth: account.jwt, sort: SortType.new_, page: 1)).timeout(timeout, onTimeout: () { - throw Exception('Error: Timeout when attempting to fetch account details'); - }); - - // This eliminates an issue which has plagued me a lot which is that there's a race condition - // with so many calls to GetAccountInformation, we can return success for the new and old account. - if (getPersonDetailsResponse.personView.person.id == (await fetchActiveProfileAccount())?.userId) { - return emit(state.copyWith(status: AccountStatus.success, subsciptions: subsciptions, favorites: favoritedCommunities, personView: getPersonDetailsResponse.personView)); - } else { - return emit(state.copyWith(status: AccountStatus.success)); - } - } catch (e) { - exception = e; - attemptCount++; - } - } - emit(state.copyWith(status: AccountStatus.failure, errorMessage: exception.toString())); - } catch (e) { - emit(state.copyWith(status: AccountStatus.failure, errorMessage: e.toString())); + /// Fetches the current account's information. This updates [personView] which holds moderated community information. + Future _getAccountInformation(GetAccountInformation event, Emitter emit) async { + Account? account = await fetchActiveProfileAccount(); + + if (account == null || account.jwt == null) { + return emit(state.copyWith(status: AccountStatus.success, personView: null, moderates: [])); + } + + try { + emit(state.copyWith(status: AccountStatus.loading)); + LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; + + GetPersonDetailsResponse? getPersonDetailsResponse = await lemmy.run(GetPersonDetails( + username: account.username, + auth: account.jwt, + sort: SortType.new_, + page: 1, + )); + + // This eliminates an issue which has plagued me a lot which is that there's a race condition + // with so many calls to GetAccountInformation, we can return success for the new and old account. + if (getPersonDetailsResponse?.personView.person.id == account.userId) { + return emit(state.copyWith(status: AccountStatus.success, personView: getPersonDetailsResponse?.personView, moderates: getPersonDetailsResponse?.moderates)); + } else { + return emit(state.copyWith(status: AccountStatus.success, personView: null)); } - }); + } catch (e) { + emit(state.copyWith(status: AccountStatus.failure, errorMessage: e.toString())); + } + } + + /// Fetches the current account's subscriptions. + Future _getAccountSubscriptions(GetAccountSubscriptions event, Emitter emit) async { + Account? account = await fetchActiveProfileAccount(); + + if (account == null || account.jwt == null) { + return emit(state.copyWith(status: AccountStatus.success, subsciptions: [], personView: null)); + } + + try { + emit(state.copyWith(status: AccountStatus.loading)); - on((event, emit) async { - Account? account = await fetchActiveProfileAccount(); + LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3; + List subscriptions = []; - if (account == null || account.jwt == null) { - return emit(state.copyWith(status: AccountStatus.success)); + int currentPage = 1; + bool hasFetchedAllSubsciptions = false; + + while (!hasFetchedAllSubsciptions) { + ListCommunitiesResponse listCommunitiesResponse = await lemmy.run( + ListCommunities( + auth: account.jwt, + page: currentPage, + type: ListingType.subscribed, + limit: 50, // Temporarily increasing this to address issue of missing subscriptions + ), + ); + + subscriptions.addAll(listCommunitiesResponse.communities); + currentPage++; + hasFetchedAllSubsciptions = listCommunitiesResponse.communities.isEmpty; } - List favorites = await Favorite.favorites(account.id); - List favoritedCommunities = - state.subsciptions.where((CommunityView communityView) => favorites.any((Favorite favorite) => favorite.communityId == communityView.community.id)).toList(); + // Sort subscriptions by their name + subscriptions.sort((CommunityView a, CommunityView b) => a.community.title.toLowerCase().compareTo(b.community.title.toLowerCase())); + return emit(state.copyWith(status: AccountStatus.success, subsciptions: subscriptions)); + } catch (e) { + emit(state.copyWith(status: AccountStatus.failure, errorMessage: e.toString())); + } + } + + /// Fetches the current account's favorited communities. + Future _getFavoritedCommunities(GetFavoritedCommunities event, Emitter emit) async { + Account? account = await fetchActiveProfileAccount(); + + if (account == null || account.jwt == null) { + return emit(state.copyWith(status: AccountStatus.success)); + } + + List favorites = await Favorite.favorites(account.id); + List favoritedCommunities = + state.subsciptions.where((CommunityView communityView) => favorites.any((Favorite favorite) => favorite.communityId == communityView.community.id)).toList(); - emit(state.copyWith(status: AccountStatus.success, favorites: favoritedCommunities)); - }); + return emit(state.copyWith(status: AccountStatus.success, favorites: favoritedCommunities)); } } diff --git a/lib/account/bloc/account_event.dart b/lib/account/bloc/account_event.dart index 0a88d0447..11f4dca84 100644 --- a/lib/account/bloc/account_event.dart +++ b/lib/account/bloc/account_event.dart @@ -7,6 +7,10 @@ abstract class AccountEvent extends Equatable { List get props => []; } +class RefreshAccountInformation extends AccountEvent {} + class GetAccountInformation extends AccountEvent {} +class GetAccountSubscriptions extends AccountEvent {} + class GetFavoritedCommunities extends AccountEvent {} diff --git a/lib/account/bloc/account_state.dart b/lib/account/bloc/account_state.dart index 047644ed6..c9a6544b3 100644 --- a/lib/account/bloc/account_state.dart +++ b/lib/account/bloc/account_state.dart @@ -7,6 +7,7 @@ class AccountState extends Equatable { this.status = AccountStatus.initial, this.subsciptions = const [], this.favorites = const [], + this.moderates = const [], this.personView, this.errorMessage, }); @@ -20,6 +21,9 @@ class AccountState extends Equatable { /// The user's favorites if logged in final List favorites; + /// The user's moderated communities + final List moderates; + /// The user's information final PersonView? personView; @@ -27,6 +31,7 @@ class AccountState extends Equatable { AccountStatus? status, List? subsciptions, List? favorites, + List? moderates, PersonView? personView, String? errorMessage, }) { @@ -34,11 +39,12 @@ class AccountState extends Equatable { status: status ?? this.status, subsciptions: subsciptions ?? this.subsciptions, favorites: favorites ?? this.favorites, + moderates: moderates ?? this.moderates, personView: personView ?? this.personView, errorMessage: errorMessage ?? this.errorMessage, ); } @override - List get props => [status, subsciptions, favorites, errorMessage]; + List get props => [status, subsciptions, favorites, moderates, personView, errorMessage]; } diff --git a/lib/community/widgets/community_drawer.dart b/lib/community/widgets/community_drawer.dart index 9b4c4189b..b8f830846 100644 --- a/lib/community/widgets/community_drawer.dart +++ b/lib/community/widgets/community_drawer.dart @@ -29,6 +29,14 @@ class CommunityDrawer extends StatefulWidget { } class _CommunityDrawerState extends State { + @override + void initState() { + super.initState(); + + context.read().add(GetAccountSubscriptions()); + context.read().add(GetFavoritedCommunities()); + } + @override Widget build(BuildContext context) { return Drawer( @@ -45,6 +53,7 @@ class _CommunityDrawerState extends State { children: [ FeedDrawerItems(), FavoriteCommunities(), + ModeratedCommunities(), SubscribedCommunities(), ], ), @@ -255,8 +264,11 @@ class SubscribedCommunities extends StatelessWidget { if (isLoggedIn) { Set favoriteCommunityIds = accountState.favorites.map((cv) => cv.community.id).toSet(); + Set moderatedCommunityIds = accountState.moderates.map((cmv) => cmv.community.id).toSet(); - List filteredSubscriptions = accountState.subsciptions.where((CommunityView communityView) => !favoriteCommunityIds.contains(communityView.community.id)).toList(); + List filteredSubscriptions = accountState.subsciptions + .where((CommunityView communityView) => !favoriteCommunityIds.contains(communityView.community.id) && !moderatedCommunityIds.contains(communityView.community.id)) + .toList(); subscriptions = filteredSubscriptions.map((CommunityView communityView) => communityView.community).toList(); } else { subscriptions = subscriptionsBloc.state.subscriptions; @@ -318,6 +330,67 @@ class SubscribedCommunities extends StatelessWidget { } } +class ModeratedCommunities extends StatelessWidget { + const ModeratedCommunities({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + + FeedState feedState = context.watch().state; + AccountState accountState = context.watch().state; + ThunderState thunderState = context.read().state; + + List moderatedCommunities = accountState.moderates; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (moderatedCommunities.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(28, 16, 16, 8.0), + child: Text(l10n.moderatedCommunities, style: theme.textTheme.titleSmall), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 14.0), + child: ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: moderatedCommunities.length, + itemBuilder: (context, index) { + Community community = moderatedCommunities[index].community; + + final bool isCommunitySelected = feedState.communityId == community.id; + + return TextButton( + style: TextButton.styleFrom( + alignment: Alignment.centerLeft, + minimumSize: const Size.fromHeight(50), + backgroundColor: isCommunitySelected ? theme.colorScheme.primaryContainer.withOpacity(0.25) : Colors.transparent, + ), + onPressed: () { + Navigator.of(context).pop(); + context.read().add( + FeedFetchedEvent( + feedType: FeedType.community, + sortType: thunderState.defaultSortType, + communityId: community.id, + reset: true, + ), + ); + }, + child: CommunityItem(community: community, showFavoriteAction: false, isFavorite: false), + ); + }, + ), + ), + ], + ], + ); + } +} + class Destination { const Destination(this.label, this.listingType, this.icon); diff --git a/lib/feed/utils/utils.dart b/lib/feed/utils/utils.dart index 6171cbef6..35872210a 100644 --- a/lib/feed/utils/utils.dart +++ b/lib/feed/utils/utils.dart @@ -111,7 +111,6 @@ Future navigateToFeedPage(BuildContext context, {required FeedType feedTyp Future triggerRefresh(BuildContext context) async { FeedState state = context.read().state; - context.read().add(GetAccountInformation()); context.read().add( FeedFetchedEvent( feedType: state.feedType, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b7cfbeadd..02a5687e4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -649,6 +649,10 @@ }, "missingErrorMessage": "No error message available", "@missingErrorMessage": {}, + "moderatedCommunities": "Moderated Communities", + "@moderatedCommunities": { + "description": "Describes a list of communities that are moderated by the current user." + }, "mostComments": "Most Comments", "@mostComments": {}, "mustBeLoggedInComment": "You need to be logged in to comment", diff --git a/lib/search/pages/search_page.dart b/lib/search/pages/search_page.dart index 5b7182522..0209a0d5a 100644 --- a/lib/search/pages/search_page.dart +++ b/lib/search/pages/search_page.dart @@ -743,7 +743,7 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi SubscribedType subscriptionStatus = _getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions); _onSubscribeIconPressed(isUserLoggedIn, context, communityView); showSnackbar(context, subscriptionStatus == SubscribedType.notSubscribed ? l10n.addedCommunityToSubscriptions : l10n.removedCommunityFromSubscriptions); - context.read().add(GetAccountInformation()); + context.read().add(GetAccountSubscriptions()); }, icon: Icon( switch (_getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions)) { diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index c9d744612..48c5b0ae1 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -483,7 +483,7 @@ class _ThunderState extends State { }, buildWhen: (previous, current) => current.status != AuthStatus.failure && current.status != AuthStatus.loading, listener: (context, state) { - context.read().add(GetAccountInformation()); + context.read().add(RefreshAccountInformation()); // Add a bit of artificial delay to allow preferences to set the proper active profile Future.delayed(const Duration(milliseconds: 500), () => context.read().add(const GetInboxEvent(reset: true)));