Skip to content

Commit

Permalink
User navigation fix and error handling (thunder-app#1014)
Browse files Browse the repository at this point in the history
* User navigation fix and error handling

* Add new feed state
  • Loading branch information
micahmo authored Jan 8, 2024
1 parent 395126c commit 1a5d6ba
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 239 deletions.
14 changes: 12 additions & 2 deletions lib/feed/bloc/feed_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:thunder/feed/utils/post.dart';
import 'package:thunder/feed/view/feed_page.dart';
import 'package:thunder/post/enums/post_action.dart';
import 'package:thunder/post/utils/post.dart';
import 'package:thunder/utils/error_messages.dart';

part 'feed_event.dart';
part 'feed_state.dart';
Expand Down Expand Up @@ -120,7 +121,7 @@ class FeedBloc extends Bloc<FeedEvent, FeedState> {

/// Handles clearing any messages from the state
Future<void> _onFeedClearMessage(FeedClearMessageEvent event, Emitter<FeedState> emit) async {
emit(state.copyWith(status: FeedStatus.success, message: null));
emit(state.copyWith(status: state.status == FeedStatus.failureLoadingCommunity ? state.status : FeedStatus.success, message: null));
}

/// Handles post related actions on a given item within the feed
Expand Down Expand Up @@ -298,7 +299,16 @@ class FeedBloc extends Bloc<FeedEvent, FeedState> {
switch (event.feedType) {
case FeedType.community:
// Fetch community information
fullCommunityView = await fetchCommunityInformation(id: event.communityId, name: event.communityName);
try {
fullCommunityView = await fetchCommunityInformation(id: event.communityId, name: event.communityName);
} catch (e) {
// If we are given a community feed, but we can't load the community, that's a problem! Emit an error.
return emit(state.copyWith(
status: FeedStatus.failureLoadingCommunity,
message: getExceptionErrorMessage(e, additionalInfo: event.communityName),
feedType: event.feedType,
));
}
break;
case FeedType.user:
// Fetch user information
Expand Down
2 changes: 1 addition & 1 deletion lib/feed/bloc/feed_state.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
part of 'feed_bloc.dart';

enum FeedStatus { initial, fetching, success, failure }
enum FeedStatus { initial, fetching, success, failure, failureLoadingCommunity }

final class FeedState extends Equatable {
const FeedState({
Expand Down
315 changes: 163 additions & 152 deletions lib/feed/view/feed_page.dart

Large diffs are not rendered by default.

164 changes: 83 additions & 81 deletions lib/feed/widgets/feed_page_app_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,91 +53,93 @@ class FeedPageAppBar extends StatelessWidget {
(Navigator.of(context).canPop() && feedBloc.state.feedType == FeedType.community) ? Navigator.of(context).maybePop() : Scaffold.of(context).openDrawer();
},
),
actions: [
if (feedState.feedType == FeedType.community) ...[
BlocListener<CommunityBloc, CommunityState>(
listener: (context, state) {
if (state.status == CommunityStatus.success && state.communityView != null) {
feedBloc.add(FeedCommunityViewUpdatedEvent(communityView: state.communityView!));
}
},
child: IconButton(
icon: Icon(
switch (_getSubscriptionStatus(context)) {
SubscribedType.notSubscribed => Icons.add_circle_outline_rounded,
SubscribedType.pending => Icons.pending_outlined,
SubscribedType.subscribed => Icons.remove_circle_outline_rounded,
_ => Icons.add_circle_outline_rounded,
actions: feedState.status != FeedStatus.failureLoadingCommunity
? [
if (feedState.feedType == FeedType.community) ...[
BlocListener<CommunityBloc, CommunityState>(
listener: (context, state) {
if (state.status == CommunityStatus.success && state.communityView != null) {
feedBloc.add(FeedCommunityViewUpdatedEvent(communityView: state.communityView!));
}
},
semanticLabel: (_getSubscriptionStatus(context) == SubscribedType.notSubscribed) ? AppLocalizations.of(context)!.subscribe : AppLocalizations.of(context)!.unsubscribe),
tooltip: switch (_getSubscriptionStatus(context)) {
SubscribedType.notSubscribed => AppLocalizations.of(context)!.subscribe,
SubscribedType.pending => AppLocalizations.of(context)!.unsubscribePending,
SubscribedType.subscribed => AppLocalizations.of(context)!.unsubscribe,
_ => null,
},
onPressed: () {
if (thunderBloc.state.isFabOpen) thunderBloc.add(const OnFabToggle(false));

HapticFeedback.mediumImpact();
_onSubscribeIconPressed(context);
},
),
),
],
if (feedState.feedType != FeedType.community)
IconButton(
onPressed: () {
HapticFeedback.mediumImpact();
triggerRefresh(context);
},
icon: Icon(Icons.refresh_rounded, semanticLabel: l10n.refresh)),
IconButton(
icon: Icon(Icons.sort, semanticLabel: l10n.sortBy),
onPressed: () {
HapticFeedback.mediumImpact();

showModalBottomSheet<void>(
showDragHandle: true,
context: context,
isScrollControlled: true,
builder: (builderContext) => SortPicker(
title: l10n.sortOptions,
onSelect: (selected) => feedBloc.add(FeedChangeSortTypeEvent(selected.payload)),
previouslySelected: feedBloc.state.sortType,
),
);
},
),
if (feedState.feedType == FeedType.community)
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
onTap: () => triggerRefresh(context),
child: ListTile(
dense: true,
horizontalTitleGap: 5,
leading: const Icon(Icons.refresh_rounded, size: 20),
title: Text(l10n.refresh),
child: IconButton(
icon: Icon(
switch (_getSubscriptionStatus(context)) {
SubscribedType.notSubscribed => Icons.add_circle_outline_rounded,
SubscribedType.pending => Icons.pending_outlined,
SubscribedType.subscribed => Icons.remove_circle_outline_rounded,
_ => Icons.add_circle_outline_rounded,
},
semanticLabel: (_getSubscriptionStatus(context) == SubscribedType.notSubscribed) ? AppLocalizations.of(context)!.subscribe : AppLocalizations.of(context)!.unsubscribe),
tooltip: switch (_getSubscriptionStatus(context)) {
SubscribedType.notSubscribed => AppLocalizations.of(context)!.subscribe,
SubscribedType.pending => AppLocalizations.of(context)!.unsubscribePending,
SubscribedType.subscribed => AppLocalizations.of(context)!.unsubscribe,
_ => null,
},
onPressed: () {
if (thunderBloc.state.isFabOpen) thunderBloc.add(const OnFabToggle(false));

HapticFeedback.mediumImpact();
_onSubscribeIconPressed(context);
},
),
),
],
if (feedState.feedType != FeedType.community)
IconButton(
onPressed: () {
HapticFeedback.mediumImpact();
triggerRefresh(context);
},
icon: Icon(Icons.refresh_rounded, semanticLabel: l10n.refresh)),
IconButton(
icon: Icon(Icons.sort, semanticLabel: l10n.sortBy),
onPressed: () {
HapticFeedback.mediumImpact();

showModalBottomSheet<void>(
showDragHandle: true,
context: context,
isScrollControlled: true,
builder: (builderContext) => SortPicker(
title: l10n.sortOptions,
onSelect: (selected) => feedBloc.add(FeedChangeSortTypeEvent(selected.payload)),
previouslySelected: feedBloc.state.sortType,
),
);
},
),
if (_getSubscriptionStatus(context) == SubscribedType.subscribed)
PopupMenuItem(
onTap: () async {
final Community community = context.read<FeedBloc>().state.fullCommunityView!.communityView.community;
bool isFavorite = _getFavoriteStatus(context);
await toggleFavoriteCommunity(context, community, isFavorite);
},
child: ListTile(
dense: true,
horizontalTitleGap: 5,
leading: Icon(_getFavoriteStatus(context) ? Icons.star_rounded : Icons.star_border_rounded, size: 20),
title: Text(_getFavoriteStatus(context) ? l10n.removeFromFavorites : l10n.addToFavorites),
),
if (feedState.feedType == FeedType.community)
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
onTap: () => triggerRefresh(context),
child: ListTile(
dense: true,
horizontalTitleGap: 5,
leading: const Icon(Icons.refresh_rounded, size: 20),
title: Text(l10n.refresh),
),
),
if (_getSubscriptionStatus(context) == SubscribedType.subscribed)
PopupMenuItem(
onTap: () async {
final Community community = context.read<FeedBloc>().state.fullCommunityView!.communityView.community;
bool isFavorite = _getFavoriteStatus(context);
await toggleFavoriteCommunity(context, community, isFavorite);
},
child: ListTile(
dense: true,
horizontalTitleGap: 5,
leading: Icon(_getFavoriteStatus(context) ? Icons.star_rounded : Icons.star_border_rounded, size: 20),
title: Text(_getFavoriteStatus(context) ? l10n.removeFromFavorites : l10n.addToFavorites),
),
),
],
),
],
),
],
]
: [],
);
}
}
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 @@ -939,6 +939,10 @@
"@trendingCommunities": {},
"unableToFindCommunity": "Unable to find community",
"@unableToFindCommunity": {},
"unableToFindCommunityName": "Unable to find community '{communityName}'",
"@unableToFindCommunityName": {
"description": "Error message for when we are unable to find a community, including the name"
},
"unableToFindInstance": "Unable to find instance",
"@unableToFindInstance": {},
"unableToFindLanguage": "Unable to find language",
Expand Down
7 changes: 4 additions & 3 deletions lib/utils/error_messages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import 'package:lemmy_api_client/v3.dart';
import 'package:thunder/utils/global_context.dart';

/// Generates a user-friendly error message from an exception (or any thrown object)
String getExceptionErrorMessage(Object e) {
String getExceptionErrorMessage(Object e, {String? additionalInfo}) {
if (e is LemmyApiException) {
return getErrorMessage(GlobalContext.context, e.message) ?? e.toString();
return getErrorMessage(GlobalContext.context, e.message, additionalInfo: additionalInfo) ?? e.toString();
}

if (e is TimeoutException) {
Expand All @@ -20,7 +20,7 @@ String getExceptionErrorMessage(Object e) {

/// Attempts to retrieve a localized error message for the given [lemmyApiErrorCode].
/// Returns null if not found.
String? getErrorMessage(BuildContext context, String lemmyApiErrorCode) {
String? getErrorMessage(BuildContext context, String lemmyApiErrorCode, {String? additionalInfo}) {
final AppLocalizations l10n = AppLocalizations.of(context)!;

return switch (lemmyApiErrorCode) {
Expand All @@ -29,6 +29,7 @@ String? getErrorMessage(BuildContext context, String lemmyApiErrorCode) {
"only_mods_can_post_in_community" => l10n.onlyModsCanPostInCommunity,
"couldnt_create_report" => l10n.couldntCreateReport,
"language_not_allowed" => l10n.languageNotAllowed,
"couldnt_find_community" => additionalInfo != null ? l10n.unableToFindCommunityName(additionalInfo) : l10n.unableToFindCommunity,
_ => lemmyApiErrorCode,
};
}
6 changes: 6 additions & 0 deletions lib/utils/instance.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ final RegExp instanceName = RegExp(r'^!?(https?:\/\/)?((?:(?!\/c\/c).)*)@(.*)$')
/// If so, returns the community name in the format [email protected].
/// Otherwise, returns null.
Future<String?> getLemmyCommunity(String text) async {
// Do an initial check for usernames in the format /u/[email protected].
// These can accidentally trip our community name detection.
if (text.toLowerCase().startsWith('/u/')) {
return null;
}

final RegExpMatch? fullCommunityUrlMatch = fullCommunityUrl.firstMatch(text);
if (fullCommunityUrlMatch != null && fullCommunityUrlMatch.groupCount >= 4 && await isLemmyInstance(fullCommunityUrlMatch.group(4))) {
return '${fullCommunityUrlMatch.group(3)}@${fullCommunityUrlMatch.group(4)}';
Expand Down

0 comments on commit 1a5d6ba

Please sign in to comment.