diff --git a/CHANGELOG.md b/CHANGELOG.md index 736ab5556..d5127e8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Added setting to import and export settings - Added liveness and latency indicators for instances in profile switcher - contribution from @micahmo - Add option to disabling graying out read posts - contribution from @micahmo +- Show sort type icon - contribution from @micahmo - Downvote actions will be disabled when instances have downvotes disabled - Automatically save drafts for posts and comments - contribution from @micahmo - Newly created comments get inserted into comment list correctly without losing your scroll position. If comment is top level, the list scrolls to your comment. The comment also gets highlighted - contribution from @ajsosa @@ -35,6 +36,7 @@ - Improve contrast and distinction of special user identifiers - contribution from @micahmo - Show swatches and live previews for accent color selection - contribution from @micahmo - Use Android system back button to navigate from Saved to History on profile page - contribution from @micahmo +- Hide community name and show usernames when viewing a community - contribution from @micahmo ### Fixed diff --git a/lib/community/bloc/community_bloc.dart b/lib/community/bloc/community_bloc.dart index c24c6a52c..0b6862689 100644 --- a/lib/community/bloc/community_bloc.dart +++ b/lib/community/bloc/community_bloc.dart @@ -15,6 +15,8 @@ import 'package:thunder/core/models/post_view_media.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/utils/constants.dart'; +import 'package:thunder/utils/error_messages.dart'; +import 'package:thunder/utils/global_context.dart'; import 'package:thunder/utils/post.dart'; part 'community_event.dart'; @@ -434,7 +436,7 @@ class CommunityBloc extends Bloc { return emit( state.copyWith( status: CommunityStatus.failure, - errorMessage: e.toString(), + errorMessage: e is LemmyApiException ? getErrorMessage(GlobalContext.context, e.message) : e.toString(), communityId: state.communityId, listingType: state.listingType, ), diff --git a/lib/community/pages/community_page.dart b/lib/community/pages/community_page.dart index 428e07c31..0cdc58433 100644 --- a/lib/community/pages/community_page.dart +++ b/lib/community/pages/community_page.dart @@ -172,7 +172,13 @@ class _CommunityPageState extends State with AutomaticKeepAliveCl maxLines: 1, overflow: TextOverflow.ellipsis, ), - subtitle: Text(getSortName(state)), + subtitle: Row( + children: [ + Icon(getSortIcon(state), size: 13), + const SizedBox(width: 4), + Text(getSortName(state)), + ], + ), contentPadding: const EdgeInsets.symmetric(horizontal: 0), ), centerTitle: false, @@ -494,6 +500,14 @@ class _CommunityPageState extends State with AutomaticKeepAliveCl return sortTypeLabel ?? ''; } + IconData? getSortIcon(CommunityState state) { + if (state.status == CommunityStatus.initial || state.status == CommunityStatus.loading) { + return null; + } + + return sortTypeIcon; + } + FutureOr _handleBack(bool stopDefaultButtonEvent, RouteInfo info) async { if (context.read().state.isFabOpen) { context.read().add(const OnFabToggle(false)); diff --git a/lib/community/utils/post_card_action_helpers.dart b/lib/community/utils/post_card_action_helpers.dart index 49c91b877..a64b9379d 100644 --- a/lib/community/utils/post_card_action_helpers.dart +++ b/lib/community/utils/post_card_action_helpers.dart @@ -19,6 +19,8 @@ import 'package:thunder/shared/picker_item.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/user/pages/user_page.dart'; +import 'package:thunder/utils/navigate_community.dart'; +import 'package:thunder/utils/navigate_user.dart'; import 'package:thunder/utils/swipe.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -116,23 +118,7 @@ void showPostActionBottomModalSheet(BuildContext context, PostViewMedia postView onTapCommunityName(context, postViewMedia.postView.community.id); break; case PostCardAction.visitProfile: - AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: UserPage(userId: postViewMedia.postView.post.creatorId), - ), - ), - ); + navigateToUserPage(context, userId: postViewMedia.postView.post.creatorId); break; case PostCardAction.sharePost: Share.share(postViewMedia.postView.post.apId); @@ -183,41 +169,9 @@ void showPostActionBottomModalSheet(BuildContext context, PostViewMedia postView } void onTapCommunityName(BuildContext context, int communityId) { - AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: CommunityPage(communityId: communityId), - ), - ), - ); + navigateToCommunityPage(context, communityId: communityId); } void onTapUserName(BuildContext context, int userId) { - AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: UserPage(userId: userId), - ), - ), - ); + navigateToUserPage(context, userId: userId); } diff --git a/lib/community/widgets/community_sidebar.dart b/lib/community/widgets/community_sidebar.dart index 65a1ac968..509dc8b1a 100644 --- a/lib/community/widgets/community_sidebar.dart +++ b/lib/community/widgets/community_sidebar.dart @@ -19,6 +19,8 @@ import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/shared/user_avatar.dart'; import 'package:thunder/utils/instance.dart'; +import 'package:thunder/utils/navigate_user.dart'; +import 'package:thunder/utils/swipe.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../shared/common_markdown_body.dart'; @@ -156,8 +158,11 @@ class _CommunitySidebarState extends State with TickerProvider } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -174,7 +179,8 @@ class _CommunitySidebarState extends State with TickerProvider ); }, ), - ).whenComplete(() async { + ) + .whenComplete(() async { timer.cancel(); if (newDraftPost?.saveAsDraft == true && newDraftPost?.isNotEmpty == true) { @@ -513,22 +519,7 @@ class _CommunitySidebarState extends State with TickerProvider for (var mods in widget.communityInfo!.moderators) GestureDetector( onTap: () { - AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: UserPage(userId: mods.moderator!.id), - ), - ), - ); + navigateToUserPage(context, userId: mods.moderator!.id); }, child: Padding( padding: const EdgeInsets.only(bottom: 8.0), diff --git a/lib/community/widgets/post_card.dart b/lib/community/widgets/post_card.dart index 517e2798f..1a21d879a 100644 --- a/lib/community/widgets/post_card.dart +++ b/lib/community/widgets/post_card.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -23,7 +25,7 @@ import '../../user/bloc/user_bloc.dart'; class PostCard extends StatefulWidget { final PostViewMedia postViewMedia; - final bool showInstanceName; + final bool communityMode; final bool indicateRead; final Function(VoteType) onVoteAction; @@ -35,7 +37,7 @@ class PostCard extends StatefulWidget { const PostCard({ super.key, required this.postViewMedia, - this.showInstanceName = true, + required this.communityMode, required this.onVoteAction, required this.onSaveAction, required this.onToggleReadAction, @@ -202,7 +204,7 @@ class _PostCardState extends State { showPostAuthor: state.showPostAuthor, hideNsfwPreviews: state.hideNsfwPreviews, markPostReadOnMediaView: state.markPostReadOnMediaView, - showInstanceName: widget.showInstanceName, + communityMode: widget.communityMode, isUserLoggedIn: isUserLoggedIn, listingType: widget.listingType, navigateToPost: () async => await navigateToPost(context), @@ -213,7 +215,7 @@ class _PostCardState extends State { showThumbnailPreviewOnRight: state.showThumbnailPreviewOnRight, hideNsfwPreviews: state.hideNsfwPreviews, markPostReadOnMediaView: state.markPostReadOnMediaView, - showInstanceName: widget.showInstanceName, + communityMode: widget.communityMode, showPostAuthor: state.showPostAuthor, showFullHeightImages: state.showFullHeightImages, edgeToEdgeImages: state.showEdgeToEdgeImages, @@ -269,7 +271,8 @@ class _PostCardState extends State { await Navigator.of(context).push( SwipeablePageRoute( - backGestureDetectionStartOffset: 45, + backGestureDetectionStartOffset: Platform.isAndroid ? 45 : 0, + backGestureDetectionWidth: 45, canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isPostPage: true), builder: (context) { return MultiBlocProvider( diff --git a/lib/community/widgets/post_card_list.dart b/lib/community/widgets/post_card_list.dart index d393339f1..9d310c4c6 100644 --- a/lib/community/widgets/post_card_list.dart +++ b/lib/community/widgets/post_card_list.dart @@ -327,7 +327,7 @@ class _PostCardListState extends State with TickerProviderStateMix child: !toRemoveSet.contains(postViewMedia.postView.post.id) ? PostCard( postViewMedia: postViewMedia, - showInstanceName: widget.communityId == null, + communityMode: widget.communityId != null || widget.communityName != null, onVoteAction: (VoteType voteType) => widget.onVoteAction(postViewMedia.postView.post.id, voteType), onSaveAction: (bool saved) => widget.onSaveAction(postViewMedia.postView.post.id, saved), onToggleReadAction: (bool read) => widget.onToggleReadAction(postViewMedia.postView.post.id, read), diff --git a/lib/community/widgets/post_card_metadata.dart b/lib/community/widgets/post_card_metadata.dart index b4dda3bd6..5adea912d 100644 --- a/lib/community/widgets/post_card_metadata.dart +++ b/lib/community/widgets/post_card_metadata.dart @@ -212,7 +212,7 @@ class PostCommunityAndAuthor extends StatelessWidget { super.key, required this.postView, required this.showCommunityIcons, - required this.showInstanceName, + required this.communityMode, this.textStyleAuthor, this.textStyleCommunity, required this.compactMode, @@ -220,7 +220,7 @@ class PostCommunityAndAuthor extends StatelessWidget { }); final bool showCommunityIcons; - final bool showInstanceName; + final bool communityMode; final bool compactMode; final PostView postView; final TextStyle? textStyleAuthor; @@ -252,7 +252,7 @@ class PostCommunityAndAuthor extends StatelessWidget { alignment: WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.end, children: [ - if (state.showPostAuthor) + if (state.showPostAuthor || communityMode) Row( mainAxisSize: MainAxisSize.min, children: [ @@ -260,13 +260,14 @@ class PostCommunityAndAuthor extends StatelessWidget { borderRadius: BorderRadius.circular(6), onTap: (compactMode && !state.tappableAuthorCommunity) ? null : () => onTapUserName(context, postView.creator.id), child: Text('$creatorName', textScaleFactor: MediaQuery.of(context).textScaleFactor * state.metadataFontSizeScale.textScaleFactor, style: textStyleAuthor)), - Text( - ' to ', - textScaleFactor: MediaQuery.of(context).textScaleFactor * state.metadataFontSizeScale.textScaleFactor, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodyMedium?.color?.withOpacity(0.4), + if (!communityMode) + Text( + ' to ', + textScaleFactor: MediaQuery.of(context).textScaleFactor * state.metadataFontSizeScale.textScaleFactor, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodyMedium?.color?.withOpacity(0.4), + ), ), - ), ], ), InkWell( @@ -275,11 +276,12 @@ class PostCommunityAndAuthor extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - '${postView.community.name}${showInstanceName ? ' · ${fetchInstanceNameFromUrl(postView.community.actorId)}' : ''}', - textScaleFactor: MediaQuery.of(context).textScaleFactor * state.metadataFontSizeScale.textScaleFactor, - style: textStyleCommunity, - ), + if (!communityMode) + Text( + '${postView.community.name} · ${fetchInstanceNameFromUrl(postView.community.actorId)}', + textScaleFactor: MediaQuery.of(context).textScaleFactor * state.metadataFontSizeScale.textScaleFactor, + style: textStyleCommunity, + ), if (showCommunitySubscription) Padding( padding: const EdgeInsets.only( diff --git a/lib/community/widgets/post_card_view_comfortable.dart b/lib/community/widgets/post_card_view_comfortable.dart index def3d6369..0d89b98b3 100644 --- a/lib/community/widgets/post_card_view_comfortable.dart +++ b/lib/community/widgets/post_card_view_comfortable.dart @@ -24,7 +24,7 @@ class PostCardViewComfortable extends StatelessWidget { final bool hideNsfwPreviews; final bool edgeToEdgeImages; final bool showTitleFirst; - final bool showInstanceName; + final bool communityMode; final bool showPostAuthor; final bool showFullHeightImages; final bool showVoteActions; @@ -44,7 +44,7 @@ class PostCardViewComfortable extends StatelessWidget { required this.hideNsfwPreviews, required this.edgeToEdgeImages, required this.showTitleFirst, - required this.showInstanceName, + required this.communityMode, required this.showPostAuthor, required this.showFullHeightImages, required this.showVoteActions, @@ -113,11 +113,6 @@ class PostCardViewComfortable extends StatelessWidget { color: indicateRead && postViewMedia.postView.read ? Colors.red.withOpacity(0.55) : Colors.red, size: 15 * textScaleFactor, )), - if (!postViewMedia.postView.post.featuredCommunity && (useSaveButton || !postViewMedia.postView.saved)) - const WidgetSpan( - child: SizedBox( - width: 3, - )), ], if (!useSaveButton && postViewMedia.postView.saved) WidgetSpan( @@ -136,6 +131,12 @@ class PostCardViewComfortable extends StatelessWidget { color: indicateRead && postViewMedia.postView.read ? Colors.green.withOpacity(0.55) : Colors.green, ), ), + if (postViewMedia.postView.post.featuredCommunity || postViewMedia.postView.saved || postViewMedia.postView.post.locked) + const WidgetSpan( + child: SizedBox( + width: 3.5, + ), + ), TextSpan( text: postViewMedia.postView.post.name, style: theme.textTheme.bodyMedium?.copyWith( @@ -173,11 +174,6 @@ class PostCardViewComfortable extends StatelessWidget { color: indicateRead && postViewMedia.postView.read ? Colors.red.withOpacity(0.55) : Colors.red, size: 15 * textScaleFactor, )), - if (!postViewMedia.postView.post.featuredCommunity && (useSaveButton || !postViewMedia.postView.saved)) - const WidgetSpan( - child: SizedBox( - width: 3, - )), ], if (!useSaveButton && postViewMedia.postView.saved) WidgetSpan( @@ -196,6 +192,12 @@ class PostCardViewComfortable extends StatelessWidget { color: indicateRead && postViewMedia.postView.read ? Colors.green.withOpacity(0.55) : Colors.green, ), ), + if (postViewMedia.postView.post.featuredCommunity || postViewMedia.postView.saved || postViewMedia.postView.post.locked) + const WidgetSpan( + child: SizedBox( + width: 3.5, + ), + ), TextSpan( text: postViewMedia.postView.post.name, style: theme.textTheme.bodyMedium?.copyWith( @@ -235,7 +237,7 @@ class PostCardViewComfortable extends StatelessWidget { children: [ PostCommunityAndAuthor( showCommunityIcons: showCommunityIcons, - showInstanceName: showInstanceName, + communityMode: communityMode, postView: postViewMedia.postView, textStyleCommunity: textStyleCommunityAndAuthor, textStyleAuthor: textStyleCommunityAndAuthor, diff --git a/lib/community/widgets/post_card_view_compact.dart b/lib/community/widgets/post_card_view_compact.dart index 1242dcc81..6a53d0123 100644 --- a/lib/community/widgets/post_card_view_compact.dart +++ b/lib/community/widgets/post_card_view_compact.dart @@ -19,7 +19,7 @@ class PostCardViewCompact extends StatelessWidget { final bool showTextPostIndicator; final bool showPostAuthor; final bool hideNsfwPreviews; - final bool showInstanceName; + final bool communityMode; final bool markPostReadOnMediaView; final bool isUserLoggedIn; final PostListingType? listingType; @@ -33,7 +33,7 @@ class PostCardViewCompact extends StatelessWidget { required this.showTextPostIndicator, required this.showPostAuthor, required this.hideNsfwPreviews, - required this.showInstanceName, + required this.communityMode, required this.markPostReadOnMediaView, required this.isUserLoggedIn, required this.listingType, @@ -112,11 +112,6 @@ class PostCardViewCompact extends StatelessWidget { color: indicateRead && postViewMedia.postView.read ? Colors.red.withOpacity(0.55) : Colors.red, size: 15 * textScaleFactor, )), - if (!postViewMedia.postView.post.featuredCommunity && !postViewMedia.postView.saved) - const WidgetSpan( - child: SizedBox( - width: 3, - )), ], if (postViewMedia.postView.saved) WidgetSpan( @@ -135,6 +130,12 @@ class PostCardViewCompact extends StatelessWidget { color: indicateRead && postViewMedia.postView.read ? Colors.green.withOpacity(0.55) : Colors.green, ), ), + if (postViewMedia.postView.post.featuredCommunity || postViewMedia.postView.saved || postViewMedia.postView.post.locked) + const WidgetSpan( + child: SizedBox( + width: 3.5, + ), + ), TextSpan( text: postViewMedia.postView.post.name, style: theme.textTheme.bodyMedium?.copyWith( @@ -152,7 +153,7 @@ class PostCardViewCompact extends StatelessWidget { PostCommunityAndAuthor( compactMode: true, showCommunityIcons: false, - showInstanceName: showInstanceName, + communityMode: communityMode, postView: postViewMedia.postView, textStyleCommunity: textStyleCommunityAndAuthor, textStyleAuthor: textStyleCommunityAndAuthor, diff --git a/lib/core/enums/fab_action.dart b/lib/core/enums/fab_action.dart index b9eb35faa..d0a7cae7f 100644 --- a/lib/core/enums/fab_action.dart +++ b/lib/core/enums/fab_action.dart @@ -139,8 +139,11 @@ enum FeedFabAction { } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -157,7 +160,8 @@ enum FeedFabAction { ); }, ), - ).whenComplete(() async { + ) + .whenComplete(() async { timer.cancel(); if (newDraftPost?.saveAsDraft == true && newDraftPost?.isNotEmpty == true) { diff --git a/lib/inbox/widgets/inbox_mentions_view.dart b/lib/inbox/widgets/inbox_mentions_view.dart index ac7d948ce..1feb6634a 100644 --- a/lib/inbox/widgets/inbox_mentions_view.dart +++ b/lib/inbox/widgets/inbox_mentions_view.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:async'; import 'dart:convert'; @@ -21,6 +22,7 @@ import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/utils/date_time.dart'; import 'package:thunder/utils/instance.dart'; +import 'package:thunder/utils/navigate_community.dart'; import 'package:thunder/utils/swipe.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -53,7 +55,8 @@ class InboxMentionsView extends StatelessWidget { // To to specific post for now, in the future, will be best to scroll to the position of the comment await Navigator.of(context).push( SwipeablePageRoute( - backGestureDetectionStartOffset: 45, + backGestureDetectionStartOffset: Platform.isAndroid ? 45 : 0, + backGestureDetectionWidth: 45, canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isPostPage: true), builder: (context) => MultiBlocProvider( providers: [ @@ -129,8 +132,11 @@ class InboxMentionsView extends StatelessWidget { } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -147,7 +153,8 @@ class InboxMentionsView extends StatelessWidget { )); }, ), - ).whenComplete(() async { + ) + .whenComplete(() async { timer.cancel(); if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true) { @@ -177,22 +184,6 @@ class InboxMentionsView extends StatelessWidget { } void onTapCommunityName(BuildContext context, int communityId) { - AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: CommunityPage(communityId: communityId), - ), - ), - ); + navigateToCommunityPage(context, communityId: communityId); } } diff --git a/lib/inbox/widgets/inbox_replies_view.dart b/lib/inbox/widgets/inbox_replies_view.dart index 3218a4d23..62ee49657 100644 --- a/lib/inbox/widgets/inbox_replies_view.dart +++ b/lib/inbox/widgets/inbox_replies_view.dart @@ -21,6 +21,7 @@ import 'package:thunder/shared/comment_reference.dart'; import 'package:thunder/post/pages/create_comment_page.dart'; import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; +import 'package:thunder/utils/navigate_community.dart'; import 'package:thunder/utils/swipe.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -93,8 +94,11 @@ class _InboxRepliesViewState extends State { } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -111,7 +115,8 @@ class _InboxRepliesViewState extends State { )); }, ), - ).whenComplete(() async { + ) + .whenComplete(() async { timer.cancel(); if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true && (!isEdit || commentView.comment.content != newDraftComment?.text)) { @@ -147,194 +152,9 @@ class _InboxRepliesViewState extends State { ); }, ); - - /*Card( - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () async { - AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - // To to specific post for now, in the future, will be best to scroll to the position of the comment - await Navigator.of(context).push( - SwipeablePageRoute( - backGestureDetectionStartOffset: 45, - canOnlySwipeFromEdge: disableFullPageSwipe( - isUserLoggedIn: authBloc.state.isLoggedIn, - state: thunderBloc.state, - isPostPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - BlocProvider(create: (context) => PostBloc()), - ], - child: PostPage( - selectedCommentId: widget.replies[index].comment.id, - selectedCommentPath: widget.replies[index].comment.path, - postId: widget.replies[index].post.id, - onPostUpdated: () => {}, - ), - ), - ), - ); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.replies[index].creator.name, - style: theme.textTheme.titleSmall - ?.copyWith(color: Colors.greenAccent), - ), - Text(formatTimeToString( - dateTime: widget.replies[index].comment.published - .toIso8601String())) - ], - ), - GestureDetector( - child: Text( - '${widget.replies[index].community.name}${' · ${fetchInstanceNameFromUrl(widget.replies[index].community.actorId)}'}', - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodyMedium?.color - ?.withOpacity(0.75), - ), - ), - onTap: () => onTapCommunityName( - context, widget.replies[index].community.id), - ), - const SizedBox(height: 10), - CommonMarkdownBody( - body: widget.replies[index].comment.content), - const Divider(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Divider( - height: 1.0, - thickness: 1.0, - color: ElevationOverlay.applySurfaceTint( - Theme.of(context).colorScheme.surface, - Theme.of(context).colorScheme.surfaceTint, - 10, - ), - ), - CommentReference( - comment: widget.replies[index], - now: now, - onVoteAction: (int commentId, VoteType voteType) => - context.read().add(VoteCommentEvent( - commentId: commentId, score: voteType)), - onSaveAction: (int commentId, bool save) => context - .read() - .add(SaveCommentEvent( - commentId: commentId, save: save)), - onDeleteAction: (int commentId, bool deleted) => context - .read() - .add(DeleteCommentEvent( - deleted: deleted, commentId: commentId)), - onReplyEditAction: - (CommentView commentView, bool isEdit) { - HapticFeedback.mediumImpact(); - InboxBloc inboxBloc = context.read(); - PostBloc postBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - AccountBloc accountBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - builder: (context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value( - value: inboxBloc), - BlocProvider.value( - value: postBloc), - BlocProvider.value( - value: thunderBloc), - BlocProvider.value( - value: accountBloc), - ], - child: CreateCommentPage( - commentView: commentView, - isEdit: isEdit)); - }, - ), - ); - }, - isOwnComment: widget.replies[index].creator.id == - context.read().state.account?.userId, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (widget.replies[index].commentReply?.read == - false) - inboxReplyMarkedAsRead != - widget.replies[index].commentReply?.id - ? IconButton( - onPressed: () { - setState(() => inboxReplyMarkedAsRead = - widget.replies[index].commentReply - ?.id); - context.read().add( - MarkReplyAsReadEvent( - commentReplyId: widget - .replies[index] - .commentReply! - .id, - read: true)); - }, - icon: const Icon( - Icons.check, - semanticLabel: 'Mark as read', - ), - visualDensity: VisualDensity.compact, - ) - : const Padding( - padding: - EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator()), - ), - ], - ), - ), - ], - ), - ], - ), - ), - ), - );*/ } void onTapCommunityName(BuildContext context, int communityId) { - AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: CommunityPage(communityId: communityId), - ), - ), - ); + navigateToCommunityPage(context, communityId: communityId); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6357756ec..584fffe66 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,8 +1,24 @@ { + "onlyModsCanPostInCommunity": "Only moderators may post in this community", "save": "Save", "posts": "Posts", "saved": "Saved", "overview": "Overview", + "blockedUsers": "Blocked Users", + "noUserBlocks": "It looks like you have not blocked anybody.", + "blockedCommunities": "Blocked Communities", + "noCommunityBlocks": "It looks like you have not blocked any communities.", + "blockUser": "Block User", + "username": "Username", + "cancel": "Cancel", + "ok": "OK", + "unableToFindUser": "Unable to find user", + "unableToFindCommunity": "Unable to find community", + "blockCommunity": "Block Community", + "community": "Community", + "cantBlockYourself": "You may not block yourself.", + "add": "Add", + "remove": "Remove", "somethingWentWrong": "Oops, something went wrong!", "unableToLoadPostsFrominstance": "Unable to load posts from {instance}", "internetOrInstanceIssues": "You may not be connected to the internet, or your instance may be currently unavailable.", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 252294842..768c825b8 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,8 +1,24 @@ { + "onlyModsCanPostInCommunity": "Only moderators may post in this community", "save": "Save", "posts": "Posts", "saved": "Saved", "overview": "Overview", + "blockedUsers": "Blocked Users", + "noUserBlocks": "It looks like you have not blocked anybody.", + "blockedCommunities": "Blocked Communities", + "noCommunityBlocks": "It looks like you have not blocked any communities.", + "blockUser": "Block User", + "username": "Username", + "cancel": "Cancel", + "ok": "OK", + "unableToFindUser": "Unable to find user", + "unableToFindCommunity": "Unable to find community", + "blockCommunity": "Block Community", + "community": "Community", + "cantBlockYourself": "You may not block yourself.", + "add": "Add", + "remove": "Remove", "somethingWentWrong": "Oops, something went wrong!", "unableToLoadPostsFrominstance": "Unable to load posts from {instance}", "internetOrInstanceIssues": "You may not be connected to the internet, or your instance may be currently unavailable.", diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index 726136edc..c26723a7b 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -1,8 +1,24 @@ { + "onlyModsCanPostInCommunity": "Only moderators may post in this community", "save": "Save", "posts": "Posts", "saved": "Saved", "overview": "Overview", + "blockedUsers": "Blocked Users", + "noUserBlocks": "It looks like you have not blocked anybody.", + "blockedCommunities": "Blocked Communities", + "noCommunityBlocks": "It looks like you have not blocked any communities.", + "blockUser": "Block User", + "username": "Username", + "cancel": "Cancel", + "ok": "OK", + "unableToFindUser": "Unable to find user", + "unableToFindCommunity": "Unable to find community", + "blockCommunity": "Block Community", + "community": "Community", + "cantBlockYourself": "You may not block yourself.", + "add": "Add", + "remove": "Remove", "somethingWentWrong": "Oops, something went wrong!", "unableToLoadPostsFrominstance": "Unable to load posts from {instance}", "internetOrInstanceIssues": "You may not be connected to the internet, or your instance may be currently unavailable.", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 78e4a70c6..7a2344e0d 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,8 +1,24 @@ { + "onlyModsCanPostInCommunity": "Only moderators may post in this community", "save": "Save", "posts": "Posts", "saved": "Saved", "overview": "Overview", + "blockedUsers": "Blocked Users", + "noUserBlocks": "It looks like you have not blocked anybody.", + "blockedCommunities": "Blocked Communities", + "noCommunityBlocks": "It looks like you have not blocked any communities.", + "blockUser": "Block User", + "username": "Username", + "cancel": "Cancel", + "ok": "OK", + "unableToFindUser": "Unable to find user", + "unableToFindCommunity": "Unable to find community", + "blockCommunity": "Block Community", + "community": "Community", + "cantBlockYourself": "You may not block yourself.", + "add": "Add", + "remove": "Remove", "somethingWentWrong": "Oops, something went wrong!", "unableToLoadPostsFrominstance": "Unable to load posts from {instance}", "internetOrInstanceIssues": "You may not be connected to the internet, or your instance may be currently unavailable.", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 726136edc..c26723a7b 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,8 +1,24 @@ { + "onlyModsCanPostInCommunity": "Only moderators may post in this community", "save": "Save", "posts": "Posts", "saved": "Saved", "overview": "Overview", + "blockedUsers": "Blocked Users", + "noUserBlocks": "It looks like you have not blocked anybody.", + "blockedCommunities": "Blocked Communities", + "noCommunityBlocks": "It looks like you have not blocked any communities.", + "blockUser": "Block User", + "username": "Username", + "cancel": "Cancel", + "ok": "OK", + "unableToFindUser": "Unable to find user", + "unableToFindCommunity": "Unable to find community", + "blockCommunity": "Block Community", + "community": "Community", + "cantBlockYourself": "You may not block yourself.", + "add": "Add", + "remove": "Remove", "somethingWentWrong": "Oops, something went wrong!", "unableToLoadPostsFrominstance": "Unable to load posts from {instance}", "internetOrInstanceIssues": "You may not be connected to the internet, or your instance may be currently unavailable.", diff --git a/lib/post/pages/post_page.dart b/lib/post/pages/post_page.dart index 1f1581592..fbb0be6a9 100644 --- a/lib/post/pages/post_page.dart +++ b/lib/post/pages/post_page.dart @@ -140,7 +140,13 @@ class _PostPageState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, ), - subtitle: Text(sortTypeLabel ?? ''), + subtitle: Row( + children: [ + Icon(sortTypeIcon, size: 13), + const SizedBox(width: 4), + Text(sortTypeLabel ?? ''), + ], + ), contentPadding: const EdgeInsets.symmetric(horizontal: 0), ), flexibleSpace: GestureDetector( @@ -460,6 +466,7 @@ class _PostPageState extends State { onSelect: (selected) { setState(() { sortType = selected.payload; + sortTypeLabel = selected.label; sortTypeIcon = selected.icon; }); context.read().add( @@ -503,8 +510,11 @@ class _PostPageState extends State { } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -519,7 +529,8 @@ class _PostPageState extends State { )); }, ), - ).whenComplete(() async { + ) + .whenComplete(() async { timer.cancel(); if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true) { diff --git a/lib/post/pages/post_page_success.dart b/lib/post/pages/post_page_success.dart index 2ae8499a8..5ae05d078 100644 --- a/lib/post/pages/post_page_success.dart +++ b/lib/post/pages/post_page_success.dart @@ -116,8 +116,11 @@ class _PostPageSuccessState extends State { } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -133,7 +136,8 @@ class _PostPageSuccessState extends State { )); }, ), - ).whenComplete(() async { + ) + .whenComplete(() async { timer.cancel(); if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true && (!isEdit || commentView.comment.content != newDraftComment?.text)) { diff --git a/lib/post/utils/comment_actions.dart b/lib/post/utils/comment_actions.dart index 24e171a2e..030a1d6ab 100644 --- a/lib/post/utils/comment_actions.dart +++ b/lib/post/utils/comment_actions.dart @@ -63,8 +63,11 @@ void triggerCommentAction({ } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -83,7 +86,8 @@ void triggerCommentAction({ ); }, ), - ).whenComplete(() async { + ) + .whenComplete(() async { timer.cancel(); if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true && (swipeAction != SwipeAction.edit || commentView.comment.content != newDraftComment?.text)) { diff --git a/lib/post/widgets/post_view.dart b/lib/post/widgets/post_view.dart index 8a26d6d7c..e6d724980 100644 --- a/lib/post/widgets/post_view.dart +++ b/lib/post/widgets/post_view.dart @@ -28,6 +28,8 @@ import 'package:thunder/thunder/thunder_icons.dart'; import 'package:thunder/user/pages/user_page.dart'; import 'package:thunder/user/utils/special_user_checks.dart'; import 'package:thunder/utils/instance.dart'; +import 'package:thunder/utils/navigate_community.dart'; +import 'package:thunder/utils/navigate_user.dart'; import 'package:thunder/utils/numbers.dart'; import 'package:thunder/utils/swipe.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -110,25 +112,7 @@ class PostSubview extends StatelessWidget { child: InkWell( borderRadius: BorderRadius.circular(5), onTap: () { - account_bloc.AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: UserPage( - userId: postView.creator.id, - ), - ), - ), - ); + navigateToUserPage(context, userId: postView.creator.id); }, child: Padding( padding: const EdgeInsets.only(left: 5, right: 5), @@ -196,23 +180,7 @@ class PostSubview extends StatelessWidget { InkWell( borderRadius: BorderRadius.circular(5), onTap: () { - account_bloc.AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: CommunityPage(communityId: postView.community.id), - ), - ), - ); + navigateToCommunityPage(context, communityId: postView.community.id); }, child: Tooltip( excludeFromSemantics: true, @@ -362,8 +330,11 @@ class PostSubview extends StatelessWidget { } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -379,7 +350,8 @@ class PostSubview extends StatelessWidget { ); }, ), - ).whenComplete(() async { + ) + .whenComplete(() async { timer.cancel(); if (newDraftComment?.saveAsDraft == true && newDraftComment?.isNotEmpty == true) { diff --git a/lib/routes.dart b/lib/routes.dart index 6d85abcce..ebe68f3e8 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:swipeable_page_route/swipeable_page_route.dart'; import 'package:thunder/account/bloc/account_bloc.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; diff --git a/lib/search/pages/search_page.dart b/lib/search/pages/search_page.dart index 3c7494af6..8c107fb4d 100644 --- a/lib/search/pages/search_page.dart +++ b/lib/search/pages/search_page.dart @@ -26,6 +26,7 @@ import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/utils/constants.dart'; import 'package:thunder/utils/debounce.dart'; import 'package:thunder/utils/instance.dart'; +import 'package:thunder/utils/navigate_community.dart'; import 'package:thunder/utils/swipe.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -261,73 +262,59 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi CommunityView communityView = state.communities![index]; final Set currentSubscriptions = context.read().state.ids; return Tooltip( - excludeFromSemantics: true, - message: '${communityView.community.title}\n${communityView.community.name} · ${fetchInstanceNameFromUrl(communityView.community.actorId)}', - preferBelow: false, - child: ListTile( - leading: CommunityIcon(community: communityView.community, radius: 25), - title: Text( - communityView.community.title, + excludeFromSemantics: true, + message: '${communityView.community.title}\n${communityView.community.name} · ${fetchInstanceNameFromUrl(communityView.community.actorId)}', + preferBelow: false, + child: ListTile( + leading: CommunityIcon(community: communityView.community, radius: 25), + title: Text( + communityView.community.title, + overflow: TextOverflow.ellipsis, + ), + subtitle: Row(children: [ + Flexible( + child: Text( + '${communityView.community.name} · ${fetchInstanceNameFromUrl(communityView.community.actorId)}', overflow: TextOverflow.ellipsis, ), - subtitle: Row(children: [ - Flexible( - child: Text( - '${communityView.community.name} · ${fetchInstanceNameFromUrl(communityView.community.actorId)}', - overflow: TextOverflow.ellipsis, - ), - ), - Text( - ' · ${communityView.counts.subscribers}', - semanticsLabel: '${communityView.counts.subscribers} subscribers', - ), - const SizedBox(width: 4), - const Icon(Icons.people_rounded, size: 16.0), - ]), - trailing: IconButton( - onPressed: () { - SubscribedType subscriptionStatus = _getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions); - _onSubscribeIconPressed(isUserLoggedIn, context, communityView); - showSnackbar( - context, - subscriptionStatus == SubscribedType.notSubscribed - ? AppLocalizations.of(context)!.addedCommunityToSubscriptions - : AppLocalizations.of(context)!.removedCommunityFromSubscriptions); - context.read().add(GetAccountInformation()); - }, - icon: Icon( - switch (_getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions)) { - SubscribedType.notSubscribed => Icons.add_circle_outline_rounded, - SubscribedType.pending => Icons.pending_outlined, - SubscribedType.subscribed => Icons.remove_circle_outline_rounded, - }, - ), - tooltip: switch (_getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions)) { - SubscribedType.notSubscribed => 'Subscribe', - SubscribedType.pending => 'Unsubscribe (subscription pending)', - SubscribedType.subscribed => 'Unsubscribe', - }, - visualDensity: VisualDensity.compact, - ), - onTap: () { - AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: CommunityPage(communityId: communityView.community.id), - ), - ), - ); - })); + ), + Text( + ' · ${communityView.counts.subscribers}', + semanticsLabel: '${communityView.counts.subscribers} subscribers', + ), + const SizedBox(width: 4), + const Icon(Icons.people_rounded, size: 16.0), + ]), + trailing: IconButton( + onPressed: () { + SubscribedType subscriptionStatus = _getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions); + _onSubscribeIconPressed(isUserLoggedIn, context, communityView); + showSnackbar( + context, + subscriptionStatus == SubscribedType.notSubscribed + ? AppLocalizations.of(context)!.addedCommunityToSubscriptions + : AppLocalizations.of(context)!.removedCommunityFromSubscriptions); + context.read().add(GetAccountInformation()); + }, + icon: Icon( + switch (_getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions)) { + SubscribedType.notSubscribed => Icons.add_circle_outline_rounded, + SubscribedType.pending => Icons.pending_outlined, + SubscribedType.subscribed => Icons.remove_circle_outline_rounded, + }, + ), + tooltip: switch (_getCurrentSubscriptionStatus(isUserLoggedIn, communityView, currentSubscriptions)) { + SubscribedType.notSubscribed => 'Subscribe', + SubscribedType.pending => 'Unsubscribe (subscription pending)', + SubscribedType.subscribed => 'Unsubscribe', + }, + visualDensity: VisualDensity.compact, + ), + onTap: () { + navigateToCommunityPage(context, communityId: communityView.community.id); + }, + ), + ); } }, ); diff --git a/lib/settings/pages/about_settings_page.dart b/lib/settings/pages/about_settings_page.dart index 680073679..f264fb235 100644 --- a/lib/settings/pages/about_settings_page.dart +++ b/lib/settings/pages/about_settings_page.dart @@ -60,7 +60,7 @@ class AboutSettingsPage extends StatelessWidget { subtitle: const Text('lemmy.world/c/thunder_app'), trailing: const Icon(Icons.chevron_right_rounded), onTap: () { - navigateToCommunityByName(context, 'thunder_app@lemmy.world'); + navigateToCommunityPage(context, communityName: 'thunder_app@lemmy.world'); }, ), ListTile( diff --git a/lib/settings/pages/general_settings_page.dart b/lib/settings/pages/general_settings_page.dart index 9f50ad49d..900f0a8bc 100644 --- a/lib/settings/pages/general_settings_page.dart +++ b/lib/settings/pages/general_settings_page.dart @@ -410,6 +410,16 @@ class _GeneralSettingsPageState extends State with SingleTi }, previouslySelected: defaultSortType, ), + valueDisplay: Row( + children: [ + Icon(allSortTypeItems.firstWhere((sortTypeItem) => sortTypeItem.payload == defaultSortType).icon, size: 13), + const SizedBox(width: 4), + Text( + allSortTypeItems.firstWhere((sortTypeItem) => sortTypeItem.payload == defaultSortType).label, + style: theme.textTheme.titleSmall, + ), + ], + ), ), ], ), @@ -603,6 +613,16 @@ class _GeneralSettingsPageState extends State with SingleTi }, previouslySelected: defaultCommentSortType, ), + valueDisplay: Row( + children: [ + Icon(commentSortTypeItems.firstWhere((sortTypeItem) => sortTypeItem.payload == defaultCommentSortType).icon, size: 13), + const SizedBox(width: 4), + Text( + commentSortTypeItems.firstWhere((sortTypeItem) => sortTypeItem.payload == defaultCommentSortType).label, + style: theme.textTheme.titleSmall, + ), + ], + ), ), ListOption( description: LocalSettings.nestedCommentIndicatorStyle.label, diff --git a/lib/shared/comment_header.dart b/lib/shared/comment_header.dart index 7dce074ed..ac343ef0a 100644 --- a/lib/shared/comment_header.dart +++ b/lib/shared/comment_header.dart @@ -10,6 +10,7 @@ import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/thunder/thunder_icons.dart'; import 'package:thunder/user/utils/special_user_checks.dart'; import 'package:thunder/utils/instance.dart'; +import 'package:thunder/utils/navigate_user.dart'; import 'package:thunder/utils/numbers.dart'; import 'package:thunder/user/pages/user_page.dart'; import 'package:thunder/utils/swipe.dart'; @@ -71,23 +72,7 @@ class CommentHeader extends StatelessWidget { child: InkWell( borderRadius: const BorderRadius.all(Radius.elliptical(5, 5)), onTap: () { - account_bloc.AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: UserPage(userId: comment.creator.id), - ), - ), - ); + navigateToUserPage(context, userId: comment.creator.id); }, child: Padding( padding: const EdgeInsets.only(left: 5, right: 5), diff --git a/lib/shared/comment_reference.dart b/lib/shared/comment_reference.dart index fa37bd2a2..cead3e4a4 100644 --- a/lib/shared/comment_reference.dart +++ b/lib/shared/comment_reference.dart @@ -89,6 +89,7 @@ class _CommentReferenceState extends State { // To to specific post for now, in the future, will be best to scroll to the position of the comment await Navigator.of(context).push( SwipeablePageRoute( + backGestureDetectionWidth: 45, canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isPostPage: true), builder: (context) => MultiBlocProvider( providers: [ diff --git a/lib/shared/common_markdown_body.dart b/lib/shared/common_markdown_body.dart index c576b344a..878c895cc 100644 --- a/lib/shared/common_markdown_body.dart +++ b/lib/shared/common_markdown_body.dart @@ -70,7 +70,7 @@ class CommonMarkdownBody extends StatelessWidget { if (communityName != null) { try { - await navigateToCommunityByName(context, communityName); + await navigateToCommunityPage(context, communityName: communityName); return; } catch (e) { // Ignore exception, if it's not a valid community we'll perform the next fallback @@ -81,7 +81,7 @@ class CommonMarkdownBody extends StatelessWidget { if (username != null) { try { - await navigateToUserByName(context, username); + await navigateToUserPage(context, username: username); return; } catch (e) { // Ignore exception, if it's not a valid user, we'll perform the next fallback diff --git a/lib/shared/image_viewer.dart b/lib/shared/image_viewer.dart index 8f7b03239..b1cae3af7 100644 --- a/lib/shared/image_viewer.dart +++ b/lib/shared/image_viewer.dart @@ -313,7 +313,13 @@ class _ImageViewerState extends State with TickerProviderStateMixin } }, icon: isDownloadingMedia - ? Container() + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white.withOpacity(0.90), + ), + ) : Icon( Icons.share_rounded, semanticLabel: "Share", diff --git a/lib/shared/input_dialogs.dart b/lib/shared/input_dialogs.dart new file mode 100644 index 000000000..30480f854 --- /dev/null +++ b/lib/shared/input_dialogs.dart @@ -0,0 +1,237 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:lemmy_api_client/v3.dart'; +import 'package:text_scroll/text_scroll.dart'; +import 'package:thunder/account/models/account.dart'; +import 'package:thunder/core/auth/helpers/fetch_account.dart'; +import 'package:thunder/core/singletons/lemmy_client.dart'; +import 'package:thunder/shared/community_icon.dart'; +import 'package:thunder/shared/user_avatar.dart'; +import 'package:thunder/utils/instance.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +/// Shows a dialog which allows typing/search for a user +void showUserInputDialog(BuildContext context, {required String title, required void Function(PersonViewSafe) onUserSelected}) async { + Future onSubmitted({PersonViewSafe? payload, String? value}) async { + if (payload != null) { + onUserSelected(payload); + Navigator.of(context).pop(); + } else if (value != null) { + // Normalize the username + final String? normalizedUsername = await getLemmyUser(value); + if (normalizedUsername != null) { + try { + Account? account = await fetchActiveProfileAccount(); + final FullPersonView fullPersonView = await LemmyClient.instance.lemmyApiV3.run(GetPersonDetails( + auth: account?.jwt, + username: normalizedUsername, + )); + + onUserSelected(fullPersonView.personView); + + Navigator.of(context).pop(); + } catch (e) { + return AppLocalizations.of(context)!.unableToFindUser; + } + } else { + return AppLocalizations.of(context)!.unableToFindUser; + } + } + return null; + } + + showInputDialog( + context: context, + title: title, + inputLabel: AppLocalizations.of(context)!.username, + onSubmitted: onSubmitted, + getSuggestions: (query) async { + if (query.isNotEmpty != true) { + return const Iterable.empty(); + } + Account? account = await fetchActiveProfileAccount(); + final SearchResults searchReults = await LemmyClient.instance.lemmyApiV3.run(Search( + q: query, + auth: account?.jwt, + type: SearchType.users, + limit: 10, + )); + return searchReults.users; + }, + suggestionBuilder: (payload) { + return Tooltip( + message: '${payload.person.name}@${fetchInstanceNameFromUrl(payload.person.actorId)}', + preferBelow: false, + child: ListTile( + leading: UserAvatar(person: payload.person), + title: Text( + payload.person.displayName ?? payload.person.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: TextScroll( + '${payload.person.name}@${fetchInstanceNameFromUrl(payload.person.actorId)}', + delayBefore: const Duration(seconds: 2), + pauseBetween: const Duration(seconds: 3), + velocity: const Velocity(pixelsPerSecond: Offset(50, 0)), + ), + ), + ); + }, + ); +} + +/// Shows a dialog which allows typing/search for a community +void showCommunityInputDialog(BuildContext context, {required String title, required void Function(CommunityView) onCommunitySelected}) async { + Future onSubmitted({CommunityView? payload, String? value}) async { + if (payload != null) { + onCommunitySelected(payload); + Navigator.of(context).pop(); + } else if (value != null) { + // Normalize the community name + final String? normalizedCommunity = await getLemmyCommunity(value); + if (normalizedCommunity != null) { + try { + Account? account = await fetchActiveProfileAccount(); + final FullCommunityView fullCommunityView = await LemmyClient.instance.lemmyApiV3.run(GetCommunity( + auth: account?.jwt, + name: normalizedCommunity, + )); + + onCommunitySelected(fullCommunityView.communityView); + + Navigator.of(context).pop(); + } catch (e) { + return AppLocalizations.of(context)!.unableToFindCommunity; + } + } else { + return AppLocalizations.of(context)!.unableToFindCommunity; + } + } + return null; + } + + showInputDialog( + context: context, + title: title, + inputLabel: AppLocalizations.of(context)!.community, + onSubmitted: onSubmitted, + getSuggestions: (query) async { + if (query.isNotEmpty != true) { + return const Iterable.empty(); + } + Account? account = await fetchActiveProfileAccount(); + final SearchResults searchReults = await LemmyClient.instance.lemmyApiV3.run(Search( + q: query, + auth: account?.jwt, + type: SearchType.communities, + limit: 10, + )); + return searchReults.communities; + }, + suggestionBuilder: (payload) { + return Tooltip( + message: '${payload.community.name}@${fetchInstanceNameFromUrl(payload.community.actorId)}', + preferBelow: false, + child: ListTile( + leading: CommunityIcon(community: payload.community), + title: Text( + payload.community.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: TextScroll( + '${payload.community.name}@${fetchInstanceNameFromUrl(payload.community.actorId)}', + delayBefore: const Duration(seconds: 2), + pauseBetween: const Duration(seconds: 3), + velocity: const Velocity(pixelsPerSecond: Offset(50, 0)), + ), + ), + ); + }, + ); +} + +/// Shows a dialog which takes input and offers suggestions +void showInputDialog({ + required BuildContext context, + required String title, + required String inputLabel, + required Future Function({T? payload, String? value}) onSubmitted, + required Future> Function(String query) getSuggestions, + required Widget Function(T payload) suggestionBuilder, +}) async { + final textController = TextEditingController(); + + await showDialog( + context: context, + builder: (context) { + bool okEnabled = false; + String? error; + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: Text(title), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TypeAheadField( + textFieldConfiguration: TextFieldConfiguration( + controller: textController, + onChanged: (value) => setState(() { + okEnabled = value.isNotEmpty; + error = null; + }), + autofocus: true, + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: inputLabel, + errorText: error, + ), + onSubmitted: (text) async { + setState(() => okEnabled = false); + final String? submitError = await onSubmitted(value: text); + setState(() => error = submitError); + }, + ), + suggestionsCallback: getSuggestions, + itemBuilder: (context, payload) => suggestionBuilder(payload), + onSuggestionSelected: (payload) async { + setState(() => okEnabled = false); + final String? submitError = await onSubmitted(payload: payload); + setState(() => error = submitError); + }, + hideOnEmpty: true, + hideOnLoading: true, + hideOnError: true, + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(AppLocalizations.of(context)!.cancel), + ), + const SizedBox(width: 5), + FilledButton( + onPressed: okEnabled + ? () async { + setState(() => okEnabled = false); + final String? submitError = await onSubmitted(value: textController.text); + setState(() => error = submitError); + } + : null, + child: Text(AppLocalizations.of(context)!.ok), + ), + ], + ), + ], + ), + ); + }, + ); + }, + ); +} diff --git a/lib/shared/link_preview_card.dart b/lib/shared/link_preview_card.dart index 176791d9b..f5b174f6c 100644 --- a/lib/shared/link_preview_card.dart +++ b/lib/shared/link_preview_card.dart @@ -254,7 +254,7 @@ class LinkPreviewCard extends StatelessWidget { if (communityName != null) { try { - await navigateToCommunityByName(context, communityName); + await navigateToCommunityPage(context, communityName: communityName); return; } catch (e) { // Ignore exception, if it's not a valid community we'll perform the next fallback @@ -265,7 +265,7 @@ class LinkPreviewCard extends StatelessWidget { if (username != null) { try { - await navigateToUserByName(context, username); + await navigateToUserPage(context, username: username); return; } catch (e) { // Ignore exception, if it's not a valid user, we'll perform the next fallback diff --git a/lib/user/bloc/user_settings_bloc.dart b/lib/user/bloc/user_settings_bloc.dart index 03a36f0aa..9ae7437dd 100644 --- a/lib/user/bloc/user_settings_bloc.dart +++ b/lib/user/bloc/user_settings_bloc.dart @@ -6,6 +6,8 @@ import 'package:stream_transform/stream_transform.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; +import 'package:thunder/utils/error_messages.dart'; +import 'package:thunder/utils/global_context.dart'; part 'user_settings_event.dart'; part 'user_settings_state.dart'; @@ -53,7 +55,7 @@ class UserSettingsBloc extends Bloc { )); } } catch (e) { - return emit(state.copyWith(status: UserSettingsStatus.failure, errorMessage: e.toString())); + return emit(state.copyWith(status: UserSettingsStatus.failure, errorMessage: e is LemmyApiException ? getErrorMessage(GlobalContext.context, e.message) : e.toString())); } } @@ -84,7 +86,9 @@ class UserSettingsBloc extends Bloc { personBeingBlocked: 0, )); } catch (e) { - return emit(state.copyWith(status: event.unblock ? UserSettingsStatus.failure : UserSettingsStatus.failedRevert, errorMessage: e.toString())); + return emit(state.copyWith( + status: event.unblock ? UserSettingsStatus.failure : UserSettingsStatus.failedRevert, + errorMessage: e is LemmyApiException ? getErrorMessage(GlobalContext.context, e.message) : e.toString())); } } @@ -115,7 +119,9 @@ class UserSettingsBloc extends Bloc { communityBeingBlocked: 0, )); } catch (e) { - return emit(state.copyWith(status: event.unblock ? UserSettingsStatus.failure : UserSettingsStatus.failedRevert, errorMessage: e.toString())); + return emit(state.copyWith( + status: event.unblock ? UserSettingsStatus.failure : UserSettingsStatus.failedRevert, + errorMessage: e is LemmyApiException ? getErrorMessage(GlobalContext.context, e.message) : e.toString())); } } } diff --git a/lib/user/pages/user_page.dart b/lib/user/pages/user_page.dart index a11cdfb17..49afa6600 100644 --- a/lib/user/pages/user_page.dart +++ b/lib/user/pages/user_page.dart @@ -3,11 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:swipeable_page_route/swipeable_page_route.dart'; +import 'package:thunder/account/bloc/account_bloc.dart'; import 'package:thunder/account/utils/profiles.dart'; import 'package:thunder/community/bloc/community_bloc.dart' as community; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/shared/snackbar.dart'; +import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/user/pages/user_page_success.dart'; import 'package:thunder/shared/error_message.dart'; import 'package:thunder/user/bloc/user_bloc.dart'; @@ -91,9 +93,17 @@ class _UserPageState extends State { padding: const EdgeInsets.fromLTRB(0.0, 4.0, 0, 4.0), child: IconButton( onPressed: () { + final AccountBloc accountBloc = context.read(); + final ThunderBloc thunderBloc = context.read(); Navigator.of(context).push( SwipeablePageRoute( - builder: (context) => UserSettingsPage(widget.userId), + builder: (context) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: accountBloc), + BlocProvider.value(value: thunderBloc), + ], + child: UserSettingsPage(widget.userId), + ), ), ); }, diff --git a/lib/user/pages/user_page_success.dart b/lib/user/pages/user_page_success.dart index 4dc2d7862..1268c1be6 100644 --- a/lib/user/pages/user_page_success.dart +++ b/lib/user/pages/user_page_success.dart @@ -304,8 +304,11 @@ class _UserPageSuccessState extends State with TickerProviderSt } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -322,7 +325,8 @@ class _UserPageSuccessState extends State with TickerProviderSt )); }, ), - ).whenComplete( + ) + .whenComplete( () async { timer.cancel(); @@ -395,8 +399,11 @@ class _UserPageSuccessState extends State with TickerProviderSt } }); - Navigator.of(context).push( + Navigator.of(context) + .push( SwipeablePageRoute( + canOnlySwipeFromEdge: true, + backGestureDetectionWidth: 45, builder: (context) { return MultiBlocProvider( providers: [ @@ -411,7 +418,8 @@ class _UserPageSuccessState extends State with TickerProviderSt )); }, ), - ).whenComplete( + ) + .whenComplete( () async { timer.cancel(); diff --git a/lib/user/pages/user_settings_page.dart b/lib/user/pages/user_settings_page.dart index edd6a3312..48bcfc8ed 100644 --- a/lib/user/pages/user_settings_page.dart +++ b/lib/user/pages/user_settings_page.dart @@ -1,10 +1,21 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:lemmy_api_client/v3.dart'; +import 'package:text_scroll/text_scroll.dart'; +import 'package:thunder/account/models/account.dart'; +import 'package:thunder/core/auth/helpers/fetch_account.dart'; +import 'package:thunder/core/singletons/lemmy_client.dart'; +import 'package:thunder/shared/community_icon.dart'; +import 'package:thunder/shared/input_dialogs.dart'; import 'package:thunder/shared/snackbar.dart'; +import 'package:thunder/shared/user_avatar.dart'; import 'package:thunder/user/bloc/user_settings_bloc.dart'; import 'package:thunder/utils/instance.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:thunder/utils/navigate_community.dart'; +import 'package:thunder/utils/navigate_user.dart'; class UserSettingsPage extends StatefulWidget { final int? userId; @@ -24,7 +35,7 @@ class _UserSettingsPageState extends State { appBar: AppBar( toolbarHeight: 70.0, centerTitle: false, - title: const AutoSizeText('Account Settings'), + title: AutoSizeText(AppLocalizations.of(context)!.accountSettings), scrolledUnderElevation: 0.0, ), body: MultiBlocProvider( @@ -36,13 +47,11 @@ class _UserSettingsPageState extends State { child: BlocConsumer( listener: (context, state) { if ((state.status == UserSettingsStatus.failure || state.status == UserSettingsStatus.failedRevert) && (state.personBeingBlocked != 0 || state.communityBeingBlocked != 0)) { - if (state.status == UserSettingsStatus.failure) { - showSnackbar( - context, - state.status == UserSettingsStatus.failure - ? AppLocalizations.of(context)!.failedToUnblock(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage) - : AppLocalizations.of(context)!.failedToBlock(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage)); - } + showSnackbar( + context, + state.status == UserSettingsStatus.failure + ? AppLocalizations.of(context)!.failedToUnblock(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage) + : AppLocalizations.of(context)!.failedToBlock(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage)); } else if (state.status == UserSettingsStatus.failure) { showSnackbar(context, AppLocalizations.of(context)!.failedToLoadBlocks(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage)); } @@ -73,10 +82,36 @@ class _UserSettingsPageState extends State { return SingleChildScrollView( child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, children: [ - const ListTile( - leading: Icon(Icons.person), - title: Text('Blocked Users'), + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListTile( + leading: const CircleAvatar( + radius: 16.0, + backgroundColor: Colors.transparent, + child: Icon(Icons.person), + ), + title: Text( + AppLocalizations.of(context)!.blockedUsers, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 12.0), + trailing: IconButton( + icon: Icon( + Icons.add_rounded, + semanticLabel: AppLocalizations.of(context)!.add, + ), + onPressed: () => showUserInputDialog( + context, + title: AppLocalizations.of(context)!.blockUser, + onUserSelected: (personViewSafe) { + context.read().add(UnblockPersonEvent(personId: personViewSafe.person.id, unblock: false)); + }, + ), + ), + ), ), AnimatedCrossFade( crossFadeState: state.status == UserSettingsStatus.initial ? CrossFadeState.showFirst : CrossFadeState.showSecond, @@ -91,6 +126,7 @@ class _UserSettingsPageState extends State { alignment: Alignment.centerLeft, child: state.personBlocks.isNotEmpty == true ? ListView.builder( + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: state.personBlocks.length, @@ -98,51 +134,91 @@ class _UserSettingsPageState extends State { return index == state.personBlocks.length ? Container() : Padding( - padding: const EdgeInsets.only(left: 42), - child: ListTile( - visualDensity: const VisualDensity(vertical: -2), - title: Tooltip( - message: '${state.personBlocks[index].name}@${fetchInstanceNameFromUrl(state.personBlocks[index].actorId) ?? '-'}', - preferBelow: false, - child: Text( - '${state.personBlocks[index].name}@${fetchInstanceNameFromUrl(state.personBlocks[index].actorId) ?? '-'}', - overflow: TextOverflow.ellipsis, + padding: const EdgeInsets.only(left: 10, right: 10), + child: Tooltip( + message: '${state.personBlocks[index].name}@${fetchInstanceNameFromUrl(state.personBlocks[index].actorId) ?? '-'}', + preferBelow: false, + child: Material( + child: InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () { + navigateToUserPage(context, username: '${state.personBlocks[index].name}@${fetchInstanceNameFromUrl(state.personBlocks[index].actorId)}'); + }, + child: ListTile( + leading: UserAvatar(person: state.personBlocks[index]), + visualDensity: const VisualDensity(vertical: -2), + title: Text( + state.personBlocks[index].displayName ?? state.personBlocks[index].name, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + '${state.personBlocks[index].name}@${fetchInstanceNameFromUrl(state.personBlocks[index].actorId) ?? '-'}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 12.0), + trailing: state.status == UserSettingsStatus.blocking && state.personBeingBlocked == state.personBlocks[index].id + ? const Padding( + padding: EdgeInsets.only(right: 12), + child: SizedBox( + width: 25, + height: 25, + child: CircularProgressIndicator(), + ), + ) + : IconButton( + icon: Icon( + Icons.clear, + semanticLabel: AppLocalizations.of(context)!.remove, + ), + onPressed: () { + context.read().add(UnblockPersonEvent(personId: state.personBlocks[index].id)); + }, + ), + ), ), ), - trailing: state.status == UserSettingsStatus.blocking && state.personBeingBlocked == state.personBlocks[index].id - ? const Padding( - padding: EdgeInsets.only(right: 12), - child: SizedBox( - width: 25, - height: 25, - child: CircularProgressIndicator(), - ), - ) - : IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - context.read().add(UnblockPersonEvent(personId: state.personBlocks[index].id)); - }, - ), ), ); }, ) : Padding( - padding: const EdgeInsets.only(left: 57, right: 20), + padding: const EdgeInsets.only(left: 70, right: 20), child: Text( - 'It looks like you have not blocked anybody.', + AppLocalizations.of(context)!.noUserBlocks, style: TextStyle(color: theme.hintColor), ), ), ), ), - const SizedBox( - height: 5, - ), - const ListTile( - leading: Icon(Icons.people_rounded), - title: Text('Blocked Communities'), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListTile( + leading: const CircleAvatar( + radius: 16.0, + backgroundColor: Colors.transparent, + child: Icon(Icons.people_rounded), + ), + title: Text( + AppLocalizations.of(context)!.blockedCommunities, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 12.0), + trailing: IconButton( + icon: Icon( + Icons.add_rounded, + semanticLabel: AppLocalizations.of(context)!.add, + ), + onPressed: () => showCommunityInputDialog( + context, + title: AppLocalizations.of(context)!.blockCommunity, + onCommunitySelected: (communityView) { + context.read().add(UnblockCommunityEvent(communityId: communityView.community.id, unblock: false)); + }, + ), + ), + ), ), AnimatedCrossFade( crossFadeState: state.status == UserSettingsStatus.initial ? CrossFadeState.showFirst : CrossFadeState.showSecond, @@ -157,6 +233,7 @@ class _UserSettingsPageState extends State { alignment: Alignment.centerLeft, child: state.communityBlocks.isNotEmpty == true ? ListView.builder( + padding: const EdgeInsets.only(bottom: 50), physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: state.communityBlocks.length, @@ -164,40 +241,59 @@ class _UserSettingsPageState extends State { return index == state.communityBlocks.length ? Container() : Padding( - padding: const EdgeInsets.only(left: 42), - child: ListTile( - visualDensity: const VisualDensity(vertical: -2), - title: Tooltip( - message: '${state.communityBlocks[index].name}@${fetchInstanceNameFromUrl(state.communityBlocks[index].actorId) ?? '-'}', - preferBelow: false, - child: Text( - '${state.communityBlocks[index].name}@${fetchInstanceNameFromUrl(state.communityBlocks[index].actorId) ?? '-'}', - overflow: TextOverflow.ellipsis, + padding: const EdgeInsets.only(left: 10, right: 10), + child: Tooltip( + message: '${state.communityBlocks[index].name}@${fetchInstanceNameFromUrl(state.communityBlocks[index].actorId) ?? '-'}', + preferBelow: false, + child: Material( + child: InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () { + navigateToCommunityPage(context, + communityName: '${state.communityBlocks[index].name}@${fetchInstanceNameFromUrl(state.communityBlocks[index].actorId)}'); + }, + child: ListTile( + leading: CommunityIcon(community: state.communityBlocks[index], radius: 16.0), + visualDensity: const VisualDensity(vertical: -2), + title: Text( + state.communityBlocks[index].title, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + '${state.communityBlocks[index].name}@${fetchInstanceNameFromUrl(state.communityBlocks[index].actorId) ?? '-'}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 12.0), + trailing: state.status == UserSettingsStatus.blocking && state.communityBeingBlocked == state.communityBlocks[index].id + ? const Padding( + padding: EdgeInsets.only(right: 12), + child: SizedBox( + width: 25, + height: 25, + child: CircularProgressIndicator(), + ), + ) + : IconButton( + icon: Icon( + Icons.clear, + semanticLabel: AppLocalizations.of(context)!.remove, + ), + onPressed: () { + context.read().add(UnblockCommunityEvent(communityId: state.communityBlocks[index].id)); + }, + ), + ), ), ), - trailing: state.status == UserSettingsStatus.blocking && state.communityBeingBlocked == state.communityBlocks[index].id - ? const Padding( - padding: EdgeInsets.only(right: 12), - child: SizedBox( - width: 25, - height: 25, - child: CircularProgressIndicator(), - ), - ) - : IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - context.read().add(UnblockCommunityEvent(communityId: state.communityBlocks[index].id)); - }, - ), ), ); }, ) : Padding( - padding: const EdgeInsets.only(left: 57, right: 20), + padding: const EdgeInsets.only(left: 70, right: 20), child: Text( - 'It looks like you have not blocked any communities.', + AppLocalizations.of(context)!.noCommunityBlocks, style: TextStyle(color: theme.hintColor), ), ), diff --git a/lib/user/widgets/comment_card.dart b/lib/user/widgets/comment_card.dart index aae2b75b0..17f48f589 100644 --- a/lib/user/widgets/comment_card.dart +++ b/lib/user/widgets/comment_card.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lemmy_api_client/v3.dart'; @@ -41,7 +43,8 @@ class CommentCard extends StatelessWidget { // To to specific post for now, in the future, will be best to scroll to the position of the comment await Navigator.of(context).push( SwipeablePageRoute( - backGestureDetectionStartOffset: 45, + backGestureDetectionStartOffset: Platform.isAndroid ? 45 : 0, + backGestureDetectionWidth: 45, canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isPostPage: true), builder: (context) => MultiBlocProvider( providers: [ @@ -115,36 +118,7 @@ class CommentCard extends StatelessWidget { const Divider(height: 20), const Row( mainAxisAlignment: MainAxisAlignment.end, - children: [ - // IconButton( - // onPressed: () { - // InboxBloc inboxBloc = context.read(); - - // showModalBottomSheet( - // isScrollControlled: true, - // context: context, - // showDragHandle: true, - // builder: (context) { - // return Padding( - // padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom + 40), - // child: FractionallySizedBox( - // heightFactor: 0.8, - // child: BlocProvider.value( - // value: inboxBloc, - // child: CreateCommentModal(comment: comment.comment, parentCommentAuthor: comment.creator.name), - // ), - // ), - // ); - // }, - // ); - // }, - // icon: const Icon( - // Icons.reply_rounded, - // semanticLabel: 'Reply', - // ), - // visualDensity: VisualDensity.compact, - // ), - ], + children: [], ) ], ), diff --git a/lib/user/widgets/user_sidebar.dart b/lib/user/widgets/user_sidebar.dart index d5de239d3..95d6aba35 100644 --- a/lib/user/widgets/user_sidebar.dart +++ b/lib/user/widgets/user_sidebar.dart @@ -13,6 +13,7 @@ import 'package:thunder/shared/snackbar.dart'; import 'package:thunder/user/widgets/user_sidebar_activity.dart'; import 'package:thunder/user/widgets/user_sidebar_stats.dart'; import 'package:thunder/utils/instance.dart'; +import 'package:thunder/utils/navigate_community.dart'; import '../../community/pages/community_page.dart'; import '../../shared/common_markdown_body.dart'; @@ -378,22 +379,7 @@ class _UserSidebarState extends State { for (var mods in widget.moderates!) GestureDetector( onTap: () { - account_bloc.AccountBloc accountBloc = context.read(); - AuthBloc authBloc = context.read(); - ThunderBloc thunderBloc = context.read(); - - Navigator.of(context).push( - SwipeablePageRoute( - builder: (context) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: accountBloc), - BlocProvider.value(value: authBloc), - BlocProvider.value(value: thunderBloc), - ], - child: CommunityPage(communityId: mods.community.id), - ), - ), - ); + navigateToCommunityPage(context, communityId: mods.community.id); }, child: Padding( padding: const EdgeInsets.only(bottom: 8.0), diff --git a/lib/utils/error_messages.dart b/lib/utils/error_messages.dart index 5cba63433..f2ca48240 100644 --- a/lib/utils/error_messages.dart +++ b/lib/utils/error_messages.dart @@ -6,6 +6,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; String? getErrorMessage(BuildContext context, String lemmyApiErrorCode) { return switch (lemmyApiErrorCode) { "cant_block_admin" => AppLocalizations.of(context)!.cantBlockAdmin, - _ => null, + "cant_block_yourself" => AppLocalizations.of(context)!.cantBlockYourself, + "only_mods_can_post_in_community" => AppLocalizations.of(context)!.onlyModsCanPostInCommunity, + _ => lemmyApiErrorCode, }; } diff --git a/lib/utils/navigate_community.dart b/lib/utils/navigate_community.dart index ef974cf20..755469b9b 100644 --- a/lib/utils/navigate_community.dart +++ b/lib/utils/navigate_community.dart @@ -1,27 +1,40 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lemmy_api_client/v3.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:swipeable_page_route/swipeable_page_route.dart'; + import 'package:thunder/account/models/account.dart'; +import 'package:thunder/community/pages/community_page.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/account/bloc/account_bloc.dart'; -import 'package:thunder/community/pages/community_page.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/utils/swipe.dart'; -Future navigateToCommunityByName(BuildContext context, String communityName) async { - // Get the id from the name - int? communityId; - Account? account = await fetchActiveProfileAccount(); - final getCommunityResponse = await LemmyClient.instance.lemmyApiV3.run(GetCommunity( - auth: account?.jwt, - name: communityName, - )); +/// Navigates to a [CommunityPage] with a given [communityName] or [communityId] +/// +/// Note: only one of [communityName] or [communityId] should be provided +/// If both are provided, the [communityId] will take precedence +/// +/// The [context] parameter should contain the following blocs within its widget tree: [AccountBloc], [AuthBloc], [ThunderBloc] +Future navigateToCommunityPage(BuildContext context, {String? communityName, int? communityId}) async { + if (communityName == null && communityId == null) return; // Return early since there's nothing to do + + int? _communityId = communityId; + + if (_communityId == null) { + // Get the id from the name + Account? account = await fetchActiveProfileAccount(); + + final getCommunityResponse = await LemmyClient.instance.lemmyApiV3.run(GetCommunity( + auth: account?.jwt, + name: communityName, + )); - communityId = getCommunityResponse.communityView.community.id; + _communityId = getCommunityResponse.communityView.community.id; + } // Push navigation AccountBloc accountBloc = context.read(); @@ -30,6 +43,7 @@ Future navigateToCommunityByName(BuildContext context, String communityNam Navigator.of(context).push( SwipeablePageRoute( + backGestureDetectionWidth: 45, canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), builder: (context) => MultiBlocProvider( providers: [ diff --git a/lib/utils/navigate_user.dart b/lib/utils/navigate_user.dart index a6e5408e6..883bff2f9 100644 --- a/lib/utils/navigate_user.dart +++ b/lib/utils/navigate_user.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lemmy_api_client/v3.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:swipeable_page_route/swipeable_page_route.dart'; + import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/auth/helpers/fetch_account.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; @@ -12,16 +13,28 @@ import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/user/pages/user_page.dart'; import 'package:thunder/utils/swipe.dart'; -Future navigateToUserByName(BuildContext context, String username) async { - // Get the id from the name - int? userId; - Account? account = await fetchActiveProfileAccount(); - final fullPersonView = await LemmyClient.instance.lemmyApiV3.run(GetPersonDetails( - auth: account?.jwt, - username: username, - )); +/// Navigates to a [UserPage] with a given [username] or [userId] +/// +/// Note: only one of [username] or [userId] should be provided +/// If both are provided, the [userId] will take precedence +/// +/// The [context] parameter should contain the following blocs within its widget tree: [AccountBloc], [AuthBloc], [ThunderBloc] +Future navigateToUserPage(BuildContext context, {String? username, int? userId}) async { + if (username == null && userId == null) return; // Return early since there's nothing to do + + int? _userId = userId; + + if (_userId == null) { + // Get the id from the name + Account? account = await fetchActiveProfileAccount(); + + final FullPersonView fullPersonView = await LemmyClient.instance.lemmyApiV3.run(GetPersonDetails( + auth: account?.jwt, + username: username, + )); - userId = fullPersonView.personView.person.id; + _userId = fullPersonView.personView.person.id; + } // Push navigation AccountBloc accountBloc = context.read(); @@ -30,6 +43,7 @@ Future navigateToUserByName(BuildContext context, String username) async { Navigator.of(context).push( SwipeablePageRoute( + backGestureDetectionWidth: 45, canOnlySwipeFromEdge: disableFullPageSwipe(isUserLoggedIn: authBloc.state.isLoggedIn, state: thunderBloc.state, isFeedPage: true), builder: (context) => MultiBlocProvider( providers: [ @@ -37,10 +51,7 @@ Future navigateToUserByName(BuildContext context, String username) async { BlocProvider.value(value: authBloc), BlocProvider.value(value: thunderBloc), ], - child: UserPage( - userId: userId, - username: username, - ), + child: UserPage(userId: userId, username: username), ), ), ); diff --git a/pubspec.lock b/pubspec.lock index d1433b86a..c6d90eec4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -431,6 +431,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + url: "https://pub.dev" + source: hosted + version: "5.4.1" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_launcher_icons: dependency: "direct main" description: @@ -489,6 +537,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: "direct main" + description: + name: flutter_typeahead + sha256: a3539f7a90246b152f569029dedcf0b842532d3f2a440701b520e0bf2acbcf42 + url: "https://pub.dev" + source: hosted + version: "4.6.2" flutter_web_plugins: dependency: transitive description: flutter @@ -905,6 +961,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.5" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: acfcd63c00ec3d5a7894b0e2a875893716d31958fe03f064734dba7dfd9113d9 + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" pointycastle: dependency: transitive description: @@ -1127,6 +1191,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + text_scroll: + dependency: "direct main" + description: + name: text_scroll + sha256: "7869d86a6fdd725dee56bdd150216a99f0372b82fbfcac319214dbd5f36e1908" + url: "https://pub.dev" + source: hosted + version: "0.2.0" translator: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a6bc88f00..af23e162f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,6 +90,8 @@ dependencies: flutter_file_dialog: ^3.0.2 dart_ping: ^9.0.0 dart_ping_ios: ^4.0.0 + flutter_typeahead: ^4.6.2 + text_scroll: ^0.2.0 dev_dependencies: flutter_test: