diff --git a/docs/BUGS.md b/docs/BUGS.md index b18a56c..33b8e30 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -15,9 +15,9 @@ updated: 2026-03-17 - [x] [8. Saved Posts — Accessible from Profile](#8-saved-posts--accessible-from-profile) - [x] [9. Saved Posts — Long Press for Local, Tap for Menu](#9-saved-posts--long-press-for-local-tap-for-menu) - [x] [10. Saved Posts — Show Save Counts](#10-saved-posts--show-save-counts) -- [ ] [11. Saved Posts — Cloud Save via AT Protocol](#11-saved-posts--cloud-save-via-at-protocol) -- [ ] [12. Failed Action Snackbar with Revert](#12-failed-action-snackbar-with-revert) -- [ ] [13. Delete Post — Remove from Feed](#13-delete-post--remove-from-feed) +- [x] [11. Saved Posts — Cloud Save via AT Protocol](#11-saved-posts--cloud-save-via-at-protocol) +- [x] [12. Failed Action Snackbar with Revert](#12-failed-action-snackbar-with-revert) +- [x] [13. Delete Post — Remove from Feed](#13-delete-post--remove-from-feed) ## 1. Post Thread Screen diff --git a/lib/features/feed/cubit/post_action_cubit.dart b/lib/features/feed/cubit/post_action_cubit.dart index 82017d9..e0420df 100644 --- a/lib/features/feed/cubit/post_action_cubit.dart +++ b/lib/features/feed/cubit/post_action_cubit.dart @@ -16,6 +16,7 @@ class PostActionState extends Equatable { this.repostUri, this.isLoadingLike = false, this.isLoadingRepost = false, + this.isDeleted = false, this.error, }); @@ -28,6 +29,7 @@ class PostActionState extends Equatable { final String? repostUri; final bool isLoadingLike; final bool isLoadingRepost; + final bool isDeleted; final String? error; PostActionState copyWith({ @@ -39,6 +41,7 @@ class PostActionState extends Equatable { String? repostUri, bool? isLoadingLike, bool? isLoadingRepost, + bool? isDeleted, String? error, }) { return PostActionState( @@ -51,6 +54,7 @@ class PostActionState extends Equatable { repostUri: repostUri ?? this.repostUri, isLoadingLike: isLoadingLike ?? this.isLoadingLike, isLoadingRepost: isLoadingRepost ?? this.isLoadingRepost, + isDeleted: isDeleted ?? this.isDeleted, error: error ?? this.error, ); } @@ -66,6 +70,7 @@ class PostActionState extends Equatable { repostUri, isLoadingLike, isLoadingRepost, + isDeleted, error, ]; } @@ -230,6 +235,7 @@ class PostActionCubit extends Cubit { Future deletePost() async { try { await _postActionRepository.deletePost(postUri: state.postUri); + emit(state.copyWith(isDeleted: true)); } catch (error) { log.e('Failed to delete post', error: error); emit(state.copyWith(error: 'Failed to delete post')); @@ -237,6 +243,21 @@ class PostActionCubit extends Cubit { } void clearError() { - emit(state.copyWith(error: null)); + if (state.error == null) return; + emit( + PostActionState( + postUri: state.postUri, + isLiked: state.isLiked, + isReposted: state.isReposted, + likeCount: state.likeCount, + repostCount: state.repostCount, + likeUri: state.likeUri, + repostUri: state.repostUri, + isLoadingLike: state.isLoadingLike, + isLoadingRepost: state.isLoadingRepost, + isDeleted: state.isDeleted, + error: null, + ), + ); } } diff --git a/lib/features/feed/presentation/home_feed_screen.dart b/lib/features/feed/presentation/home_feed_screen.dart index b556a0a..7027c3b 100644 --- a/lib/features/feed/presentation/home_feed_screen.dart +++ b/lib/features/feed/presentation/home_feed_screen.dart @@ -350,7 +350,15 @@ class _FeedListViewState extends State<_FeedListView> with AutomaticKeepAliveCli } final accountDid = context.read().state.tokens?.did ?? ''; - return PostCardWithActions(feedViewPost: _posts[index], accountDid: accountDid); + final post = _posts[index]; + return PostCardWithActions( + feedViewPost: post, + accountDid: accountDid, + onDeleted: () { + final uri = post.post.uri.toString(); + setState(() => _posts.removeWhere((p) => p.post.uri.toString() == uri)); + }, + ); }, ), ); diff --git a/lib/features/feed/presentation/widgets/post_card_with_actions.dart b/lib/features/feed/presentation/widgets/post_card_with_actions.dart index 3351150..e69218e 100644 --- a/lib/features/feed/presentation/widgets/post_card_with_actions.dart +++ b/lib/features/feed/presentation/widgets/post_card_with_actions.dart @@ -18,10 +18,11 @@ import 'package:lazurite/features/profile/data/profile_action_repository.dart'; import 'package:lazurite/features/profile/presentation/widgets/report_dialog.dart'; class PostCardWithActions extends StatelessWidget { - const PostCardWithActions({super.key, required this.feedViewPost, required this.accountDid}); + const PostCardWithActions({super.key, required this.feedViewPost, required this.accountDid, this.onDeleted}); final FeedViewPost feedViewPost; final String accountDid; + final VoidCallback? onDeleted; @override Widget build(BuildContext context) { @@ -41,27 +42,54 @@ class PostCardWithActions extends StatelessWidget { repostUri: viewer?.repost?.toString(), cache: ctx.read(), ), - child: _PostCardWithActionsContent(feedViewPost: feedViewPost, accountDid: accountDid), + child: _PostCardWithActionsContent(feedViewPost: feedViewPost, accountDid: accountDid, onDeleted: onDeleted), ); } } class _PostCardWithActionsContent extends StatelessWidget { - const _PostCardWithActionsContent({required this.feedViewPost, required this.accountDid}); + const _PostCardWithActionsContent({required this.feedViewPost, required this.accountDid, this.onDeleted}); final FeedViewPost feedViewPost; final String accountDid; + final VoidCallback? onDeleted; @override Widget build(BuildContext context) { return BlocListener( - listenWhen: (previous, current) => previous.error != current.error && current.error != null, + listenWhen: (previous, current) => + (previous.error != current.error && current.error != null) || + (!previous.isDeleted && current.isDeleted), listener: (context, state) { + if (state.isDeleted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Post deleted'), behavior: SnackBarBehavior.floating), + ); + onDeleted?.call(); + return; + } if (state.error != null) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(state.error!), behavior: SnackBarBehavior.floating)); - context.read().clearError(); + final cubit = context.read(); + final error = state.error!; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(error), + behavior: SnackBarBehavior.floating, + action: SnackBarAction( + label: 'Retry', + onPressed: () { + if (error.contains('like')) { + cubit.toggleLike(); + } else if (error.contains('repost')) { + cubit.toggleRepost(); + } else if (error.contains('delete')) { + cubit.deletePost(); + } + }, + ), + ), + ); + cubit.clearError(); } }, child: PostCard( diff --git a/test/features/feed/cubit/post_action_cubit_test.dart b/test/features/feed/cubit/post_action_cubit_test.dart index ed9ef51..b4ac952 100644 --- a/test/features/feed/cubit/post_action_cubit_test.dart +++ b/test/features/feed/cubit/post_action_cubit_test.dart @@ -245,12 +245,13 @@ void main() { group('deletePost', () { blocTest( - 'calls deletePost on repository', + 'emits isDeleted true on success', build: () { when(() => mockRepository.deletePost(postUri: testPostUri)).thenAnswer((_) async => {}); return PostActionCubit(postActionRepository: mockRepository, postUri: testPostUri, postCid: testPostCid); }, act: (cubit) => cubit.deletePost(), + expect: () => [isA().having((s) => s.isDeleted, 'isDeleted', isTrue)], verify: (_) { verify(() => mockRepository.deletePost(postUri: testPostUri)).called(1); }, @@ -266,6 +267,23 @@ void main() { expect: () => [isA().having((s) => s.error, 'error', 'Failed to delete post')], ); }); + + group('clearError', () { + blocTest( + 'clears error from state', + build: () => PostActionCubit(postActionRepository: mockRepository, postUri: testPostUri, postCid: testPostCid), + seed: () => const PostActionState(postUri: testPostUri, error: 'some error'), + act: (cubit) => cubit.clearError(), + expect: () => [isA().having((s) => s.error, 'error', isNull)], + ); + + blocTest( + 'does nothing when error is already null', + build: () => PostActionCubit(postActionRepository: mockRepository, postUri: testPostUri, postCid: testPostCid), + act: (cubit) => cubit.clearError(), + expect: () => [], + ); + }); }); group('PostActionCubit cache integration', () { @@ -419,6 +437,7 @@ void main() { repostUri: testRepostUri, isLoadingLike: true, isLoadingRepost: false, + isDeleted: false, error: 'test error', ); @@ -432,6 +451,7 @@ void main() { repostUri: testRepostUri, isLoadingLike: true, isLoadingRepost: false, + isDeleted: false, error: 'test error', );