Skip to content

Commit

Permalink
Add moderated communities to drawer (#1063)
Browse files Browse the repository at this point in the history
  • Loading branch information
hjiangsu authored Jan 17, 2024
1 parent d4a5ac1 commit 226a382
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 80 deletions.
178 changes: 103 additions & 75 deletions lib/account/bloc/account_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,95 +14,123 @@ part 'account_event.dart';
part 'account_state.dart';

const throttleDuration = Duration(seconds: 1);
const timeout = Duration(seconds: 5);

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

class AccountBloc extends Bloc<AccountEvent, AccountState> {
AccountBloc() : super(const AccountState()) {
on<GetAccountInformation>((event, emit) async {
int attemptCount = 0;
on<RefreshAccountInformation>(
_refreshAccountInformation,
transformer: restartable(),
);

on<GetAccountInformation>(
_getAccountInformation,
transformer: restartable(),
);

on<GetAccountSubscriptions>(
_getAccountSubscriptions,
transformer: restartable(),
);

on<GetFavoritedCommunities>(
_getFavoritedCommunities,
transformer: restartable(),
);
}

bool hasFetchedAllSubsciptions = false;
int currentPage = 1;
Future<void> _refreshAccountInformation(RefreshAccountInformation event, Emitter<AccountState> 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<CommunityView> subsciptions = [];
List<CommunityView> 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<Favorite> 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<void> _getAccountInformation(GetAccountInformation event, Emitter<AccountState> 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<void> _getAccountSubscriptions(GetAccountSubscriptions event, Emitter<AccountState> 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<GetFavoritedCommunities>((event, emit) async {
Account? account = await fetchActiveProfileAccount();
LemmyApiV3 lemmy = LemmyClient.instance.lemmyApiV3;
List<CommunityView> 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<Favorite> favorites = await Favorite.favorites(account.id);
List<CommunityView> 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<void> _getFavoritedCommunities(GetFavoritedCommunities event, Emitter<AccountState> emit) async {
Account? account = await fetchActiveProfileAccount();

if (account == null || account.jwt == null) {
return emit(state.copyWith(status: AccountStatus.success));
}

List<Favorite> favorites = await Favorite.favorites(account.id);
List<CommunityView> 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));
}
}
4 changes: 4 additions & 0 deletions lib/account/bloc/account_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ abstract class AccountEvent extends Equatable {
List<Object> get props => [];
}

class RefreshAccountInformation extends AccountEvent {}

class GetAccountInformation extends AccountEvent {}

class GetAccountSubscriptions extends AccountEvent {}

class GetFavoritedCommunities extends AccountEvent {}
8 changes: 7 additions & 1 deletion lib/account/bloc/account_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand All @@ -20,25 +21,30 @@ class AccountState extends Equatable {
/// The user's favorites if logged in
final List<CommunityView> favorites;

/// The user's moderated communities
final List<CommunityModeratorView> moderates;

/// The user's information
final PersonView? personView;

AccountState copyWith({
AccountStatus? status,
List<CommunityView>? subsciptions,
List<CommunityView>? favorites,
List<CommunityModeratorView>? moderates,
PersonView? personView,
String? errorMessage,
}) {
return AccountState(
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<Object?> get props => [status, subsciptions, favorites, errorMessage];
List<Object?> get props => [status, subsciptions, favorites, moderates, personView, errorMessage];
}
75 changes: 74 additions & 1 deletion lib/community/widgets/community_drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ class CommunityDrawer extends StatefulWidget {
}

class _CommunityDrawerState extends State<CommunityDrawer> {
@override
void initState() {
super.initState();

context.read<AccountBloc>().add(GetAccountSubscriptions());
context.read<AccountBloc>().add(GetFavoritedCommunities());
}

@override
Widget build(BuildContext context) {
return Drawer(
Expand All @@ -45,6 +53,7 @@ class _CommunityDrawerState extends State<CommunityDrawer> {
children: [
FeedDrawerItems(),
FavoriteCommunities(),
ModeratedCommunities(),
SubscribedCommunities(),
],
),
Expand Down Expand Up @@ -255,8 +264,11 @@ class SubscribedCommunities extends StatelessWidget {

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

List<CommunityView> filteredSubscriptions = accountState.subsciptions.where((CommunityView communityView) => !favoriteCommunityIds.contains(communityView.community.id)).toList();
List<CommunityView> 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;
Expand Down Expand Up @@ -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<FeedBloc>().state;
AccountState accountState = context.watch<AccountBloc>().state;
ThunderState thunderState = context.read<ThunderBloc>().state;

List<CommunityModeratorView> 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<FeedBloc>().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);

Expand Down
1 change: 0 additions & 1 deletion lib/feed/utils/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ Future<void> navigateToFeedPage(BuildContext context, {required FeedType feedTyp
Future<void> triggerRefresh(BuildContext context) async {
FeedState state = context.read<FeedBloc>().state;

context.read<AccountBloc>().add(GetAccountInformation());
context.read<FeedBloc>().add(
FeedFetchedEvent(
feedType: state.feedType,
Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion lib/search/pages/search_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ class _SearchPageState extends State<SearchPage> with AutomaticKeepAliveClientMi
SubscribedType subscriptionStatus = _getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions);
_onSubscribeIconPressed(isUserLoggedIn, context, communityView);
showSnackbar(context, subscriptionStatus == SubscribedType.notSubscribed ? l10n.addedCommunityToSubscriptions : l10n.removedCommunityFromSubscriptions);
context.read<AccountBloc>().add(GetAccountInformation());
context.read<AccountBloc>().add(GetAccountSubscriptions());
},
icon: Icon(
switch (_getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/thunder/pages/thunder_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ class _ThunderState extends State<Thunder> {
},
buildWhen: (previous, current) => current.status != AuthStatus.failure && current.status != AuthStatus.loading,
listener: (context, state) {
context.read<AccountBloc>().add(GetAccountInformation());
context.read<AccountBloc>().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<InboxBloc>().add(const GetInboxEvent(reset: true)));
Expand Down

0 comments on commit 226a382

Please sign in to comment.