Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add moderated communities to drawer #1063

Merged
merged 1 commit into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading