From b874799768033d16cd07270435f77892b0016b2e Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:09:44 +0200 Subject: [PATCH 01/27] feat: add ability to delete own post from feed --- ios/Podfile.lock | 58 ++++++++-------- .../delete_feed_item_menu.dart | 68 +++++++++++++++++++ .../feed/views/components/post/post.dart | 33 ++++++++- .../user_info_menu/user_info_menu_item.dart | 6 +- lib/l10n/app_en.arb | 1 + 5 files changed, 135 insertions(+), 31 deletions(-) create mode 100644 lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4a611e71e..5664631f1 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -361,7 +361,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/video_player_avfoundation/darwin" SPEC CHECKSUMS: - audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf + audio_waveforms: cd736909ebc6b6a164eb74701d8c705ce3241e1c BanubaARCloudSDK: 40da62afeea642301ca8092600c9bb9cf67fe30e BanubaAudioBrowserSDK: 3a92807e60daa70dcaa1e76920abab97e7a73bf4 BanubaLicenseServicingSDK: f2b51a4288d6b9796063855f5db6dc227819de22 @@ -384,48 +384,48 @@ SPEC CHECKSUMS: BNBSdkApi: f773621958cf0f7776e18f90b96ace13b29d84e2 BNBSdkCore: cc7711127dbcaf392d0a25f4a858d98ffc5c9213 BNBSkin: 3a741bbba57a2a860fe4a69dbd597d7d9aea5793 - camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf - device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 external_app_launcher: 3411245965270a74040a3de17e27bd02b8abb905 ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b - ffmpeg_kit_flutter_full_gpl: ce18b888487c05c46ed252cd2e7956812f2e3bd1 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 - file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 + ffmpeg_kit_flutter_full_gpl: 8d15c14c0c3aba616fac04fe44b3d27d02e3c330 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 - flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 - flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 - image_cropper: 5f162dcf988100dc1513f9c6b7eb42cd6fbf9156 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - ion_screenshot_detector: 6981b7ec0add023ce57337882e28dc4b80caad4b - local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 + flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + ion_screenshot_detector: bd85296e1347b5bf5acf71407a6d9b7933bfe604 + local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: 566e1b7a2f3900e4b0020914ad3fc051dcc95596 - passkeys_ios: 939d7d44f825048c8dffd4644f52444164c80ecd - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413 - qr_code_scanner: d77f94ecc9abf96d9b9b8fc04ef13f611e5a147a - quill_native_bridge: fd2819cf6da02fb6cbf9de37835f96e798e145eb + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + passkeys_ios: fdae8c06e2178a9fcb9261a6cb21fb9a06a81d53 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a + qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e + quill_native_bridge: e5afa7d49c08cf68c52a5e23bc272eba6925c622 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 - shake_gesture_ios: 85dce5e26785da11cf73e0234a5f4028f69afeef - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3 + shake_gesture_ios: 64f1f579f314c58445761992a123111b3d7b3492 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqlite3: 7559e33dae4c78538df563795af3a86fc887ee71 - sqlite3_flutter_libs: f0b59f6bb2a18597d0796558725007e5a7428397 + sqlite3_flutter_libs: 1b4e98da20ebd4e9b1240269b78cdcf492dbe9f3 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 - ua_client_hints: ef4ddde0e2b2be5f0731a31721c4cbbb889b1aa4 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - ve_sdk_flutter: e23c4a6f1b84eeacee1daf2a81ba0f0694ed76f1 + ua_client_hints: 3b617011e47bea4b1ea65647efa12860b7280ad5 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + ve_sdk_flutter: c90ec3fc424953a7b82cd1edd5143bc756899e92 VEEffectsSDK: 40dca6a76aeff8630ce1b49ef7643d2a2655dd0f VEExportSDK: 32e97cf98ecbf86aed563fbd4c9131d7bbddbe92 VEPlaybackSDK: 65bb7377c9c8524557d7b5d6723e321fc16fc4a1 - video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 VideoEditor: b3e55fe6a6589e715365da822172f54b6fea8169 PODFILE CHECKSUM: f19afb20048b05a04dbc809ad7d199f1df053e9a diff --git a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart new file mode 100644 index 000000000..165dad923 --- /dev/null +++ b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/components/overlay_menu/overlay_menu.dart'; +import 'package:ion/app/components/overlay_menu/overlay_menu_container.dart'; +import 'package:ion/app/extensions/asset_gen_image.dart'; +import 'package:ion/app/extensions/build_context.dart'; +import 'package:ion/app/extensions/num.dart'; +import 'package:ion/app/extensions/theme_data.dart'; +import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; +import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu_item.dart'; +import 'package:ion/app/features/ion_connect/model/deletion_request.c.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_notifier.c.dart'; +import 'package:ion/generated/assets.gen.dart'; + +class DeleteFeedItemMenu extends ConsumerWidget { + const DeleteFeedItemMenu({ + required this.postEntity, + this.iconColor, + super.key, + }); + + static double get iconSize => 20.0.s; + + final PostEntity postEntity; + final Color? iconColor; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return OverlayMenu( + menuBuilder: (closeMenu) => Column( + children: [ + OverlayMenuContainer( + child: UserInfoMenuItem( + label: context.i18n.post_menu_delete, + labelColor: context.theme.appColors.attentionRed, + icon: Assets.svg.iconBlockDelete.icon( + size: iconSize, + color: context.theme.appColors.attentionRed, + ), + onPressed: () async { + closeMenu(); + await _deletePost(ref, postEntity); + }, + ), + ), + ], + ), + child: Assets.svg.iconMorePopup.icon( + color: iconColor ?? context.theme.appColors.onTertararyBackground, + ), + ); + } + + Future _deletePost(WidgetRef ref, PostEntity postEntity) async { + final deletionRequest = DeletionRequest( + events: [EventToDelete(eventId: postEntity.id, kind: PostEntity.kind)], + ); + + await ref + .read(ionConnectNotifierProvider.notifier) + .sendEntityData(deletionRequest, cache: false); + + ref.read(ionConnectCacheProvider.notifier).remove(postEntity.cacheKey); + } +} diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 9b6eaa4e2..a2675bfe2 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -6,17 +6,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/components/counter_items_footer/counter_items_footer.dart'; import 'package:ion/app/components/skeleton/skeleton.dart'; import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/auth/providers/auth_provider.c.dart'; import 'package:ion/app/features/feed/data/models/entities/article_data.c.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; import 'package:ion/app/features/feed/views/components/article/article.dart'; +import 'package:ion/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart'; import 'package:ion/app/features/feed/views/components/post/components/post_body/post_body.dart'; import 'package:ion/app/features/feed/views/components/post/post_skeleton.dart'; import 'package:ion/app/features/feed/views/components/quoted_entity_frame/quoted_entity_frame.dart'; import 'package:ion/app/features/feed/views/components/user_info/user_info.dart'; import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_entity_provider.c.dart'; +import 'package:ion/app/features/user/model/user_metadata.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; +import 'package:ion/app/services/logger/logger.dart'; class Post extends ConsumerWidget { const Post({ @@ -38,12 +43,36 @@ class Post extends ConsumerWidget { .watch(ionConnectEntityProvider(eventReference: eventReference)) .valueOrNull as PostEntity?; + final userMetadata = ref.watch( + ionConnectCacheProvider.select( + cacheSelector( + UserMetadataEntity.cacheKeyBuilder(pubkey: eventReference.pubkey), + ), + ), + ); + if (postEntity == null) { return const Skeleton(child: PostSkeleton()); } + final isOwnedByCurrentUser = ref.watch(isCurrentUserSelectorProvider(postEntity.masterPubkey)); + final framedEvent = _getFramedEventReference(postEntity); + if (postEntity.data.content.toString().contains('Test post to delete')) { + Logger.log('POST WHERE I AM AUTHOR'); + Logger.log('postEntity: $postEntity'); + Logger.log('eventReference pubkey: ${eventReference.pubkey}'); + Logger.log('userMetadata: $userMetadata'); + Logger.log('isOwnedByCurrentUser: $isOwnedByCurrentUser'); + } else { + Logger.log('POST WHERE I AM NOT AUTHOR'); + Logger.log('postEntity: $postEntity'); + Logger.log('eventReference pubkey: ${eventReference.pubkey}'); + Logger.log('userMetadata: $userMetadata'); + Logger.log('isOwnedByCurrentUser Some other post: $isOwnedByCurrentUser'); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -51,7 +80,9 @@ class Post extends ConsumerWidget { header ?? UserInfo( pubkey: eventReference.pubkey, - trailing: UserInfoMenu(pubkey: eventReference.pubkey), + trailing: isOwnedByCurrentUser + ? DeleteFeedItemMenu(postEntity: postEntity) + : UserInfoMenu(pubkey: eventReference.pubkey), ), SizedBox(height: 10.0.s), PostBody(postEntity: postEntity), diff --git a/lib/app/features/feed/views/components/user_info_menu/user_info_menu_item.dart b/lib/app/features/feed/views/components/user_info_menu/user_info_menu_item.dart index c638249b3..2e83afa50 100644 --- a/lib/app/features/feed/views/components/user_info_menu/user_info_menu_item.dart +++ b/lib/app/features/feed/views/components/user_info_menu/user_info_menu_item.dart @@ -8,12 +8,14 @@ class UserInfoMenuItem extends StatelessWidget { required this.label, required this.icon, required this.onPressed, + this.labelColor, super.key, }); final String label; final Widget icon; final VoidCallback onPressed; + final Color? labelColor; @override Widget build(BuildContext context) { @@ -34,7 +36,9 @@ class UserInfoMenuItem extends StatelessWidget { label, maxLines: 1, overflow: TextOverflow.ellipsis, - style: textStyles.subtitle3.copyWith(color: colors.primaryText), + style: textStyles.subtitle3.copyWith( + color: labelColor ?? colors.primaryText, + ), ), ), icon, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 05143c8ed..44f4731d2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -475,6 +475,7 @@ "post_menu_block_nickname": "Block @{nickname}", "post_menu_unblock_nickname": "Unblock @{nickname}", "post_menu_report_post": "Report post", + "post_menu_delete": "Delete", "protect_account_header_security": "Security", "protect_account_title_secure_account": "Secure your account", "protect_account_description_secure_account": "Securing your account ensures you never lose access to your data and funds", From 6e4638c831de848ddbb4d111fc61d8ed4d477bb8 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:07:40 +0200 Subject: [PATCH 02/27] feat: clean up and rebuild feed after deletion --- .../delete_feed_item_menu.dart | 7 +++++++ .../features/feed/views/components/post/post.dart | 15 --------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart index 165dad923..721ab597f 100644 --- a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart +++ b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart @@ -9,8 +9,10 @@ import 'package:ion/app/extensions/build_context.dart'; import 'package:ion/app/extensions/num.dart'; import 'package:ion/app/extensions/theme_data.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; +import 'package:ion/app/features/feed/providers/feed_posts_data_source_provider.c.dart'; import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu_item.dart'; import 'package:ion/app/features/ion_connect/model/deletion_request.c.dart'; +import 'package:ion/app/features/ion_connect/providers/entities_paged_data_provider.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_notifier.c.dart'; import 'package:ion/generated/assets.gen.dart'; @@ -64,5 +66,10 @@ class DeleteFeedItemMenu extends ConsumerWidget { .sendEntityData(deletionRequest, cache: false); ref.read(ionConnectCacheProvider.notifier).remove(postEntity.cacheKey); + + final dataSources = ref.read(feedPostsDataSourceProvider) ?? []; + if (dataSources.isNotEmpty) { + ref.invalidate(entitiesPagedDataProvider(dataSources)); + } } } diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index a2675bfe2..30ccd8d5f 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -21,7 +21,6 @@ import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart' import 'package:ion/app/features/ion_connect/providers/ion_connect_entity_provider.c.dart'; import 'package:ion/app/features/user/model/user_metadata.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; -import 'package:ion/app/services/logger/logger.dart'; class Post extends ConsumerWidget { const Post({ @@ -59,20 +58,6 @@ class Post extends ConsumerWidget { final framedEvent = _getFramedEventReference(postEntity); - if (postEntity.data.content.toString().contains('Test post to delete')) { - Logger.log('POST WHERE I AM AUTHOR'); - Logger.log('postEntity: $postEntity'); - Logger.log('eventReference pubkey: ${eventReference.pubkey}'); - Logger.log('userMetadata: $userMetadata'); - Logger.log('isOwnedByCurrentUser: $isOwnedByCurrentUser'); - } else { - Logger.log('POST WHERE I AM NOT AUTHOR'); - Logger.log('postEntity: $postEntity'); - Logger.log('eventReference pubkey: ${eventReference.pubkey}'); - Logger.log('userMetadata: $userMetadata'); - Logger.log('isOwnedByCurrentUser Some other post: $isOwnedByCurrentUser'); - } - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 6aa66ad2ac372c2750024f35cf7743bbcdadd236 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:30:18 +0200 Subject: [PATCH 03/27] fix: unused var --- lib/app/features/feed/views/components/post/post.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 30ccd8d5f..3b4f99fb7 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -17,9 +17,7 @@ import 'package:ion/app/features/feed/views/components/quoted_entity_frame/quote import 'package:ion/app/features/feed/views/components/user_info/user_info.dart'; import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; -import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_entity_provider.c.dart'; -import 'package:ion/app/features/user/model/user_metadata.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; class Post extends ConsumerWidget { @@ -42,14 +40,6 @@ class Post extends ConsumerWidget { .watch(ionConnectEntityProvider(eventReference: eventReference)) .valueOrNull as PostEntity?; - final userMetadata = ref.watch( - ionConnectCacheProvider.select( - cacheSelector( - UserMetadataEntity.cacheKeyBuilder(pubkey: eventReference.pubkey), - ), - ), - ); - if (postEntity == null) { return const Skeleton(child: PostSkeleton()); } From 8c80ad5d8866626d72100d9cf7af9a29776e6e2b Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Mon, 13 Jan 2025 19:05:48 +0200 Subject: [PATCH 04/27] feat: handle feed update after deletion --- .../delete_feed_item_menu/delete_feed_item_menu.dart | 2 +- .../providers/entities_paged_data_provider.c.dart | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart index 721ab597f..c682519f6 100644 --- a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart +++ b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart @@ -69,7 +69,7 @@ class DeleteFeedItemMenu extends ConsumerWidget { final dataSources = ref.read(feedPostsDataSourceProvider) ?? []; if (dataSources.isNotEmpty) { - ref.invalidate(entitiesPagedDataProvider(dataSources)); + await ref.read(entitiesPagedDataProvider(dataSources).notifier).deleteEntity(postEntity); } } } diff --git a/lib/app/features/ion_connect/providers/entities_paged_data_provider.c.dart b/lib/app/features/ion_connect/providers/entities_paged_data_provider.c.dart index 2679c78da..1eee03175 100644 --- a/lib/app/features/ion_connect/providers/entities_paged_data_provider.c.dart +++ b/lib/app/features/ion_connect/providers/entities_paged_data_provider.c.dart @@ -75,6 +75,18 @@ class EntitiesPagedData extends _$EntitiesPagedData { ); } + Future deleteEntity(IonConnectEntity postEntity) async { + final currentState = state; + if (currentState == null) return; + + final oldItems = currentState.data.items ?? {}; + final updatedItems = oldItems.where((entity) => entity.id != postEntity.id).toSet(); + + state = currentState.copyWith( + data: currentState.data.copyWith(items: updatedItems), + ); + } + Future> _fetchEntitiesFromDataSource( EntitiesDataSource dataSource, ) async { From 0c6f315e5384e9c43a9861516f9a31bdb6016104 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Mon, 13 Jan 2025 21:34:12 +0200 Subject: [PATCH 05/27] fix: close post details screen on delete --- .../delete_feed_item_menu/delete_feed_item_menu.dart | 10 +++++++--- lib/app/features/feed/views/components/post/post.dart | 4 +++- .../pages/post_details_page/post_details_page.dart | 8 +++++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart index c682519f6..014c61b7e 100644 --- a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart +++ b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart @@ -21,6 +21,7 @@ class DeleteFeedItemMenu extends ConsumerWidget { const DeleteFeedItemMenu({ required this.postEntity, this.iconColor, + this.onDelete, super.key, }); @@ -28,6 +29,7 @@ class DeleteFeedItemMenu extends ConsumerWidget { final PostEntity postEntity; final Color? iconColor; + final VoidCallback? onDelete; @override Widget build(BuildContext context, WidgetRef ref) { @@ -45,6 +47,7 @@ class DeleteFeedItemMenu extends ConsumerWidget { onPressed: () async { closeMenu(); await _deletePost(ref, postEntity); + onDelete?.call(); }, ), ), @@ -67,9 +70,10 @@ class DeleteFeedItemMenu extends ConsumerWidget { ref.read(ionConnectCacheProvider.notifier).remove(postEntity.cacheKey); - final dataSources = ref.read(feedPostsDataSourceProvider) ?? []; - if (dataSources.isNotEmpty) { - await ref.read(entitiesPagedDataProvider(dataSources).notifier).deleteEntity(postEntity); + final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; + + if (feedDataSources.isNotEmpty) { + await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(postEntity); } } } diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 3b4f99fb7..28c685cf6 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -26,6 +26,7 @@ class Post extends ConsumerWidget { this.header, this.footer, this.showParent = false, + this.onDelete, super.key, }); @@ -33,6 +34,7 @@ class Post extends ConsumerWidget { final bool showParent; final Widget? header; final Widget? footer; + final VoidCallback? onDelete; @override Widget build(BuildContext context, WidgetRef ref) { @@ -56,7 +58,7 @@ class Post extends ConsumerWidget { UserInfo( pubkey: eventReference.pubkey, trailing: isOwnedByCurrentUser - ? DeleteFeedItemMenu(postEntity: postEntity) + ? DeleteFeedItemMenu(postEntity: postEntity, onDelete: onDelete) : UserInfoMenu(pubkey: eventReference.pubkey), ), SizedBox(height: 10.0.s), diff --git a/lib/app/features/feed/views/pages/post_details_page/post_details_page.dart b/lib/app/features/feed/views/pages/post_details_page/post_details_page.dart index 5f302f2f1..1540e9450 100644 --- a/lib/app/features/feed/views/pages/post_details_page/post_details_page.dart +++ b/lib/app/features/feed/views/pages/post_details_page/post_details_page.dart @@ -1,6 +1,7 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; import 'package:ion/app/components/separated/separator.dart'; @@ -40,7 +41,12 @@ class PostDetailsPage extends ConsumerWidget { slivers: [ SliverToBoxAdapter( child: ScreenSideOffset.small( - child: Post(eventReference: eventReference), + child: Post( + eventReference: eventReference, + onDelete: () { + context.pop(); + }, + ), ), ), SliverToBoxAdapter(child: FeedListSeparator()), From 68d4246c00741fb058e2bfe31c671f94926e43dd Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:22:55 +0200 Subject: [PATCH 06/27] feat: article deletion from the feed --- .../components/article_list_item.dart | 2 +- .../feed/views/components/article/article.dart | 16 +++++++++++++--- .../delete_feed_item_menu.dart | 17 +++++++++-------- .../feed/views/components/post/post.dart | 6 +++++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/lib/app/features/components/entities_list/components/article_list_item.dart b/lib/app/features/components/entities_list/components/article_list_item.dart index 16c7cee4a..d1fc07abc 100644 --- a/lib/app/features/components/entities_list/components/article_list_item.dart +++ b/lib/app/features/components/entities_list/components/article_list_item.dart @@ -18,7 +18,7 @@ class ArticleListItem extends ConsumerWidget { final eventReference = EventReference.fromIonConnectEntity(article); return Padding( - padding: EdgeInsets.only(top: 12.0.s, bottom: 12.0.s, right: 16.0.s), + padding: EdgeInsets.only(top: 12.0.s, bottom: 24.0.s, right: 16.0.s), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => diff --git a/lib/app/features/feed/views/components/article/article.dart b/lib/app/features/feed/views/components/article/article.dart index 40380cdc0..38790aa1a 100644 --- a/lib/app/features/feed/views/components/article/article.dart +++ b/lib/app/features/feed/views/components/article/article.dart @@ -4,10 +4,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/components/skeleton/skeleton.dart'; import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/auth/providers/auth_provider.c.dart'; import 'package:ion/app/features/components/entities_list/components/bookmark_button/bookmark_button.dart'; import 'package:ion/app/features/feed/data/models/entities/article_data.c.dart'; import 'package:ion/app/features/feed/views/components/article/components/article_footer/article_footer.dart'; import 'package:ion/app/features/feed/views/components/article/components/article_image/article_image.dart'; +import 'package:ion/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart'; import 'package:ion/app/features/feed/views/components/post/post_skeleton.dart'; import 'package:ion/app/features/feed/views/components/user_info/user_info.dart'; import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu.dart'; @@ -28,9 +30,8 @@ class Article extends ConsumerWidget { return Article(eventReference: eventReference, showActionButtons: false); } - final bool showActionButtons; - final EventReference eventReference; + final bool showActionButtons; @override Widget build(BuildContext context, WidgetRef ref) { @@ -42,6 +43,9 @@ class Article extends ConsumerWidget { return const Skeleton(child: PostSkeleton()); } + final isOwnedByCurrentUser = + ref.watch(isCurrentUserSelectorProvider(articleEntity.masterPubkey)); + return ColoredBox( color: context.theme.appColors.onPrimaryAccent, child: IntrinsicHeight( @@ -69,7 +73,13 @@ class Article extends ConsumerWidget { ? Row( children: [ BookmarkButton(eventReference: eventReference), - UserInfoMenu(pubkey: eventReference.pubkey), + if (isOwnedByCurrentUser) + DeleteFeedItemMenu( + entity: articleEntity, + kind: ArticleEntity.kind, + ) + else + UserInfoMenu(pubkey: eventReference.pubkey), ], ) : null, diff --git a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart index 014c61b7e..6d463e2d6 100644 --- a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart +++ b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart @@ -8,7 +8,6 @@ import 'package:ion/app/extensions/asset_gen_image.dart'; import 'package:ion/app/extensions/build_context.dart'; import 'package:ion/app/extensions/num.dart'; import 'package:ion/app/extensions/theme_data.dart'; -import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; import 'package:ion/app/features/feed/providers/feed_posts_data_source_provider.c.dart'; import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu_item.dart'; import 'package:ion/app/features/ion_connect/model/deletion_request.c.dart'; @@ -19,7 +18,8 @@ import 'package:ion/generated/assets.gen.dart'; class DeleteFeedItemMenu extends ConsumerWidget { const DeleteFeedItemMenu({ - required this.postEntity, + required this.entity, + required this.kind, this.iconColor, this.onDelete, super.key, @@ -27,7 +27,8 @@ class DeleteFeedItemMenu extends ConsumerWidget { static double get iconSize => 20.0.s; - final PostEntity postEntity; + final CacheableEntity entity; + final int kind; final Color? iconColor; final VoidCallback? onDelete; @@ -46,7 +47,7 @@ class DeleteFeedItemMenu extends ConsumerWidget { ), onPressed: () async { closeMenu(); - await _deletePost(ref, postEntity); + await _deleteEntity(ref, entity); onDelete?.call(); }, ), @@ -59,21 +60,21 @@ class DeleteFeedItemMenu extends ConsumerWidget { ); } - Future _deletePost(WidgetRef ref, PostEntity postEntity) async { + Future _deleteEntity(WidgetRef ref, CacheableEntity entity) async { final deletionRequest = DeletionRequest( - events: [EventToDelete(eventId: postEntity.id, kind: PostEntity.kind)], + events: [EventToDelete(eventId: entity.id, kind: kind)], ); await ref .read(ionConnectNotifierProvider.notifier) .sendEntityData(deletionRequest, cache: false); - ref.read(ionConnectCacheProvider.notifier).remove(postEntity.cacheKey); + ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; if (feedDataSources.isNotEmpty) { - await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(postEntity); + await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); } } } diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 28c685cf6..0c3824526 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -58,7 +58,11 @@ class Post extends ConsumerWidget { UserInfo( pubkey: eventReference.pubkey, trailing: isOwnedByCurrentUser - ? DeleteFeedItemMenu(postEntity: postEntity, onDelete: onDelete) + ? DeleteFeedItemMenu( + entity: postEntity, + onDelete: onDelete, + kind: PostEntity.kind, + ) : UserInfoMenu(pubkey: eventReference.pubkey), ), SizedBox(height: 10.0.s), From 5d70bb235b95082457ce6dfc806b70510fb052cd Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:14:26 +0200 Subject: [PATCH 07/27] feat: delete entity provider and reply deletion --- lib/app/exceptions/exceptions.dart | 5 ++ .../providers/delete_entity_provider.c.dart | 61 +++++++++++++++++++ .../feed/providers/replies_provider.c.dart | 20 +++++- .../delete_feed_item_menu.dart | 25 +------- 4 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 lib/app/features/feed/providers/delete_entity_provider.c.dart diff --git a/lib/app/exceptions/exceptions.dart b/lib/app/exceptions/exceptions.dart index 54819affb..63ef1a5df 100644 --- a/lib/app/exceptions/exceptions.dart +++ b/lib/app/exceptions/exceptions.dart @@ -227,3 +227,8 @@ class ForceUpdateCouldntLaunchUrlException extends IONException { class ForceUpdateFetchConfigException extends IONException { ForceUpdateFetchConfigException() : super(10047, 'Failed to get version config'); } + +class DeleteEntityUnsupportedTypeException extends IONException { + DeleteEntityUnsupportedTypeException() + : super(10046, 'Failed to delete entity, unsupported type'); +} diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart new file mode 100644 index 000000000..7c80d185b --- /dev/null +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:ion/app/exceptions/exceptions.dart'; +import 'package:ion/app/features/feed/data/models/entities/article_data.c.dart'; +import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; +import 'package:ion/app/features/feed/providers/feed_posts_data_source_provider.c.dart'; +import 'package:ion/app/features/feed/providers/replies_provider.c.dart'; +import 'package:ion/app/features/ion_connect/model/deletion_request.c.dart'; +import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; +import 'package:ion/app/features/ion_connect/providers/entities_paged_data_provider.c.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_notifier.c.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'delete_entity_provider.c.g.dart'; + +@riverpod +class DeleteEntity extends _$DeleteEntity { + @override + Future build(CacheableEntity entity) async { + return; + } + + Future delete() async { + final entityKind = switch (entity) { + PostEntity() => PostEntity.kind, + ArticleEntity() => ArticleEntity.kind, + _ => throw DeleteEntityUnsupportedTypeException(), + }; + + final deletionRequest = DeletionRequest( + events: [EventToDelete(eventId: entity.id, kind: entityKind)], + ); + + await ref + .read(ionConnectNotifierProvider.notifier) + .sendEntityData(deletionRequest, cache: false); + + ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); + + if (entity case final PostEntity post when post.data.parentEvent != null) { + // Post reply + await ref + .read( + repliesProvider( + EventReference( + eventId: post.data.parentEvent!.eventId, + pubkey: post.data.parentEvent!.pubkey, + ), + ).notifier, + ) + .deleteReply(entity: entity); + } else { + // Post or Article + final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; + if (feedDataSources.isNotEmpty) { + await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); + } + } + } +} diff --git a/lib/app/features/feed/providers/replies_provider.c.dart b/lib/app/features/feed/providers/replies_provider.c.dart index 9d344d30e..5c1ebe85d 100644 --- a/lib/app/features/feed/providers/replies_provider.c.dart +++ b/lib/app/features/feed/providers/replies_provider.c.dart @@ -19,7 +19,7 @@ class Replies extends _$Replies { final subscription = ref .watch(ionConnectCacheStreamProvider) - .where((entity) => _isReply(entity, eventReference)) + .where((entity) => isReply(entity, eventReference)) .distinct() .listen((entity) { state = state?.copyWith.data(items: {entity, ...state?.data.items ?? {}}); @@ -29,7 +29,23 @@ class Replies extends _$Replies { return entitiesPagedData; } - bool _isReply(IonConnectEntity entity, EventReference parentEventReference) { + bool isReply(IonConnectEntity entity, EventReference parentEventReference) { return entity is PostEntity && entity.data.parentEvent?.eventId == parentEventReference.eventId; } + + Future deleteReply({ + required CacheableEntity entity, + }) async { + final currentState = state; + if (currentState != null) { + final updatedItems = + currentState.data.items?.where((entity) => entity.id != entity.id).toSet() ?? {}; + state = currentState.copyWith( + data: currentState.data.copyWith( + items: updatedItems, + pagination: currentState.data.pagination, + ), + ); + } + } } diff --git a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart index 6d463e2d6..2fc08e59e 100644 --- a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart +++ b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart @@ -8,12 +8,9 @@ import 'package:ion/app/extensions/asset_gen_image.dart'; import 'package:ion/app/extensions/build_context.dart'; import 'package:ion/app/extensions/num.dart'; import 'package:ion/app/extensions/theme_data.dart'; -import 'package:ion/app/features/feed/providers/feed_posts_data_source_provider.c.dart'; +import 'package:ion/app/features/feed/providers/delete_entity_provider.c.dart'; import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu_item.dart'; -import 'package:ion/app/features/ion_connect/model/deletion_request.c.dart'; -import 'package:ion/app/features/ion_connect/providers/entities_paged_data_provider.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; -import 'package:ion/app/features/ion_connect/providers/ion_connect_notifier.c.dart'; import 'package:ion/generated/assets.gen.dart'; class DeleteFeedItemMenu extends ConsumerWidget { @@ -47,7 +44,7 @@ class DeleteFeedItemMenu extends ConsumerWidget { ), onPressed: () async { closeMenu(); - await _deleteEntity(ref, entity); + await ref.read(deleteEntityProvider(entity).notifier).delete(); onDelete?.call(); }, ), @@ -59,22 +56,4 @@ class DeleteFeedItemMenu extends ConsumerWidget { ), ); } - - Future _deleteEntity(WidgetRef ref, CacheableEntity entity) async { - final deletionRequest = DeletionRequest( - events: [EventToDelete(eventId: entity.id, kind: kind)], - ); - - await ref - .read(ionConnectNotifierProvider.notifier) - .sendEntityData(deletionRequest, cache: false); - - ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); - - final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; - - if (feedDataSources.isNotEmpty) { - await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); - } - } } From b16a49784fbd9e5cd878287a03ed3b1d33c4b1f8 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:27:09 +0200 Subject: [PATCH 08/27] feat: delete article from article details screen --- .../views/components/article/article.dart | 1 - .../delete_feed_item_menu.dart | 2 -- .../feed/views/components/post/post.dart | 1 - .../article_details_page.dart | 2 +- .../components/user_biography.dart | 30 ++++++++++++++----- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/app/features/feed/views/components/article/article.dart b/lib/app/features/feed/views/components/article/article.dart index 38790aa1a..82ccd10de 100644 --- a/lib/app/features/feed/views/components/article/article.dart +++ b/lib/app/features/feed/views/components/article/article.dart @@ -76,7 +76,6 @@ class Article extends ConsumerWidget { if (isOwnedByCurrentUser) DeleteFeedItemMenu( entity: articleEntity, - kind: ArticleEntity.kind, ) else UserInfoMenu(pubkey: eventReference.pubkey), diff --git a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart index 2fc08e59e..ccd7f9792 100644 --- a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart +++ b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart @@ -16,7 +16,6 @@ import 'package:ion/generated/assets.gen.dart'; class DeleteFeedItemMenu extends ConsumerWidget { const DeleteFeedItemMenu({ required this.entity, - required this.kind, this.iconColor, this.onDelete, super.key, @@ -25,7 +24,6 @@ class DeleteFeedItemMenu extends ConsumerWidget { static double get iconSize => 20.0.s; final CacheableEntity entity; - final int kind; final Color? iconColor; final VoidCallback? onDelete; diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 0c3824526..96af01198 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -61,7 +61,6 @@ class Post extends ConsumerWidget { ? DeleteFeedItemMenu( entity: postEntity, onDelete: onDelete, - kind: PostEntity.kind, ) : UserInfoMenu(pubkey: eventReference.pubkey), ), diff --git a/lib/app/features/feed/views/pages/article_details_page/article_details_page.dart b/lib/app/features/feed/views/pages/article_details_page/article_details_page.dart index fbca65040..ce47109ac 100644 --- a/lib/app/features/feed/views/pages/article_details_page/article_details_page.dart +++ b/lib/app/features/feed/views/pages/article_details_page/article_details_page.dart @@ -96,7 +96,7 @@ class ArticleDetailsPage extends HookConsumerWidget { Container(color: context.theme.appColors.primaryBackground, height: 8.0.s), SizedBox(height: 20.0.s), ScreenSideOffset.small( - child: UserBiography(pubkey: articleEntity.masterPubkey), + child: UserBiography(entity: articleEntity), ), SizedBox(height: 4.0.s), const ArticleDetailsTopics(), diff --git a/lib/app/features/feed/views/pages/article_details_page/components/user_biography.dart b/lib/app/features/feed/views/pages/article_details_page/components/user_biography.dart index cc0fd7a7f..0161e34bd 100644 --- a/lib/app/features/feed/views/pages/article_details_page/components/user_biography.dart +++ b/lib/app/features/feed/views/pages/article_details_page/components/user_biography.dart @@ -1,19 +1,26 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/auth/providers/auth_provider.c.dart'; import 'package:ion/app/features/components/user/user_about/user_about.dart'; import 'package:ion/app/features/components/user/user_info_summary/user_info_summary.dart'; +import 'package:ion/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart'; import 'package:ion/app/features/feed/views/components/user_info/user_info.dart'; import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; -class UserBiography extends StatelessWidget { - const UserBiography({required this.pubkey, super.key}); +class UserBiography extends ConsumerWidget { + const UserBiography({required this.entity, super.key}); - final String pubkey; + final CacheableEntity entity; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final isOwnedByCurrentUser = ref.watch(isCurrentUserSelectorProvider(entity.masterPubkey)); + return Container( alignment: Alignment.topLeft, decoration: BoxDecoration( @@ -30,13 +37,20 @@ class UserBiography extends StatelessWidget { child: Column( children: [ UserInfo( - pubkey: pubkey, - trailing: UserInfoMenu(pubkey: pubkey), + pubkey: entity.masterPubkey, + trailing: isOwnedByCurrentUser + ? DeleteFeedItemMenu( + entity: entity, + onDelete: () { + context.pop(); + }, + ) + : UserInfoMenu(pubkey: entity.masterPubkey), ), SizedBox(height: 12.0.s), - UserAbout(pubkey: pubkey), + UserAbout(pubkey: entity.masterPubkey), SizedBox(height: 12.0.s), - UserInfoSummary(pubkey: pubkey), + UserInfoSummary(pubkey: entity.masterPubkey), ], ), ); From eb5d43ef4842034bd8a74edd220c6d093ab03bf7 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:45:25 +0200 Subject: [PATCH 09/27] feat: handle undo repost --- .../repost_option_action.dart | 4 ++ .../repost_options_modal.dart | 40 ++++++++++++++----- lib/l10n/app_en.arb | 1 + 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/lib/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart b/lib/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart index 170ffb4e6..aea0fcfda 100644 --- a/lib/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart +++ b/lib/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart @@ -6,21 +6,25 @@ import 'package:ion/generated/assets.gen.dart'; enum RepostOptionAction { repost, + undoRepost, quotePost; String getLabel(BuildContext context) => switch (this) { RepostOptionAction.repost => context.i18n.feed_repost, + RepostOptionAction.undoRepost => context.i18n.feed_undo_repost, RepostOptionAction.quotePost => context.i18n.feed_quote, }; Color getIconColor(BuildContext context) => switch (this) { RepostOptionAction.repost => context.theme.appColors.primaryAccent, + RepostOptionAction.undoRepost => context.theme.appColors.primaryAccent, RepostOptionAction.quotePost => context.theme.appColors.primaryAccent, }; Widget getIcon(BuildContext context) { final icon = switch (this) { RepostOptionAction.repost => Assets.svg.iconFeedRepost, + RepostOptionAction.undoRepost => Assets.svg.iconFeedRepost, RepostOptionAction.quotePost => Assets.svg.iconFeedQuote, }; diff --git a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart index 7272d1650..594ba269e 100644 --- a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart +++ b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart @@ -10,9 +10,13 @@ import 'package:ion/app/components/screen_offset/screen_bottom_offset.dart'; import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; import 'package:ion/app/components/separated/separated_column.dart'; import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/feed/providers/counters/reposted_events_provider.c.dart'; +import 'package:ion/app/features/feed/providers/delete_entity_provider.c.dart'; import 'package:ion/app/features/feed/providers/repost_notifier.c.dart'; import 'package:ion/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_entity_provider.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; @@ -31,9 +35,14 @@ class RepostOptionsModal extends HookConsumerWidget { ref.displayErrors(repostNotifierProvider); final selectedAction = useState(null); - + final isReposted = ref.watch(isRepostedProvider(eventReference)); final repostLoading = ref.watch(repostNotifierProvider).isLoading; + final actions = [ + if (isReposted) RepostOptionAction.undoRepost else RepostOptionAction.repost, + RepostOptionAction.quotePost, + ]; + return SheetContent( body: SingleChildScrollView( child: Column( @@ -50,7 +59,7 @@ class RepostOptionsModal extends HookConsumerWidget { separator: SizedBox(height: 9.0.s), mainAxisSize: MainAxisSize.min, children: [ - for (final option in RepostOptionAction.values) + for (final option in actions) ModalActionButton( icon: (repostLoading && selectedAction.value == option) ? const IONLoadingIndicator(type: IndicatorType.dark) @@ -58,17 +67,26 @@ class RepostOptionsModal extends HookConsumerWidget { label: option.getLabel(context), onTap: () async { selectedAction.value = option; - if (option == RepostOptionAction.repost) { - await ref - .read(repostNotifierProvider.notifier) - .repost(eventReference: eventReference); - if (!ref.read(repostNotifierProvider).hasError) { - if (context.mounted) { + switch (option) { + case RepostOptionAction.repost: + await ref + .read(repostNotifierProvider.notifier) + .repost(eventReference: eventReference); + if (!ref.read(repostNotifierProvider).hasError && context.mounted) { context.pop(); } - } - } else if (option == RepostOptionAction.quotePost) { - CreatePostRoute(quotedEvent: eventReference.toString()).go(context); + + case RepostOptionAction.quotePost: + CreatePostRoute(quotedEvent: eventReference.toString()).go(context); + case RepostOptionAction.undoRepost: + final entity = ref + .read(ionConnectEntityProvider(eventReference: eventReference)) + .valueOrNull; + if (entity case final CacheableEntity cacheableEntity) { + await ref + .read(deleteEntityProvider(cacheableEntity).notifier) + .delete(); + } } selectedAction.value = null; }, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 44f4731d2..211201e89 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -406,6 +406,7 @@ "feed_modal_article": "Article", "feed_repost_type": "Type", "feed_repost": "Repost", + "feed_undo_repost": "Undo repost", "feed_someone_reposted": "{someone} reposted", "feed_quote": "Quote", "feed_write_comment": "Write comment", From b339f2ad0f31de8d92d57fe5135a3a67925ce8fd Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:31:42 +0200 Subject: [PATCH 10/27] feat: adjust undo repost design --- .../modal_action_button/modal_action_button.dart | 7 ++++++- .../pages/repost_options_modal/repost_option_action.dart | 8 +++++++- .../pages/repost_options_modal/repost_options_modal.dart | 3 +++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/app/components/modal_action_button/modal_action_button.dart b/lib/app/components/modal_action_button/modal_action_button.dart index 976cf87bc..57e4379db 100644 --- a/lib/app/components/modal_action_button/modal_action_button.dart +++ b/lib/app/components/modal_action_button/modal_action_button.dart @@ -15,6 +15,7 @@ class ModalActionButton extends StatelessWidget { required this.label, required this.onTap, this.trailing, + this.labelStyle, super.key, }); @@ -22,6 +23,7 @@ class ModalActionButton extends StatelessWidget { final String label; final Widget? trailing; final VoidCallback onTap; + final TextStyle? labelStyle; @override Widget build(BuildContext context) { @@ -40,7 +42,10 @@ class ModalActionButton extends StatelessWidget { title: Row( children: [ Expanded( - child: Text(label, style: context.theme.appTextThemes.body), + child: Text( + label, + style: labelStyle ?? context.theme.appTextThemes.body, + ), ), if (trailing != null) ...[ SizedBox(width: 16.0.s), diff --git a/lib/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart b/lib/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart index aea0fcfda..b936e70b7 100644 --- a/lib/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart +++ b/lib/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart @@ -15,9 +15,15 @@ enum RepostOptionAction { RepostOptionAction.quotePost => context.i18n.feed_quote, }; + Color getLabelColor(BuildContext context) => switch (this) { + RepostOptionAction.repost => context.theme.appColors.primaryAccent, + RepostOptionAction.undoRepost => context.theme.appColors.attentionRed, + RepostOptionAction.quotePost => context.theme.appColors.primaryAccent, + }; + Color getIconColor(BuildContext context) => switch (this) { RepostOptionAction.repost => context.theme.appColors.primaryAccent, - RepostOptionAction.undoRepost => context.theme.appColors.primaryAccent, + RepostOptionAction.undoRepost => context.theme.appColors.attentionRed, RepostOptionAction.quotePost => context.theme.appColors.primaryAccent, }; diff --git a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart index 594ba269e..c7d7e476b 100644 --- a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart +++ b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart @@ -65,6 +65,9 @@ class RepostOptionsModal extends HookConsumerWidget { ? const IONLoadingIndicator(type: IndicatorType.dark) : option.getIcon(context), label: option.getLabel(context), + labelStyle: context.theme.appTextThemes.body.copyWith( + color: option.getLabelColor(context), + ), onTap: () async { selectedAction.value = option; switch (option) { From 15d5b4cd10db20ad827283183be29531491ad098 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Thu, 16 Jan 2025 23:42:13 +0200 Subject: [PATCH 11/27] feat: add ability to delete own post from feed --- .../feed/views/components/post/post.dart | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 96af01198..0949cc6c4 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -17,8 +17,11 @@ import 'package:ion/app/features/feed/views/components/quoted_entity_frame/quote import 'package:ion/app/features/feed/views/components/user_info/user_info.dart'; import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_entity_provider.c.dart'; +import 'package:ion/app/features/user/model/user_metadata.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; +import 'package:ion/app/services/logger/logger.dart'; class Post extends ConsumerWidget { const Post({ @@ -42,6 +45,14 @@ class Post extends ConsumerWidget { .watch(ionConnectEntityProvider(eventReference: eventReference)) .valueOrNull as PostEntity?; + final userMetadata = ref.watch( + ionConnectCacheProvider.select( + cacheSelector( + UserMetadataEntity.cacheKeyBuilder(pubkey: eventReference.pubkey), + ), + ), + ); + if (postEntity == null) { return const Skeleton(child: PostSkeleton()); } @@ -50,6 +61,20 @@ class Post extends ConsumerWidget { final framedEvent = _getFramedEventReference(postEntity); + if (postEntity.data.content.toString().contains('Test post to delete')) { + Logger.log('POST WHERE I AM AUTHOR'); + Logger.log('postEntity: $postEntity'); + Logger.log('eventReference pubkey: ${eventReference.pubkey}'); + Logger.log('userMetadata: $userMetadata'); + Logger.log('isOwnedByCurrentUser: $isOwnedByCurrentUser'); + } else { + Logger.log('POST WHERE I AM NOT AUTHOR'); + Logger.log('postEntity: $postEntity'); + Logger.log('eventReference pubkey: ${eventReference.pubkey}'); + Logger.log('userMetadata: $userMetadata'); + Logger.log('isOwnedByCurrentUser Some other post: $isOwnedByCurrentUser'); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 7aec7b04b723d55f696fd31bc681a787619553b2 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:37:40 +0200 Subject: [PATCH 12/27] feat: remove reposts --- .../counter_items_footer.dart | 3 +++ .../reposts_counter_button.dart | 7 ++++- .../components/repost_list_item.dart | 8 +++--- .../providers/delete_entity_provider.c.dart | 6 ++++- .../feed/views/components/post/post.dart | 8 +++++- .../repost_options_modal.dart | 27 +++++++++++++------ lib/app/router/feed_routes.dart | 5 ++++ 7 files changed, 50 insertions(+), 14 deletions(-) diff --git a/lib/app/components/counter_items_footer/counter_items_footer.dart b/lib/app/components/counter_items_footer/counter_items_footer.dart index c7f643b85..c30ec61a9 100644 --- a/lib/app/components/counter_items_footer/counter_items_footer.dart +++ b/lib/app/components/counter_items_footer/counter_items_footer.dart @@ -13,6 +13,7 @@ import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; class CounterItemsFooter extends HookConsumerWidget { CounterItemsFooter({ required this.eventReference, + this.repostEventReference, double? bottomPadding, double? topPadding, this.color, @@ -21,6 +22,7 @@ class CounterItemsFooter extends HookConsumerWidget { topPadding = topPadding ?? 10.0.s; final EventReference eventReference; + final EventReference? repostEventReference; final double bottomPadding; final double topPadding; final Color? color; @@ -41,6 +43,7 @@ class CounterItemsFooter extends HookConsumerWidget { Flexible( child: RepostsCounterButton( eventReference: eventReference, + repostEventReference: repostEventReference, color: color, ), ), diff --git a/lib/app/components/counter_items_footer/reposts_counter_button.dart b/lib/app/components/counter_items_footer/reposts_counter_button.dart index 41d2c290e..38670b2b9 100644 --- a/lib/app/components/counter_items_footer/reposts_counter_button.dart +++ b/lib/app/components/counter_items_footer/reposts_counter_button.dart @@ -15,11 +15,13 @@ import 'package:ion/generated/assets.gen.dart'; class RepostsCounterButton extends ConsumerWidget { const RepostsCounterButton({ required this.eventReference, + this.repostEventReference, this.color, super.key, }); final EventReference eventReference; + final EventReference? repostEventReference; final Color? color; @override @@ -30,7 +32,10 @@ class RepostsCounterButton extends ConsumerWidget { return GestureDetector( onTap: () { HapticFeedback.lightImpact(); - RepostOptionsModalRoute(eventReference: eventReference.toString()).push(context); + RepostOptionsModalRoute( + eventReference: eventReference.toString(), + repostEventReference: repostEventReference?.toString(), + ).push(context); }, child: TextActionButton( icon: Assets.svg.iconBlockRepost.icon( diff --git a/lib/app/features/components/entities_list/components/repost_list_item.dart b/lib/app/features/components/entities_list/components/repost_list_item.dart index a96f4a635..8d1de7279 100644 --- a/lib/app/features/components/entities_list/components/repost_list_item.dart +++ b/lib/app/features/components/entities_list/components/repost_list_item.dart @@ -1,6 +1,7 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; import 'package:ion/app/features/components/entities_list/components/repost_author_header.dart'; import 'package:ion/app/features/feed/data/models/entities/repost_data.c.dart'; @@ -8,14 +9,15 @@ import 'package:ion/app/features/feed/views/components/post/post.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; -class RepostListItem extends StatelessWidget { +class RepostListItem extends ConsumerWidget { const RepostListItem({required this.repost, super.key}); final RepostEntity repost; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final eventReference = EventReference(eventId: repost.data.eventId, pubkey: repost.data.pubkey); + final repostEventReference = EventReference(eventId: repost.id, pubkey: repost.pubkey); return GestureDetector( onTap: () => PostDetailsRoute(eventReference: eventReference.toString()).push(context), @@ -24,7 +26,7 @@ class RepostListItem extends StatelessWidget { child: Column( children: [ RepostAuthorHeader(pubkey: repost.masterPubkey), - Post(eventReference: eventReference), + Post(eventReference: eventReference, repostEventReference: repostEventReference), ], ), ), diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart index 7c80d185b..eb16b813b 100644 --- a/lib/app/features/feed/providers/delete_entity_provider.c.dart +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -3,6 +3,8 @@ import 'package:ion/app/exceptions/exceptions.dart'; import 'package:ion/app/features/feed/data/models/entities/article_data.c.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; +import 'package:ion/app/features/feed/data/models/entities/repost_data.c.dart'; +import 'package:ion/app/features/feed/data/models/generic_repost.c.dart'; import 'package:ion/app/features/feed/providers/feed_posts_data_source_provider.c.dart'; import 'package:ion/app/features/feed/providers/replies_provider.c.dart'; import 'package:ion/app/features/ion_connect/model/deletion_request.c.dart'; @@ -25,6 +27,8 @@ class DeleteEntity extends _$DeleteEntity { final entityKind = switch (entity) { PostEntity() => PostEntity.kind, ArticleEntity() => ArticleEntity.kind, + RepostEntity() => RepostEntity.kind, + GenericRepostEntity() => GenericRepostEntity.kind, _ => throw DeleteEntityUnsupportedTypeException(), }; @@ -51,7 +55,7 @@ class DeleteEntity extends _$DeleteEntity { ) .deleteReply(entity: entity); } else { - // Post or Article + // Post or Article or Repost or GenericRepost final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; if (feedDataSources.isNotEmpty) { await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 0949cc6c4..2bb1370cf 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -26,6 +26,7 @@ import 'package:ion/app/services/logger/logger.dart'; class Post extends ConsumerWidget { const Post({ required this.eventReference, + this.repostEventReference, this.header, this.footer, this.showParent = false, @@ -34,6 +35,7 @@ class Post extends ConsumerWidget { }); final EventReference eventReference; + final EventReference? repostEventReference; final bool showParent; final Widget? header; final Widget? footer; @@ -92,7 +94,11 @@ class Post extends ConsumerWidget { SizedBox(height: 10.0.s), PostBody(postEntity: postEntity), if (framedEvent != null) _FramedEvent(eventReference: framedEvent), - footer ?? CounterItemsFooter(eventReference: eventReference), + footer ?? + CounterItemsFooter( + eventReference: eventReference, + repostEventReference: repostEventReference, + ), ], ); } diff --git a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart index c7d7e476b..e11f7986e 100644 --- a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart +++ b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart @@ -25,10 +25,12 @@ import 'package:ion/app/router/components/sheet_content/sheet_content.dart'; class RepostOptionsModal extends HookConsumerWidget { const RepostOptionsModal({ required this.eventReference, + this.repostEventReference, super.key, }); final EventReference eventReference; + final EventReference? repostEventReference; @override Widget build(BuildContext context, WidgetRef ref) { @@ -82,16 +84,25 @@ class RepostOptionsModal extends HookConsumerWidget { case RepostOptionAction.quotePost: CreatePostRoute(quotedEvent: eventReference.toString()).go(context); case RepostOptionAction.undoRepost: - final entity = ref - .read(ionConnectEntityProvider(eventReference: eventReference)) - .valueOrNull; - if (entity case final CacheableEntity cacheableEntity) { - await ref - .read(deleteEntityProvider(cacheableEntity).notifier) - .delete(); + selectedAction.value = option; + + if (repostEventReference != null) { + final entity = await ref.read( + ionConnectEntityProvider(eventReference: repostEventReference!) + .future, + ); + + if (entity case final CacheableEntity cacheableEntity) { + await ref + .read(deleteEntityProvider(cacheableEntity).notifier) + .delete(); + if (context.mounted) { + context.pop(); + } + } } + selectedAction.value = null; } - selectedAction.value = null; }, ), ], diff --git a/lib/app/router/feed_routes.dart b/lib/app/router/feed_routes.dart index 389bb9e9c..6dbaac26f 100644 --- a/lib/app/router/feed_routes.dart +++ b/lib/app/router/feed_routes.dart @@ -69,14 +69,19 @@ class NotificationsHistoryRoute extends BaseRouteData { class RepostOptionsModalRoute extends BaseRouteData { RepostOptionsModalRoute({ required this.eventReference, + this.repostEventReference, }) : super( child: RepostOptionsModal( eventReference: EventReference.fromString(eventReference), + repostEventReference: repostEventReference != null + ? EventReference.fromString(repostEventReference) + : null, ), type: IceRouteType.bottomSheet, ); final String eventReference; + final String? repostEventReference; } class SharePostModalRoute extends BaseRouteData { From 52f20fcaf1e9262e385dd6544db6e457ea1b3949 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Fri, 17 Jan 2025 18:21:12 +0200 Subject: [PATCH 13/27] fix: reply remove from profile --- .../features/feed/providers/delete_entity_provider.c.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart index eb16b813b..238fd6ef7 100644 --- a/lib/app/features/feed/providers/delete_entity_provider.c.dart +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -12,6 +12,7 @@ import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; import 'package:ion/app/features/ion_connect/providers/entities_paged_data_provider.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_notifier.c.dart'; +import 'package:ion/app/features/user/providers/user_replies_data_source_provider.c.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'delete_entity_provider.c.g.dart'; @@ -54,8 +55,11 @@ class DeleteEntity extends _$DeleteEntity { ).notifier, ) .deleteReply(entity: entity); + + final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; + await ref.read(entitiesPagedDataProvider(dataSource).notifier).deleteEntity(entity); } else { - // Post or Article or Repost or GenericRepost + // Post / Article / Repost final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; if (feedDataSources.isNotEmpty) { await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); From 7ec899b01977023ba3a62f72a4d5cb3e25b6c496 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Fri, 17 Jan 2025 18:43:00 +0200 Subject: [PATCH 14/27] feat: handle article deletion from profile --- .../feed/providers/delete_entity_provider.c.dart | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart index 238fd6ef7..10c5a0d77 100644 --- a/lib/app/features/feed/providers/delete_entity_provider.c.dart +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -12,6 +12,7 @@ import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; import 'package:ion/app/features/ion_connect/providers/entities_paged_data_provider.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_notifier.c.dart'; +import 'package:ion/app/features/user/providers/user_articles_data_source_provider.c.dart'; import 'package:ion/app/features/user/providers/user_replies_data_source_provider.c.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -59,9 +60,17 @@ class DeleteEntity extends _$DeleteEntity { final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; await ref.read(entitiesPagedDataProvider(dataSource).notifier).deleteEntity(entity); } else { - // Post / Article / Repost - final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; - if (feedDataSources.isNotEmpty) { + if (entity case final ArticleEntity _) { + final userArticlesDataSource = + ref.watch(userArticlesDataSourceProvider(entity.masterPubkey)); + final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; + await ref + .read(entitiesPagedDataProvider(userArticlesDataSource).notifier) + .deleteEntity(entity); + await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); + } else { + // Also delete from feed if present + final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); } } From 7c5a4cc6198ca5c17e7f25e72a4b58270007126b Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:22:52 +0200 Subject: [PATCH 15/27] feat: handle videos and post entities deletion from profile --- .../providers/delete_entity_provider.c.dart | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart index 10c5a0d77..ef587cf35 100644 --- a/lib/app/features/feed/providers/delete_entity_provider.c.dart +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -13,7 +13,9 @@ import 'package:ion/app/features/ion_connect/providers/entities_paged_data_provi import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_notifier.c.dart'; import 'package:ion/app/features/user/providers/user_articles_data_source_provider.c.dart'; +import 'package:ion/app/features/user/providers/user_posts_data_source_provider.c.dart'; import 'package:ion/app/features/user/providers/user_replies_data_source_provider.c.dart'; +import 'package:ion/app/features/user/providers/user_videos_data_source_provider.c.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'delete_entity_provider.c.g.dart'; @@ -45,7 +47,7 @@ class DeleteEntity extends _$DeleteEntity { ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); if (entity case final PostEntity post when post.data.parentEvent != null) { - // Post reply + // Reply await ref .read( repliesProvider( @@ -59,20 +61,23 @@ class DeleteEntity extends _$DeleteEntity { final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; await ref.read(entitiesPagedDataProvider(dataSource).notifier).deleteEntity(entity); + } else if (entity case final ArticleEntity _) { + // Article + final userArticlesDataSource = ref.watch(userArticlesDataSourceProvider(entity.masterPubkey)); + final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; + await ref + .read(entitiesPagedDataProvider(userArticlesDataSource).notifier) + .deleteEntity(entity); + await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); } else { - if (entity case final ArticleEntity _) { - final userArticlesDataSource = - ref.watch(userArticlesDataSourceProvider(entity.masterPubkey)); - final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; - await ref - .read(entitiesPagedDataProvider(userArticlesDataSource).notifier) - .deleteEntity(entity); - await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); - } else { - // Also delete from feed if present - final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; - await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); - } + final userVideosDataSource = ref.watch(userVideosDataSourceProvider(entity.masterPubkey)); + await ref.read(entitiesPagedDataProvider(userVideosDataSource).notifier).deleteEntity(entity); + + final userPostsDataSource = ref.watch(userPostsDataSourceProvider(entity.masterPubkey)); + await ref.read(entitiesPagedDataProvider(userPostsDataSource).notifier).deleteEntity(entity); + + final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; + await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); } } } From 5d49289281d0a5a36851a7aed0db6570e65cab39 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Fri, 17 Jan 2025 21:30:14 +0200 Subject: [PATCH 16/27] refactor: delete provider --- .../providers/delete_entity_provider.c.dart | 77 +++++++++++-------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart index ef587cf35..5c3608e60 100644 --- a/lib/app/features/feed/providers/delete_entity_provider.c.dart +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -28,6 +28,11 @@ class DeleteEntity extends _$DeleteEntity { } Future delete() async { + await _deleteFromServer(); + await _deleteFromDataSources(); + } + + Future _deleteFromServer() async { final entityKind = switch (entity) { PostEntity() => PostEntity.kind, ArticleEntity() => ArticleEntity.kind, @@ -39,45 +44,57 @@ class DeleteEntity extends _$DeleteEntity { final deletionRequest = DeletionRequest( events: [EventToDelete(eventId: entity.id, kind: entityKind)], ); - await ref .read(ionConnectNotifierProvider.notifier) .sendEntityData(deletionRequest, cache: false); - ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); + } + Future _deleteFromDataSources() async { if (entity case final PostEntity post when post.data.parentEvent != null) { - // Reply - await ref - .read( - repliesProvider( - EventReference( - eventId: post.data.parentEvent!.eventId, - pubkey: post.data.parentEvent!.pubkey, - ), - ).notifier, - ) - .deleteReply(entity: entity); - - final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; - await ref.read(entitiesPagedDataProvider(dataSource).notifier).deleteEntity(entity); + await _deleteReply(post); } else if (entity case final ArticleEntity _) { - // Article - final userArticlesDataSource = ref.watch(userArticlesDataSourceProvider(entity.masterPubkey)); - final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; - await ref - .read(entitiesPagedDataProvider(userArticlesDataSource).notifier) - .deleteEntity(entity); - await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); + await _deleteArticle(); } else { - final userVideosDataSource = ref.watch(userVideosDataSourceProvider(entity.masterPubkey)); - await ref.read(entitiesPagedDataProvider(userVideosDataSource).notifier).deleteEntity(entity); + await _deletePost(); + } + } + + Future _deleteReply(PostEntity post) async { + await ref + .read( + repliesProvider( + EventReference( + eventId: post.data.parentEvent!.eventId, + pubkey: post.data.parentEvent!.pubkey, + ), + ).notifier, + ) + .deleteReply(entity: entity); - final userPostsDataSource = ref.watch(userPostsDataSourceProvider(entity.masterPubkey)); - await ref.read(entitiesPagedDataProvider(userPostsDataSource).notifier).deleteEntity(entity); + final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; + await _deleteFromDataSource(dataSource); + } - final feedDataSources = ref.read(feedPostsDataSourceProvider) ?? []; - await ref.read(entitiesPagedDataProvider(feedDataSources).notifier).deleteEntity(entity); - } + Future _deleteArticle() async { + final userArticlesDataSource = ref.watch(userArticlesDataSourceProvider(entity.masterPubkey)); + final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; + + await _deleteFromDataSource(userArticlesDataSource ?? []); + await _deleteFromDataSource(feedDataSources); + } + + Future _deletePost() async { + final userVideosDataSource = ref.watch(userVideosDataSourceProvider(entity.masterPubkey)); + final userPostsDataSource = ref.watch(userPostsDataSourceProvider(entity.masterPubkey)); + final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; + + await _deleteFromDataSource(userVideosDataSource ?? []); + await _deleteFromDataSource(userPostsDataSource ?? []); + await _deleteFromDataSource(feedDataSources); + } + + Future _deleteFromDataSource(List dataSource) async { + await ref.read(entitiesPagedDataProvider(dataSource).notifier).deleteEntity(entity); } } From f879643ab8694bd2efcafe07e7a55a942534f9a1 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:22:51 +0200 Subject: [PATCH 17/27] fix: remove logs --- ios/Podfile.lock | 2 +- .../features/feed/views/components/post/post.dart | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5664631f1..b31bcca80 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -388,7 +388,7 @@ SPEC CHECKSUMS: device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - external_app_launcher: 3411245965270a74040a3de17e27bd02b8abb905 + external_app_launcher: ad55ac844aa21f2d2197d7cec58ff0d0dc40bbc0 ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b ffmpeg_kit_flutter_full_gpl: 8d15c14c0c3aba616fac04fe44b3d27d02e3c330 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 2bb1370cf..63c2e80d5 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -21,7 +21,6 @@ import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart' import 'package:ion/app/features/ion_connect/providers/ion_connect_entity_provider.c.dart'; import 'package:ion/app/features/user/model/user_metadata.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; -import 'package:ion/app/services/logger/logger.dart'; class Post extends ConsumerWidget { const Post({ @@ -63,20 +62,6 @@ class Post extends ConsumerWidget { final framedEvent = _getFramedEventReference(postEntity); - if (postEntity.data.content.toString().contains('Test post to delete')) { - Logger.log('POST WHERE I AM AUTHOR'); - Logger.log('postEntity: $postEntity'); - Logger.log('eventReference pubkey: ${eventReference.pubkey}'); - Logger.log('userMetadata: $userMetadata'); - Logger.log('isOwnedByCurrentUser: $isOwnedByCurrentUser'); - } else { - Logger.log('POST WHERE I AM NOT AUTHOR'); - Logger.log('postEntity: $postEntity'); - Logger.log('eventReference pubkey: ${eventReference.pubkey}'); - Logger.log('userMetadata: $userMetadata'); - Logger.log('isOwnedByCurrentUser Some other post: $isOwnedByCurrentUser'); - } - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 10f1b07224802ee1c4e80826c933d97f5a7cc6c8 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:37:13 +0200 Subject: [PATCH 18/27] feat: handle reply counter increase and decrease --- .../providers/create_post_notifier.c.dart | 4 - .../counters/replied_events_provider.c.dart | 105 +++++++++++++----- .../counters/replies_count_provider.c.dart | 39 ++++--- .../providers/delete_entity_provider.c.dart | 8 +- .../feed/providers/replies_provider.c.dart | 7 +- 5 files changed, 114 insertions(+), 49 deletions(-) diff --git a/lib/app/features/feed/create_post/providers/create_post_notifier.c.dart b/lib/app/features/feed/create_post/providers/create_post_notifier.c.dart index ab05e8e78..b2e5b2db2 100644 --- a/lib/app/features/feed/create_post/providers/create_post_notifier.c.dart +++ b/lib/app/features/feed/create_post/providers/create_post_notifier.c.dart @@ -8,7 +8,6 @@ import 'package:ion/app/features/feed/create_post/model/create_post_option.dart' import 'package:ion/app/features/feed/data/models/entities/article_data.c.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; import 'package:ion/app/features/feed/data/models/who_can_reply_settings_option.dart'; -import 'package:ion/app/features/feed/providers/counters/replies_count_provider.c.dart'; import 'package:ion/app/features/feed/providers/counters/reposts_count_provider.c.dart'; import 'package:ion/app/features/ion_connect/model/entity_expiration.c.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; @@ -123,9 +122,6 @@ class CreatePostNotifier extends _$CreatePostNotifier { if (quotedEvent != null) { ref.read(repostsCountProvider(quotedEvent).notifier).addOne(); } - if (parentEvent != null) { - ref.read(repliesCountProvider(parentEvent).notifier).addOne(); - } }); } diff --git a/lib/app/features/feed/providers/counters/replied_events_provider.c.dart b/lib/app/features/feed/providers/counters/replied_events_provider.c.dart index b30ccce99..8ce124cb0 100644 --- a/lib/app/features/feed/providers/counters/replied_events_provider.c.dart +++ b/lib/app/features/feed/providers/counters/replied_events_provider.c.dart @@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/features/auth/providers/auth_provider.c.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; +import 'package:ion/app/features/feed/providers/replies_provider.c.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; import 'package:ion/app/features/ion_connect/model/ion_connect_entity.dart'; import 'package:ion/app/features/ion_connect/model/related_event.c.dart'; @@ -12,51 +13,103 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'replied_events_provider.c.g.dart'; @Riverpod(keepAlive: true) -Stream?> repliedEvents(Ref ref) async* { +Stream>?> repliedEvents(Ref ref) async* { final currentPubkey = await ref.watch(currentPubkeySelectorProvider.future); if (currentPubkey == null) { yield {}; } else { - final cache = ref.read(ionConnectCacheProvider); - var repliedIds = cache.values.fold>({}, (result, entry) { - final currentUserRepliedIds = _getCurrentUserRepliedIds(entry, currentPubkey: currentPubkey); - if (currentUserRepliedIds != null) { - result.addAll(currentUserRepliedIds); - } - return result; - }); + var repliedMap = >{}; - yield repliedIds; + final cache = ref.read(ionConnectCacheProvider); + repliedMap = _buildInitialMap(cache, currentPubkey); + yield repliedMap; await for (final entity in ref.watch(ionConnectCacheStreamProvider)) { - final currentUserRepliedIds = _getCurrentUserRepliedIds(entity, currentPubkey: currentPubkey); - if (currentUserRepliedIds != null) { - yield repliedIds = {...repliedIds, ...currentUserRepliedIds}; + if (entity case final PostEntity post) { + final parentId = post.data.parentEvent?.eventId; + if (parentId == null) { + continue; + } + + final currentCache = ref.read(ionConnectCacheProvider); + final isInCache = currentCache.containsKey(post.id); + + if (!isInCache) { + if (repliedMap.containsKey(post.id)) { + repliedMap.remove(post.id); + } + + if (repliedMap.containsKey(parentId)) { + repliedMap[parentId] = repliedMap[parentId]!.where((id) => id != post.id).toList(); + if (repliedMap[parentId]!.isEmpty) { + repliedMap.remove(parentId); + } + } + + yield repliedMap; + } else { + final currentUserRepliedIds = + _getCurrentUserRepliedIds(post, currentPubkey: currentPubkey); + + if (currentUserRepliedIds != null) { + repliedMap[parentId] = currentUserRepliedIds; + + yield repliedMap; + } + } } } } } +Map> _buildInitialMap( + Map cache, + String currentPubkey, +) { + return cache.values.fold>>({}, (result, entry) { + if (entry case final PostEntity post) { + final currentUserRepliedIds = _getCurrentUserRepliedIds(post, currentPubkey: currentPubkey); + final parentId = post.data.parentEvent?.eventId; + if (currentUserRepliedIds != null && parentId != null) { + if (!result.containsKey(parentId)) { + result[parentId] = []; + } + final newIds = currentUserRepliedIds.where((id) => !result[parentId]!.contains(id)); + result[parentId] = [...result[parentId]!, ...newIds]; + } + } + return result; + }); +} + List? _getCurrentUserRepliedIds(IonConnectEntity entity, {required String currentPubkey}) { - if (entity.masterPubkey != currentPubkey || entity is! PostEntity) { - return null; - } + if (entity case final PostEntity post when post.masterPubkey == currentPubkey) { + final relatedEvents = post.data.relatedEvents; - final relatedEvents = entity.data.relatedEvents; + if (relatedEvents == null) { + return null; + } - if (relatedEvents == null) { - return null; + final replyIds = [ + for (final event in relatedEvents) + if (event.marker == RelatedEventMarker.reply || event.marker == RelatedEventMarker.root) + event.eventId, + ]; + return replyIds; } - - return [ - for (final event in relatedEvents) - if (event.marker == RelatedEventMarker.reply || event.marker == RelatedEventMarker.root) - event.eventId, - ]; + return null; } @riverpod bool isReplied(Ref ref, EventReference eventReference) { - return ref.watch(repliedEventsProvider).valueOrNull?.contains(eventReference.eventId) ?? false; + final repliedMap = ref.watch(repliedEventsProvider).valueOrNull; + final hasReply = repliedMap?[eventReference.eventId]?.isNotEmpty ?? false; + + final replies = ref.watch(repliesProvider(eventReference)); + final hasActiveReplies = replies?.data.items?.isNotEmpty ?? false; + + final isActive = hasReply && hasActiveReplies; + + return isActive; } diff --git a/lib/app/features/feed/providers/counters/replies_count_provider.c.dart b/lib/app/features/feed/providers/counters/replies_count_provider.c.dart index d7675a88f..dc152ab3e 100644 --- a/lib/app/features/feed/providers/counters/replies_count_provider.c.dart +++ b/lib/app/features/feed/providers/counters/replies_count_provider.c.dart @@ -1,6 +1,7 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:ion/app/features/feed/data/models/entities/event_count_result_data.c.dart'; +import 'package:ion/app/features/feed/providers/replies_provider.c.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -11,29 +12,35 @@ part 'replies_count_provider.c.g.dart'; class RepliesCount extends _$RepliesCount { @override int build(EventReference eventReference) { - final entity = ref.watch( - ionConnectCacheProvider.select( - cacheSelector( - EventCountResultEntity.cacheKeyBuilder( - key: eventReference.eventId, - type: EventCountResultType.replies, - ), - ), - ), + final realTimeCount = ref.watch( + repliesProvider(eventReference).select((value) { + final count = value?.data.items?.length ?? 0; + return count; + }), ); - if (entity == null) { - return 0; + if (realTimeCount > 0) { + return realTimeCount; } - return entity.data.content as int; + final cacheCount = ref + .watch( + ionConnectCacheProvider.select( + cacheSelector( + EventCountResultEntity.cacheKeyBuilder( + key: eventReference.eventId, + type: EventCountResultType.replies, + ), + ), + ), + ) + ?.data + .content as int? ?? + 0; + return cacheCount; } void addOne() { state = state + 1; } - - void removeOne() { - state = state - 1; - } } diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart index 5c3608e60..699164a54 100644 --- a/lib/app/features/feed/providers/delete_entity_provider.c.dart +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -47,7 +47,6 @@ class DeleteEntity extends _$DeleteEntity { await ref .read(ionConnectNotifierProvider.notifier) .sendEntityData(deletionRequest, cache: false); - ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); } Future _deleteFromDataSources() async { @@ -61,6 +60,8 @@ class DeleteEntity extends _$DeleteEntity { } Future _deleteReply(PostEntity post) async { + final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; + await ref .read( repliesProvider( @@ -72,8 +73,9 @@ class DeleteEntity extends _$DeleteEntity { ) .deleteReply(entity: entity); - final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; await _deleteFromDataSource(dataSource); + + ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); } Future _deleteArticle() async { @@ -82,6 +84,7 @@ class DeleteEntity extends _$DeleteEntity { await _deleteFromDataSource(userArticlesDataSource ?? []); await _deleteFromDataSource(feedDataSources); + ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); } Future _deletePost() async { @@ -92,6 +95,7 @@ class DeleteEntity extends _$DeleteEntity { await _deleteFromDataSource(userVideosDataSource ?? []); await _deleteFromDataSource(userPostsDataSource ?? []); await _deleteFromDataSource(feedDataSources); + ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); } Future _deleteFromDataSource(List dataSource) async { diff --git a/lib/app/features/feed/providers/replies_provider.c.dart b/lib/app/features/feed/providers/replies_provider.c.dart index 5c1ebe85d..ca3e06ecb 100644 --- a/lib/app/features/feed/providers/replies_provider.c.dart +++ b/lib/app/features/feed/providers/replies_provider.c.dart @@ -22,6 +22,10 @@ class Replies extends _$Replies { .where((entity) => isReply(entity, eventReference)) .distinct() .listen((entity) { + if (state?.data.items?.any((e) => e.id == entity.id) ?? false) { + return; + } + state = state?.copyWith.data(items: {entity, ...state?.data.items ?? {}}); }); ref.onDispose(subscription.cancel); @@ -39,7 +43,8 @@ class Replies extends _$Replies { final currentState = state; if (currentState != null) { final updatedItems = - currentState.data.items?.where((entity) => entity.id != entity.id).toSet() ?? {}; + currentState.data.items?.where((item) => item.id != entity.id).toSet() ?? {}; + state = currentState.copyWith( data: currentState.data.copyWith( items: updatedItems, From 200e749e8b6f57caf32d33bba3c3c59bb9da6cb4 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:45:35 +0200 Subject: [PATCH 19/27] fix: warning --- lib/app/features/feed/views/components/post/post.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 63c2e80d5..4423e9506 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -17,9 +17,7 @@ import 'package:ion/app/features/feed/views/components/quoted_entity_frame/quote import 'package:ion/app/features/feed/views/components/user_info/user_info.dart'; import 'package:ion/app/features/feed/views/components/user_info_menu/user_info_menu.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; -import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_entity_provider.c.dart'; -import 'package:ion/app/features/user/model/user_metadata.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; class Post extends ConsumerWidget { @@ -46,14 +44,6 @@ class Post extends ConsumerWidget { .watch(ionConnectEntityProvider(eventReference: eventReference)) .valueOrNull as PostEntity?; - final userMetadata = ref.watch( - ionConnectCacheProvider.select( - cacheSelector( - UserMetadataEntity.cacheKeyBuilder(pubkey: eventReference.pubkey), - ), - ), - ); - if (postEntity == null) { return const Skeleton(child: PostSkeleton()); } From 46582c362a45282519c4672169af481858e2e365 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Tue, 21 Jan 2025 10:10:00 +0200 Subject: [PATCH 20/27] fix: comments --- lib/app/exceptions/exceptions.dart | 2 +- .../entities_list/components/repost_list_item.dart | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/app/exceptions/exceptions.dart b/lib/app/exceptions/exceptions.dart index 63ef1a5df..44e19f6e0 100644 --- a/lib/app/exceptions/exceptions.dart +++ b/lib/app/exceptions/exceptions.dart @@ -230,5 +230,5 @@ class ForceUpdateFetchConfigException extends IONException { class DeleteEntityUnsupportedTypeException extends IONException { DeleteEntityUnsupportedTypeException() - : super(10046, 'Failed to delete entity, unsupported type'); + : super(10048, 'Failed to delete entity, unsupported type'); } diff --git a/lib/app/features/components/entities_list/components/repost_list_item.dart b/lib/app/features/components/entities_list/components/repost_list_item.dart index 8d1de7279..4eb8303b4 100644 --- a/lib/app/features/components/entities_list/components/repost_list_item.dart +++ b/lib/app/features/components/entities_list/components/repost_list_item.dart @@ -1,7 +1,6 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:flutter/widgets.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; import 'package:ion/app/features/components/entities_list/components/repost_author_header.dart'; import 'package:ion/app/features/feed/data/models/entities/repost_data.c.dart'; @@ -9,13 +8,13 @@ import 'package:ion/app/features/feed/views/components/post/post.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; -class RepostListItem extends ConsumerWidget { +class RepostListItem extends StatelessWidget { const RepostListItem({required this.repost, super.key}); final RepostEntity repost; @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { final eventReference = EventReference(eventId: repost.data.eventId, pubkey: repost.data.pubkey); final repostEventReference = EventReference(eventId: repost.id, pubkey: repost.pubkey); From 4e6f258721c50bce0b8b2663fb5c31706abcab6e Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:15:51 +0200 Subject: [PATCH 21/27] fix: replies count increase and decrease --- .../providers/create_post_notifier.c.dart | 4 ++++ .../counters/replies_count_provider.c.dart | 17 +++++------------ .../providers/delete_entity_provider.c.dart | 12 ++++++++++++ .../feed/providers/replies_provider.c.dart | 4 ++-- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/app/features/feed/create_post/providers/create_post_notifier.c.dart b/lib/app/features/feed/create_post/providers/create_post_notifier.c.dart index b2e5b2db2..ab05e8e78 100644 --- a/lib/app/features/feed/create_post/providers/create_post_notifier.c.dart +++ b/lib/app/features/feed/create_post/providers/create_post_notifier.c.dart @@ -8,6 +8,7 @@ import 'package:ion/app/features/feed/create_post/model/create_post_option.dart' import 'package:ion/app/features/feed/data/models/entities/article_data.c.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; import 'package:ion/app/features/feed/data/models/who_can_reply_settings_option.dart'; +import 'package:ion/app/features/feed/providers/counters/replies_count_provider.c.dart'; import 'package:ion/app/features/feed/providers/counters/reposts_count_provider.c.dart'; import 'package:ion/app/features/ion_connect/model/entity_expiration.c.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; @@ -122,6 +123,9 @@ class CreatePostNotifier extends _$CreatePostNotifier { if (quotedEvent != null) { ref.read(repostsCountProvider(quotedEvent).notifier).addOne(); } + if (parentEvent != null) { + ref.read(repliesCountProvider(parentEvent).notifier).addOne(); + } }); } diff --git a/lib/app/features/feed/providers/counters/replies_count_provider.c.dart b/lib/app/features/feed/providers/counters/replies_count_provider.c.dart index dc152ab3e..45006bbd1 100644 --- a/lib/app/features/feed/providers/counters/replies_count_provider.c.dart +++ b/lib/app/features/feed/providers/counters/replies_count_provider.c.dart @@ -1,7 +1,6 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:ion/app/features/feed/data/models/entities/event_count_result_data.c.dart'; -import 'package:ion/app/features/feed/providers/replies_provider.c.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -12,17 +11,6 @@ part 'replies_count_provider.c.g.dart'; class RepliesCount extends _$RepliesCount { @override int build(EventReference eventReference) { - final realTimeCount = ref.watch( - repliesProvider(eventReference).select((value) { - final count = value?.data.items?.length ?? 0; - return count; - }), - ); - - if (realTimeCount > 0) { - return realTimeCount; - } - final cacheCount = ref .watch( ionConnectCacheProvider.select( @@ -37,10 +25,15 @@ class RepliesCount extends _$RepliesCount { ?.data .content as int? ?? 0; + return cacheCount; } void addOne() { state = state + 1; } + + void removeOne() { + state = state - 1; + } } diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart index 699164a54..498d4c75f 100644 --- a/lib/app/features/feed/providers/delete_entity_provider.c.dart +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -5,6 +5,7 @@ import 'package:ion/app/features/feed/data/models/entities/article_data.c.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; import 'package:ion/app/features/feed/data/models/entities/repost_data.c.dart'; import 'package:ion/app/features/feed/data/models/generic_repost.c.dart'; +import 'package:ion/app/features/feed/providers/counters/replies_count_provider.c.dart'; import 'package:ion/app/features/feed/providers/feed_posts_data_source_provider.c.dart'; import 'package:ion/app/features/feed/providers/replies_provider.c.dart'; import 'package:ion/app/features/ion_connect/model/deletion_request.c.dart'; @@ -62,6 +63,17 @@ class DeleteEntity extends _$DeleteEntity { Future _deleteReply(PostEntity post) async { final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; + ref + .read( + repliesCountProvider( + EventReference( + eventId: post.data.parentEvent!.eventId, + pubkey: post.data.parentEvent!.pubkey, + ), + ).notifier, + ) + .removeOne(); + await ref .read( repliesProvider( diff --git a/lib/app/features/feed/providers/replies_provider.c.dart b/lib/app/features/feed/providers/replies_provider.c.dart index ca3e06ecb..dfd76615e 100644 --- a/lib/app/features/feed/providers/replies_provider.c.dart +++ b/lib/app/features/feed/providers/replies_provider.c.dart @@ -19,7 +19,7 @@ class Replies extends _$Replies { final subscription = ref .watch(ionConnectCacheStreamProvider) - .where((entity) => isReply(entity, eventReference)) + .where((entity) => _isReply(entity, eventReference)) .distinct() .listen((entity) { if (state?.data.items?.any((e) => e.id == entity.id) ?? false) { @@ -33,7 +33,7 @@ class Replies extends _$Replies { return entitiesPagedData; } - bool isReply(IonConnectEntity entity, EventReference parentEventReference) { + bool _isReply(IonConnectEntity entity, EventReference parentEventReference) { return entity is PostEntity && entity.data.parentEvent?.eventId == parentEventReference.eventId; } From bf8825000db34268f77c3dc929670e28c12ee073 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:42:16 +0200 Subject: [PATCH 22/27] fix: pr comment --- .../counters/replied_events_provider.c.dart | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/lib/app/features/feed/providers/counters/replied_events_provider.c.dart b/lib/app/features/feed/providers/counters/replied_events_provider.c.dart index 8ce124cb0..30066ba61 100644 --- a/lib/app/features/feed/providers/counters/replied_events_provider.c.dart +++ b/lib/app/features/feed/providers/counters/replied_events_provider.c.dart @@ -32,31 +32,12 @@ Stream>?> repliedEvents(Ref ref) async* { continue; } - final currentCache = ref.read(ionConnectCacheProvider); - final isInCache = currentCache.containsKey(post.id); + final currentUserRepliedIds = _getCurrentUserRepliedIds(post, currentPubkey: currentPubkey); - if (!isInCache) { - if (repliedMap.containsKey(post.id)) { - repliedMap.remove(post.id); - } - - if (repliedMap.containsKey(parentId)) { - repliedMap[parentId] = repliedMap[parentId]!.where((id) => id != post.id).toList(); - if (repliedMap[parentId]!.isEmpty) { - repliedMap.remove(parentId); - } - } + if (currentUserRepliedIds != null) { + repliedMap[parentId] = currentUserRepliedIds; yield repliedMap; - } else { - final currentUserRepliedIds = - _getCurrentUserRepliedIds(post, currentPubkey: currentPubkey); - - if (currentUserRepliedIds != null) { - repliedMap[parentId] = currentUserRepliedIds; - - yield repliedMap; - } } } } From a35c872681eb22fc61058de5ea3ecd7967f96e3b Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:10:38 +0200 Subject: [PATCH 23/27] fix: isReplied --- .../counters/replied_events_provider.c.dart | 100 +++++++++++------- .../providers/delete_entity_provider.c.dart | 10 +- 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/lib/app/features/feed/providers/counters/replied_events_provider.c.dart b/lib/app/features/feed/providers/counters/replied_events_provider.c.dart index 30066ba61..42d2c3da9 100644 --- a/lib/app/features/feed/providers/counters/replied_events_provider.c.dart +++ b/lib/app/features/feed/providers/counters/replied_events_provider.c.dart @@ -3,45 +3,73 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/features/auth/providers/auth_provider.c.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; -import 'package:ion/app/features/feed/providers/replies_provider.c.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; import 'package:ion/app/features/ion_connect/model/ion_connect_entity.dart'; -import 'package:ion/app/features/ion_connect/model/related_event.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'replied_events_provider.c.g.dart'; @Riverpod(keepAlive: true) -Stream>?> repliedEvents(Ref ref) async* { - final currentPubkey = await ref.watch(currentPubkeySelectorProvider.future); - - if (currentPubkey == null) { - yield {}; - } else { - var repliedMap = >{}; - - final cache = ref.read(ionConnectCacheProvider); - repliedMap = _buildInitialMap(cache, currentPubkey); - yield repliedMap; - - await for (final entity in ref.watch(ionConnectCacheStreamProvider)) { - if (entity case final PostEntity post) { - final parentId = post.data.parentEvent?.eventId; - if (parentId == null) { - continue; - } +class RepliedEvents extends _$RepliedEvents { + final _deletedIds = {}; + + @override + Stream>?> build() async* { + final currentPubkey = await ref.watch(currentPubkeySelectorProvider.future); + + if (currentPubkey == null) { + yield {}; + } else { + var repliedMap = >{}; + + final cache = ref.read(ionConnectCacheProvider); + repliedMap = _buildInitialMap(cache, currentPubkey); + yield repliedMap; + + await for (final entity in ref.watch(ionConnectCacheStreamProvider)) { + if (entity case final PostEntity post) { + final parentId = post.data.parentEvent?.eventId; + if (parentId == null) continue; - final currentUserRepliedIds = _getCurrentUserRepliedIds(post, currentPubkey: currentPubkey); + final currentUserRepliedIds = + _getCurrentUserRepliedIds(post, currentPubkey: currentPubkey); - if (currentUserRepliedIds != null) { - repliedMap[parentId] = currentUserRepliedIds; + if (currentUserRepliedIds != null) { + final validIds = + currentUserRepliedIds.where((id) => !_deletedIds.contains(id)).toList(); - yield repliedMap; + final updatedMap = Map>.from(repliedMap); + + if (validIds.isEmpty) { + updatedMap.remove(parentId); + } else { + updatedMap[parentId] = validIds; + } + + repliedMap = updatedMap; + yield repliedMap; + } } } } } + + void removeReply(String parentId, String replyId) { + _deletedIds.add(replyId); + + final currentMap = state.valueOrNull; + if (currentMap != null && currentMap.containsKey(parentId)) { + final updatedReplies = Map>.from(currentMap); + updatedReplies[parentId] = updatedReplies[parentId]!.where((id) => id != replyId).toList(); + + if (updatedReplies[parentId]!.isEmpty) { + updatedReplies.remove(parentId); + } + + state = AsyncData(updatedReplies); + } + } } Map> _buildInitialMap( @@ -66,18 +94,9 @@ Map> _buildInitialMap( List? _getCurrentUserRepliedIds(IonConnectEntity entity, {required String currentPubkey}) { if (entity case final PostEntity post when post.masterPubkey == currentPubkey) { - final relatedEvents = post.data.relatedEvents; - - if (relatedEvents == null) { - return null; + if (post.data.parentEvent != null) { + return [post.id]; } - - final replyIds = [ - for (final event in relatedEvents) - if (event.marker == RelatedEventMarker.reply || event.marker == RelatedEventMarker.root) - event.eventId, - ]; - return replyIds; } return null; } @@ -85,12 +104,11 @@ List? _getCurrentUserRepliedIds(IonConnectEntity entity, {required Strin @riverpod bool isReplied(Ref ref, EventReference eventReference) { final repliedMap = ref.watch(repliedEventsProvider).valueOrNull; - final hasReply = repliedMap?[eventReference.eventId]?.isNotEmpty ?? false; - - final replies = ref.watch(repliesProvider(eventReference)); - final hasActiveReplies = replies?.data.items?.isNotEmpty ?? false; + final replyIds = repliedMap?[eventReference.eventId]; - final isActive = hasReply && hasActiveReplies; + final deletedIds = ref.read(repliedEventsProvider.notifier)._deletedIds; + final validReplyIds = replyIds?.where((id) => !deletedIds.contains(id)).toList(); + final hasReply = validReplyIds?.isNotEmpty ?? false; - return isActive; + return hasReply; } diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart index 498d4c75f..bc4dd72e3 100644 --- a/lib/app/features/feed/providers/delete_entity_provider.c.dart +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -5,6 +5,7 @@ import 'package:ion/app/features/feed/data/models/entities/article_data.c.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; import 'package:ion/app/features/feed/data/models/entities/repost_data.c.dart'; import 'package:ion/app/features/feed/data/models/generic_repost.c.dart'; +import 'package:ion/app/features/feed/providers/counters/replied_events_provider.c.dart'; import 'package:ion/app/features/feed/providers/counters/replies_count_provider.c.dart'; import 'package:ion/app/features/feed/providers/feed_posts_data_source_provider.c.dart'; import 'package:ion/app/features/feed/providers/replies_provider.c.dart'; @@ -63,11 +64,13 @@ class DeleteEntity extends _$DeleteEntity { Future _deleteReply(PostEntity post) async { final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; + final parentId = post.data.parentEvent!.eventId; + ref .read( repliesCountProvider( EventReference( - eventId: post.data.parentEvent!.eventId, + eventId: parentId, pubkey: post.data.parentEvent!.pubkey, ), ).notifier, @@ -78,15 +81,16 @@ class DeleteEntity extends _$DeleteEntity { .read( repliesProvider( EventReference( - eventId: post.data.parentEvent!.eventId, + eventId: parentId, pubkey: post.data.parentEvent!.pubkey, ), ).notifier, ) .deleteReply(entity: entity); - await _deleteFromDataSource(dataSource); + ref.read(repliedEventsProvider.notifier).removeReply(parentId, post.id); + await _deleteFromDataSource(dataSource); ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); } From 319f3ee3269fca2cfdcf6d95a26ab8436975a935 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:30:11 +0200 Subject: [PATCH 24/27] refactor: delete entity provider --- .../providers/delete_entity_provider.c.dart | 169 +++++++++--------- .../delete_feed_item_menu.dart | 2 +- .../repost_options_modal.dart | 4 +- 3 files changed, 86 insertions(+), 89 deletions(-) diff --git a/lib/app/features/feed/providers/delete_entity_provider.c.dart b/lib/app/features/feed/providers/delete_entity_provider.c.dart index bc4dd72e3..5864a7037 100644 --- a/lib/app/features/feed/providers/delete_entity_provider.c.dart +++ b/lib/app/features/feed/providers/delete_entity_provider.c.dart @@ -1,5 +1,6 @@ // SPDX-License-Identifier: ice License 1.0 +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/exceptions/exceptions.dart'; import 'package:ion/app/features/feed/data/models/entities/article_data.c.dart'; import 'package:ion/app/features/feed/data/models/entities/post_data.c.dart'; @@ -23,98 +24,96 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'delete_entity_provider.c.g.dart'; @riverpod -class DeleteEntity extends _$DeleteEntity { - @override - Future build(CacheableEntity entity) async { - return; - } - - Future delete() async { - await _deleteFromServer(); - await _deleteFromDataSources(); - } +Future deleteEntity( + Ref ref, + CacheableEntity entity, +) async { + await _deleteFromServer(ref, entity); + await _deleteFromDataSources(ref, entity); +} - Future _deleteFromServer() async { - final entityKind = switch (entity) { - PostEntity() => PostEntity.kind, - ArticleEntity() => ArticleEntity.kind, - RepostEntity() => RepostEntity.kind, - GenericRepostEntity() => GenericRepostEntity.kind, - _ => throw DeleteEntityUnsupportedTypeException(), - }; - - final deletionRequest = DeletionRequest( - events: [EventToDelete(eventId: entity.id, kind: entityKind)], - ); - await ref - .read(ionConnectNotifierProvider.notifier) - .sendEntityData(deletionRequest, cache: false); - } +Future _deleteFromServer(Ref ref, CacheableEntity entity) async { + final entityKind = switch (entity) { + PostEntity() => PostEntity.kind, + ArticleEntity() => ArticleEntity.kind, + RepostEntity() => RepostEntity.kind, + GenericRepostEntity() => GenericRepostEntity.kind, + _ => throw DeleteEntityUnsupportedTypeException(), + }; + + final deletionRequest = DeletionRequest( + events: [EventToDelete(eventId: entity.id, kind: entityKind)], + ); + await ref.read(ionConnectNotifierProvider.notifier).sendEntityData(deletionRequest, cache: false); +} - Future _deleteFromDataSources() async { - if (entity case final PostEntity post when post.data.parentEvent != null) { - await _deleteReply(post); - } else if (entity case final ArticleEntity _) { - await _deleteArticle(); - } else { - await _deletePost(); - } +Future _deleteFromDataSources(Ref ref, CacheableEntity entity) async { + if (entity case final PostEntity post when post.data.parentEvent != null) { + await _deleteReply(ref, post); + } else if (entity case final ArticleEntity _) { + await _deleteArticle(ref, entity); + } else { + await _deletePost(ref, entity); } +} - Future _deleteReply(PostEntity post) async { - final dataSource = ref.watch(userRepliesDataSourceProvider(entity.masterPubkey)) ?? []; - - final parentId = post.data.parentEvent!.eventId; - - ref - .read( - repliesCountProvider( - EventReference( - eventId: parentId, - pubkey: post.data.parentEvent!.pubkey, - ), - ).notifier, - ) - .removeOne(); - - await ref - .read( - repliesProvider( - EventReference( - eventId: parentId, - pubkey: post.data.parentEvent!.pubkey, - ), - ).notifier, - ) - .deleteReply(entity: entity); - - ref.read(repliedEventsProvider.notifier).removeReply(parentId, post.id); - - await _deleteFromDataSource(dataSource); - ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); - } +Future _deleteReply(Ref ref, PostEntity post) async { + final dataSource = ref.watch(userRepliesDataSourceProvider(post.masterPubkey)) ?? []; + + final parentId = post.data.parentEvent!.eventId; + + ref + .read( + repliesCountProvider( + EventReference( + eventId: parentId, + pubkey: post.data.parentEvent!.pubkey, + ), + ).notifier, + ) + .removeOne(); + + await ref + .read( + repliesProvider( + EventReference( + eventId: parentId, + pubkey: post.data.parentEvent!.pubkey, + ), + ).notifier, + ) + .deleteReply(entity: post); + + ref.read(repliedEventsProvider.notifier).removeReply(parentId, post.id); + + await _deleteFromDataSource(ref, dataSource, post); + ref.read(ionConnectCacheProvider.notifier).remove(post.cacheKey); +} - Future _deleteArticle() async { - final userArticlesDataSource = ref.watch(userArticlesDataSourceProvider(entity.masterPubkey)); - final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; +Future _deleteArticle(Ref ref, CacheableEntity entity) async { + final userArticlesDataSource = ref.watch(userArticlesDataSourceProvider(entity.masterPubkey)); + final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; - await _deleteFromDataSource(userArticlesDataSource ?? []); - await _deleteFromDataSource(feedDataSources); - ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); - } + await _deleteFromDataSource(ref, userArticlesDataSource ?? [], entity); + await _deleteFromDataSource(ref, feedDataSources, entity); + ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); +} - Future _deletePost() async { - final userVideosDataSource = ref.watch(userVideosDataSourceProvider(entity.masterPubkey)); - final userPostsDataSource = ref.watch(userPostsDataSourceProvider(entity.masterPubkey)); - final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; +Future _deletePost(Ref ref, CacheableEntity entity) async { + final userVideosDataSource = ref.watch(userVideosDataSourceProvider(entity.masterPubkey)); + final userPostsDataSource = ref.watch(userPostsDataSourceProvider(entity.masterPubkey)); + final feedDataSources = ref.watch(feedPostsDataSourceProvider) ?? []; - await _deleteFromDataSource(userVideosDataSource ?? []); - await _deleteFromDataSource(userPostsDataSource ?? []); - await _deleteFromDataSource(feedDataSources); - ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); - } + await _deleteFromDataSource(ref, userVideosDataSource ?? [], entity); + await _deleteFromDataSource(ref, userPostsDataSource ?? [], entity); + await _deleteFromDataSource(ref, feedDataSources, entity); + ref.read(ionConnectCacheProvider.notifier).remove(entity.cacheKey); +} - Future _deleteFromDataSource(List dataSource) async { - await ref.read(entitiesPagedDataProvider(dataSource).notifier).deleteEntity(entity); - } +Future _deleteFromDataSource( + Ref ref, + List dataSource, + CacheableEntity entity, +) async { + await ref.read(entitiesPagedDataProvider(dataSource).notifier).deleteEntity(entity); } diff --git a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart index ccd7f9792..4b2539c34 100644 --- a/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart +++ b/lib/app/features/feed/views/components/delete_feed_item_menu/delete_feed_item_menu.dart @@ -42,7 +42,7 @@ class DeleteFeedItemMenu extends ConsumerWidget { ), onPressed: () async { closeMenu(); - await ref.read(deleteEntityProvider(entity).notifier).delete(); + await ref.read(deleteEntityProvider(entity).future); onDelete?.call(); }, ), diff --git a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart index e11f7986e..02b1c30d9 100644 --- a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart +++ b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart @@ -93,9 +93,7 @@ class RepostOptionsModal extends HookConsumerWidget { ); if (entity case final CacheableEntity cacheableEntity) { - await ref - .read(deleteEntityProvider(cacheableEntity).notifier) - .delete(); + await ref.read(deleteEntityProvider(cacheableEntity).future); if (context.mounted) { context.pop(); } From bf8e0aa1142f59e465dd6f2b8fc874ee97d29109 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:22:35 +0200 Subject: [PATCH 25/27] refactor: parent ref --- .../counter_items_footer.dart | 3 --- .../reposts_counter_button.dart | 3 --- .../providers/repost_entity_provider.c.dart | 17 ++++++++++++++++ .../feed/views/components/post/post.dart | 1 - .../repost_options_modal.dart | 20 ++++++------------- lib/app/router/feed_routes.dart | 3 --- 6 files changed, 23 insertions(+), 24 deletions(-) create mode 100644 lib/app/features/feed/providers/repost_entity_provider.c.dart diff --git a/lib/app/components/counter_items_footer/counter_items_footer.dart b/lib/app/components/counter_items_footer/counter_items_footer.dart index c30ec61a9..c7f643b85 100644 --- a/lib/app/components/counter_items_footer/counter_items_footer.dart +++ b/lib/app/components/counter_items_footer/counter_items_footer.dart @@ -13,7 +13,6 @@ import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; class CounterItemsFooter extends HookConsumerWidget { CounterItemsFooter({ required this.eventReference, - this.repostEventReference, double? bottomPadding, double? topPadding, this.color, @@ -22,7 +21,6 @@ class CounterItemsFooter extends HookConsumerWidget { topPadding = topPadding ?? 10.0.s; final EventReference eventReference; - final EventReference? repostEventReference; final double bottomPadding; final double topPadding; final Color? color; @@ -43,7 +41,6 @@ class CounterItemsFooter extends HookConsumerWidget { Flexible( child: RepostsCounterButton( eventReference: eventReference, - repostEventReference: repostEventReference, color: color, ), ), diff --git a/lib/app/components/counter_items_footer/reposts_counter_button.dart b/lib/app/components/counter_items_footer/reposts_counter_button.dart index 38670b2b9..03f39b255 100644 --- a/lib/app/components/counter_items_footer/reposts_counter_button.dart +++ b/lib/app/components/counter_items_footer/reposts_counter_button.dart @@ -15,13 +15,11 @@ import 'package:ion/generated/assets.gen.dart'; class RepostsCounterButton extends ConsumerWidget { const RepostsCounterButton({ required this.eventReference, - this.repostEventReference, this.color, super.key, }); final EventReference eventReference; - final EventReference? repostEventReference; final Color? color; @override @@ -34,7 +32,6 @@ class RepostsCounterButton extends ConsumerWidget { HapticFeedback.lightImpact(); RepostOptionsModalRoute( eventReference: eventReference.toString(), - repostEventReference: repostEventReference?.toString(), ).push(context); }, child: TextActionButton( diff --git a/lib/app/features/feed/providers/repost_entity_provider.c.dart b/lib/app/features/feed/providers/repost_entity_provider.c.dart new file mode 100644 index 000000000..2a17a2890 --- /dev/null +++ b/lib/app/features/feed/providers/repost_entity_provider.c.dart @@ -0,0 +1,17 @@ +import 'package:collection/collection.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/features/feed/data/models/entities/repost_data.c.dart'; +import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; +import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'repost_entity_provider.c.g.dart'; + +@riverpod +CacheableEntity? repostEntity(Ref ref, EventReference eventReference) { + final cache = ref.read(ionConnectCacheProvider); + + return cache.values.firstWhereOrNull( + (entity) => entity is RepostEntity && entity.data.eventId == eventReference.eventId, + ); +} diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index 4423e9506..c6198e55b 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -72,7 +72,6 @@ class Post extends ConsumerWidget { footer ?? CounterItemsFooter( eventReference: eventReference, - repostEventReference: repostEventReference, ), ], ); diff --git a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart index 02b1c30d9..d0ebdf03b 100644 --- a/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart +++ b/lib/app/features/feed/views/pages/repost_options_modal/repost_options_modal.dart @@ -12,11 +12,11 @@ import 'package:ion/app/components/separated/separated_column.dart'; import 'package:ion/app/extensions/extensions.dart'; import 'package:ion/app/features/feed/providers/counters/reposted_events_provider.c.dart'; import 'package:ion/app/features/feed/providers/delete_entity_provider.c.dart'; +import 'package:ion/app/features/feed/providers/repost_entity_provider.c.dart'; import 'package:ion/app/features/feed/providers/repost_notifier.c.dart'; import 'package:ion/app/features/feed/views/pages/repost_options_modal/repost_option_action.dart'; import 'package:ion/app/features/ion_connect/model/event_reference.c.dart'; import 'package:ion/app/features/ion_connect/providers/ion_connect_cache.c.dart'; -import 'package:ion/app/features/ion_connect/providers/ion_connect_entity_provider.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; @@ -25,12 +25,10 @@ import 'package:ion/app/router/components/sheet_content/sheet_content.dart'; class RepostOptionsModal extends HookConsumerWidget { const RepostOptionsModal({ required this.eventReference, - this.repostEventReference, super.key, }); final EventReference eventReference; - final EventReference? repostEventReference; @override Widget build(BuildContext context, WidgetRef ref) { @@ -85,18 +83,12 @@ class RepostOptionsModal extends HookConsumerWidget { CreatePostRoute(quotedEvent: eventReference.toString()).go(context); case RepostOptionAction.undoRepost: selectedAction.value = option; + final repostEntity = ref.read(repostEntityProvider(eventReference)); - if (repostEventReference != null) { - final entity = await ref.read( - ionConnectEntityProvider(eventReference: repostEventReference!) - .future, - ); - - if (entity case final CacheableEntity cacheableEntity) { - await ref.read(deleteEntityProvider(cacheableEntity).future); - if (context.mounted) { - context.pop(); - } + if (repostEntity case final CacheableEntity entity) { + await ref.read(deleteEntityProvider(entity).future); + if (context.mounted) { + context.pop(); } } selectedAction.value = null; diff --git a/lib/app/router/feed_routes.dart b/lib/app/router/feed_routes.dart index 6dbaac26f..2f3a5bb89 100644 --- a/lib/app/router/feed_routes.dart +++ b/lib/app/router/feed_routes.dart @@ -73,9 +73,6 @@ class RepostOptionsModalRoute extends BaseRouteData { }) : super( child: RepostOptionsModal( eventReference: EventReference.fromString(eventReference), - repostEventReference: repostEventReference != null - ? EventReference.fromString(repostEventReference) - : null, ), type: IceRouteType.bottomSheet, ); From 8eaf10fb4f1de9fb26a59615dad1e5ef889f0d89 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:25:57 +0200 Subject: [PATCH 26/27] fix: add license --- lib/app/features/feed/providers/repost_entity_provider.c.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/app/features/feed/providers/repost_entity_provider.c.dart b/lib/app/features/feed/providers/repost_entity_provider.c.dart index 2a17a2890..ced29500a 100644 --- a/lib/app/features/feed/providers/repost_entity_provider.c.dart +++ b/lib/app/features/feed/providers/repost_entity_provider.c.dart @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: ice License 1.0 + import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/features/feed/data/models/entities/repost_data.c.dart'; From 0b838d7c7b2f968f2dc22eb6cbff18bbfaa954b0 Mon Sep 17 00:00:00 2001 From: ice-hector <96414297+ice-hector@users.noreply.github.com> Date: Wed, 22 Jan 2025 09:42:59 +0200 Subject: [PATCH 27/27] fix: pr comments --- .../components/entities_list/components/repost_list_item.dart | 3 +-- lib/app/features/feed/views/components/post/post.dart | 2 -- lib/app/router/feed_routes.dart | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/app/features/components/entities_list/components/repost_list_item.dart b/lib/app/features/components/entities_list/components/repost_list_item.dart index 4eb8303b4..a96f4a635 100644 --- a/lib/app/features/components/entities_list/components/repost_list_item.dart +++ b/lib/app/features/components/entities_list/components/repost_list_item.dart @@ -16,7 +16,6 @@ class RepostListItem extends StatelessWidget { @override Widget build(BuildContext context) { final eventReference = EventReference(eventId: repost.data.eventId, pubkey: repost.data.pubkey); - final repostEventReference = EventReference(eventId: repost.id, pubkey: repost.pubkey); return GestureDetector( onTap: () => PostDetailsRoute(eventReference: eventReference.toString()).push(context), @@ -25,7 +24,7 @@ class RepostListItem extends StatelessWidget { child: Column( children: [ RepostAuthorHeader(pubkey: repost.masterPubkey), - Post(eventReference: eventReference, repostEventReference: repostEventReference), + Post(eventReference: eventReference), ], ), ), diff --git a/lib/app/features/feed/views/components/post/post.dart b/lib/app/features/feed/views/components/post/post.dart index c6198e55b..e584933e8 100644 --- a/lib/app/features/feed/views/components/post/post.dart +++ b/lib/app/features/feed/views/components/post/post.dart @@ -23,7 +23,6 @@ import 'package:ion/app/router/app_routes.c.dart'; class Post extends ConsumerWidget { const Post({ required this.eventReference, - this.repostEventReference, this.header, this.footer, this.showParent = false, @@ -32,7 +31,6 @@ class Post extends ConsumerWidget { }); final EventReference eventReference; - final EventReference? repostEventReference; final bool showParent; final Widget? header; final Widget? footer; diff --git a/lib/app/router/feed_routes.dart b/lib/app/router/feed_routes.dart index 2f3a5bb89..389bb9e9c 100644 --- a/lib/app/router/feed_routes.dart +++ b/lib/app/router/feed_routes.dart @@ -69,7 +69,6 @@ class NotificationsHistoryRoute extends BaseRouteData { class RepostOptionsModalRoute extends BaseRouteData { RepostOptionsModalRoute({ required this.eventReference, - this.repostEventReference, }) : super( child: RepostOptionsModal( eventReference: EventReference.fromString(eventReference), @@ -78,7 +77,6 @@ class RepostOptionsModalRoute extends BaseRouteData { ); final String eventReference; - final String? repostEventReference; } class SharePostModalRoute extends BaseRouteData {