Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: delete posts/articles/quotes/replies from the feed and profile #568

Merged
merged 27 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b874799
feat: add ability to delete own post from feed
ice-hector Jan 13, 2025
6e4638c
feat: clean up and rebuild feed after deletion
ice-hector Jan 13, 2025
6aa66ad
fix: unused var
ice-hector Jan 13, 2025
8c80ad5
feat: handle feed update after deletion
ice-hector Jan 13, 2025
0c6f315
fix: close post details screen on delete
ice-hector Jan 13, 2025
68d4246
feat: article deletion from the feed
ice-hector Jan 15, 2025
5d70bb2
feat: delete entity provider and reply deletion
ice-hector Jan 15, 2025
b16a497
feat: delete article from article details screen
ice-hector Jan 15, 2025
eb5d43e
feat: handle undo repost
ice-hector Jan 16, 2025
b339f2a
feat: adjust undo repost design
ice-hector Jan 16, 2025
15d5b4c
feat: add ability to delete own post from feed
ice-hector Jan 16, 2025
7aec7b0
feat: remove reposts
ice-hector Jan 16, 2025
52f20fc
fix: reply remove from profile
ice-hector Jan 17, 2025
7ec899b
feat: handle article deletion from profile
ice-hector Jan 17, 2025
7c5a4cc
feat: handle videos and post entities deletion from profile
ice-hector Jan 17, 2025
5d49289
refactor: delete provider
ice-hector Jan 17, 2025
f879643
fix: remove logs
ice-hector Jan 20, 2025
10f1b07
feat: handle reply counter increase and decrease
ice-hector Jan 20, 2025
200e749
fix: warning
ice-hector Jan 20, 2025
46582c3
fix: comments
ice-hector Jan 21, 2025
4e6f258
fix: replies count increase and decrease
ice-hector Jan 21, 2025
bf88250
fix: pr comment
ice-hector Jan 21, 2025
a35c872
fix: isReplied
ice-hector Jan 21, 2025
319f3ee
refactor: delete entity provider
ice-hector Jan 21, 2025
bf8e0aa
refactor: parent ref
ice-hector Jan 21, 2025
8eaf10f
fix: add license
ice-hector Jan 21, 2025
0b838d7
fix: pr comments
ice-hector Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 30 additions & 30 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
external_app_launcher: ad55ac844aa21f2d2197d7cec58ff0d0dc40bbc0
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ class RepostsCounterButton extends ConsumerWidget {
return GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
RepostOptionsModalRoute(eventReference: eventReference.toString()).push<void>(context);
RepostOptionsModalRoute(
eventReference: eventReference.toString(),
).push<void>(context);
},
child: TextActionButton(
icon: Assets.svg.iconBlockRepost.icon(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ class ModalActionButton extends StatelessWidget {
required this.label,
required this.onTap,
this.trailing,
this.labelStyle,
super.key,
});

final Widget icon;
final String label;
final Widget? trailing;
final VoidCallback onTap;
final TextStyle? labelStyle;

@override
Widget build(BuildContext context) {
Expand All @@ -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),
Expand Down
5 changes: 5 additions & 0 deletions lib/app/exceptions/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(10048, 'Failed to delete entity, unsupported type');
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,110 @@ 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/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<Set<String>?> 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<Set<String>>({}, (result, entry) {
final currentUserRepliedIds = _getCurrentUserRepliedIds(entry, currentPubkey: currentPubkey);
if (currentUserRepliedIds != null) {
result.addAll(currentUserRepliedIds);
}
return result;
});
class RepliedEvents extends _$RepliedEvents {
final _deletedIds = <String>{};

@override
Stream<Map<String, List<String>>?> build() async* {
final currentPubkey = await ref.watch(currentPubkeySelectorProvider.future);

if (currentPubkey == null) {
yield {};
} else {
var repliedMap = <String, List<String>>{};

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);

yield repliedIds;
if (currentUserRepliedIds != null) {
final validIds =
currentUserRepliedIds.where((id) => !_deletedIds.contains(id)).toList();

await for (final entity in ref.watch(ionConnectCacheStreamProvider)) {
final currentUserRepliedIds = _getCurrentUserRepliedIds(entity, currentPubkey: currentPubkey);
if (currentUserRepliedIds != null) {
yield repliedIds = {...repliedIds, ...currentUserRepliedIds};
final updatedMap = Map<String, List<String>>.from(repliedMap);

if (validIds.isEmpty) {
updatedMap.remove(parentId);
} else {
updatedMap[parentId] = validIds;
}

repliedMap = updatedMap;
yield repliedMap;
}
}
}
}
}
}

List<String>? _getCurrentUserRepliedIds(IonConnectEntity entity, {required String currentPubkey}) {
if (entity.masterPubkey != currentPubkey || entity is! PostEntity) {
return null;
}
void removeReply(String parentId, String replyId) {
_deletedIds.add(replyId);

final currentMap = state.valueOrNull;
if (currentMap != null && currentMap.containsKey(parentId)) {
final updatedReplies = Map<String, List<String>>.from(currentMap);
updatedReplies[parentId] = updatedReplies[parentId]!.where((id) => id != replyId).toList();

final relatedEvents = entity.data.relatedEvents;
if (updatedReplies[parentId]!.isEmpty) {
updatedReplies.remove(parentId);
}

if (relatedEvents == null) {
return null;
state = AsyncData(updatedReplies);
}
}
}

Map<String, List<String>> _buildInitialMap(
Map<String, CacheableEntity> cache,
String currentPubkey,
) {
return cache.values.fold<Map<String, List<String>>>({}, (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;
});
}

return [
for (final event in relatedEvents)
if (event.marker == RelatedEventMarker.reply || event.marker == RelatedEventMarker.root)
event.eventId,
];
List<String>? _getCurrentUserRepliedIds(IonConnectEntity entity, {required String currentPubkey}) {
if (entity case final PostEntity post when post.masterPubkey == currentPubkey) {
if (post.data.parentEvent != null) {
return [post.id];
}
}
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 replyIds = repliedMap?[eventReference.eventId];

final deletedIds = ref.read(repliedEventsProvider.notifier)._deletedIds;
final validReplyIds = replyIds?.where((id) => !deletedIds.contains(id)).toList();
final hasReply = validReplyIds?.isNotEmpty ?? false;

return hasReply;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ part 'replies_count_provider.c.g.dart';
class RepliesCount extends _$RepliesCount {
@override
int build(EventReference eventReference) {
final entity = ref.watch(
ionConnectCacheProvider.select(
cacheSelector<EventCountResultEntity>(
EventCountResultEntity.cacheKeyBuilder(
key: eventReference.eventId,
type: EventCountResultType.replies,
),
),
),
);
final cacheCount = ref
.watch(
ionConnectCacheProvider.select(
cacheSelector<EventCountResultEntity>(
EventCountResultEntity.cacheKeyBuilder(
key: eventReference.eventId,
type: EventCountResultType.replies,
),
),
),
)
?.data
.content as int? ??
0;

if (entity == null) {
return 0;
}

return entity.data.content as int;
return cacheCount;
}

void addOne() {
Expand Down
Loading