Skip to content

Commit 0c57512

Browse files
committed
Added delete for everyone/self option in conversation>settings>attachment
1 parent 9e923e1 commit 0c57512

File tree

8 files changed

+194
-208
lines changed

8 files changed

+194
-208
lines changed

Session.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,7 @@
11641164
FDFE75B42ABD46B600655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; };
11651165
FDFE75B52ABD46B700655640 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; };
11661166
FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; };
1167+
FE4234C92E9CDECD00F2C9F7 /* AttachmentDeleter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE4234C82E9CDECD00F2C9F7 /* AttachmentDeleter.swift */; };
11671168
FED288F32E4C28CF00C31171 /* AppReviewPromptDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */; };
11681169
FED288F82E4C3BE100C31171 /* AppReviewPromptModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */; };
11691170
/* End PBXBuildFile section */
@@ -2450,6 +2451,7 @@
24502451
FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = "<group>"; };
24512452
FDFE75B02ABD2D2400655640 /* _030_MakeBrokenProfileTimestampsNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _030_MakeBrokenProfileTimestampsNullable.swift; sourceTree = "<group>"; };
24522453
FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = "<group>"; };
2454+
FE4234C82E9CDECD00F2C9F7 /* AttachmentDeleter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDeleter.swift; sourceTree = "<group>"; };
24532455
FED288F22E4C28CF00C31171 /* AppReviewPromptDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptDialog.swift; sourceTree = "<group>"; };
24542456
FED288F72E4C3BE100C31171 /* AppReviewPromptModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReviewPromptModel.swift; sourceTree = "<group>"; };
24552457
/* End PBXFileReference section */
@@ -3213,6 +3215,7 @@
32133215
FD7115EA28C5D78E00B47552 /* ThreadSettingsViewModel.swift */,
32143216
9479981B2DD44AC5008F5CD5 /* ThreadNotificationSettingsViewModel.swift */,
32153217
FD7115F328C71EB200B47552 /* ThreadDisappearingMessagesSettingsViewModel.swift */,
3218+
FE4234C82E9CDECD00F2C9F7 /* AttachmentDeleter.swift */,
32163219
);
32173220
path = Settings;
32183221
sourceTree = "<group>";
@@ -7033,6 +7036,7 @@
70337036
7B0EFDF0275084AA00FFAAE7 /* CallMessageCell.swift in Sources */,
70347037
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */,
70357038
FD10AF0C2AF32B9A007709E5 /* SessionListViewModel.swift in Sources */,
7039+
FE4234C92E9CDECD00F2C9F7 /* AttachmentDeleter.swift in Sources */,
70367040
942256822C23F8BB00C0FDBF /* InviteAFriendScreen.swift in Sources */,
70377041
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
70387042
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,

Session/Conversations/ConversationVC+Interaction.swift

Lines changed: 137 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -2352,102 +2352,7 @@ extension ConversationVC:
23522352
/// Retrieve the deletion actions for the selected message(s) of there are any
23532353
let messagesToDelete: [MessageViewModel] = [cellViewModel]
23542354

2355-
guard let deletionBehaviours: MessageViewModel.DeletionBehaviours = self.viewModel.deletionActions(for: messagesToDelete) else {
2356-
return
2357-
}
2358-
2359-
let modal: ConfirmationModal = ConfirmationModal(
2360-
info: ConfirmationModal.Info(
2361-
title: deletionBehaviours.title,
2362-
body: .radio(
2363-
explanation: ThemedAttributedString(string: deletionBehaviours.body),
2364-
warning: deletionBehaviours.warning.map { ThemedAttributedString(string: $0) },
2365-
options: deletionBehaviours.actions.map { action in
2366-
ConfirmationModal.Info.Body.RadioOptionInfo(
2367-
title: action.title,
2368-
enabled: action.state != .disabled,
2369-
selected: action.state == .enabledAndDefaultSelected,
2370-
accessibility: action.accessibility
2371-
)
2372-
}
2373-
),
2374-
confirmTitle: "delete".localized(),
2375-
confirmStyle: .danger,
2376-
cancelTitle: "cancel".localized(),
2377-
cancelStyle: .alert_text,
2378-
dismissOnConfirm: false,
2379-
onConfirm: { [weak self, dependencies = viewModel.dependencies] modal in
2380-
/// Determine the selected action index
2381-
let selectedIndex: Int = {
2382-
switch modal.info.body {
2383-
case .radio(_, _, let options):
2384-
return options
2385-
.enumerated()
2386-
.first(where: { _, value in value.selected })
2387-
.map { index, _ in index }
2388-
.defaulting(to: 0)
2389-
2390-
default: return 0
2391-
}
2392-
}()
2393-
2394-
/// Stop the messages audio if needed
2395-
messagesToDelete.forEach { cellViewModel in
2396-
self?.viewModel.stopAudioIfNeeded(for: cellViewModel)
2397-
}
2398-
2399-
/// Trigger the deletion behaviours
2400-
deletionBehaviours
2401-
.publisherForAction(at: selectedIndex, using: dependencies)
2402-
.showingBlockingLoading(
2403-
in: deletionBehaviours.requiresNetworkRequestForAction(at: selectedIndex) ?
2404-
self?.viewModel.navigatableState :
2405-
nil
2406-
)
2407-
.sinkUntilComplete(
2408-
receiveCompletion: { result in
2409-
DispatchQueue.main.async {
2410-
switch result {
2411-
case .finished:
2412-
modal.dismiss(animated: true) {
2413-
/// Dispatch after a delay because becoming the first responder can cause
2414-
/// an odd appearance animation
2415-
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) {
2416-
self?.viewModel.showToast(
2417-
text: "deleteMessageDeleted"
2418-
.putNumber(messagesToDelete.count)
2419-
.localized(),
2420-
backgroundColor: .backgroundSecondary,
2421-
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
2422-
)
2423-
}
2424-
}
2425-
2426-
case .failure:
2427-
self?.viewModel.showToast(
2428-
text: "deleteMessageFailed"
2429-
.putNumber(messagesToDelete.count)
2430-
.localized(),
2431-
backgroundColor: .backgroundSecondary,
2432-
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
2433-
)
2434-
}
2435-
completion?()
2436-
}
2437-
}
2438-
)
2439-
},
2440-
afterClosed: { [weak self] in
2441-
self?.becomeFirstResponder()
2442-
}
2443-
)
2444-
)
2445-
2446-
/// Show the modal after a small delay so it doesn't look as weird with the context menu dismissal
2447-
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in
2448-
self?.present(modal, animated: true)
2449-
self?.resignFirstResponder()
2450-
}
2355+
onDelete(messagesToDelete, completion: completion)
24512356
}
24522357

24532358
func save(_ cellViewModel: MessageViewModel, completion: (() -> Void)?) {
@@ -3348,4 +3253,140 @@ extension ConversationVC: MediaPresentationContextProvider {
33483253
func snapshotOverlayView(in coordinateSpace: UICoordinateSpace) -> (UIView, CGRect)? {
33493254
return self.navigationController?.navigationBar.generateSnapshot(in: coordinateSpace)
33503255
}
3256+
3257+
3258+
// MARK: - Delete Messages
3259+
func deleteAttachments(_ items: [MediaGalleryViewModel.Item], completion: @escaping () -> Void) {
3260+
let desiredIDSet: Set<Int64> = Set(items.map { $0.interactionId })
3261+
3262+
let messagesToDelete: [MessageViewModel] = viewModel.interactionData
3263+
.flatMap { $0.elements }
3264+
.filter { desiredIDSet.contains($0.id) }
3265+
3266+
onDelete(messagesToDelete, fromAttachments: true) {
3267+
completion()
3268+
}
3269+
}
3270+
3271+
func onDelete(_ messagesToDelete: [MessageViewModel], fromAttachments: Bool = false, completion: (() -> Void)? = nil) {
3272+
guard let topViewController = viewModel.dependencies[singleton: .appContext].frontMostViewController else {
3273+
return
3274+
}
3275+
3276+
guard let deletionBehaviours: MessageViewModel.DeletionBehaviours = self.viewModel.deletionActions(for: messagesToDelete) else {
3277+
return
3278+
}
3279+
3280+
var alertContentForDeletion: (title: String, body: String) {
3281+
guard fromAttachments else {
3282+
return (
3283+
deletionBehaviours.title,
3284+
deletionBehaviours.body,
3285+
)
3286+
}
3287+
3288+
// If items to delete contains other messages aside from attachments w/ message
3289+
return (
3290+
"deleteAttachments"
3291+
.putNumber(messagesToDelete.count)
3292+
.localized(),
3293+
"deleteAttachmentsDescription"
3294+
.putNumber(messagesToDelete.count)
3295+
.localized(),
3296+
)
3297+
}
3298+
3299+
let modal: ConfirmationModal = ConfirmationModal(
3300+
info: ConfirmationModal.Info(
3301+
title: alertContentForDeletion.title,
3302+
body: .radio(
3303+
explanation: ThemedAttributedString(string: alertContentForDeletion.body),
3304+
warning: deletionBehaviours.warning.map { ThemedAttributedString(string: $0) },
3305+
options: deletionBehaviours.actions.map { action in
3306+
ConfirmationModal.Info.Body.RadioOptionInfo(
3307+
title: action.title,
3308+
enabled: action.state != .disabled,
3309+
selected: action.state == .enabledAndDefaultSelected,
3310+
accessibility: action.accessibility
3311+
)
3312+
}
3313+
),
3314+
confirmTitle: "delete".localized(),
3315+
confirmStyle: .danger,
3316+
cancelTitle: "cancel".localized(),
3317+
cancelStyle: .alert_text,
3318+
dismissOnConfirm: false,
3319+
onConfirm: { [weak self, dependencies = viewModel.dependencies] modal in
3320+
/// Determine the selected action index
3321+
let selectedIndex: Int = {
3322+
switch modal.info.body {
3323+
case .radio(_, _, let options):
3324+
return options
3325+
.enumerated()
3326+
.first(where: { _, value in value.selected })
3327+
.map { index, _ in index }
3328+
.defaulting(to: 0)
3329+
3330+
default: return 0
3331+
}
3332+
}()
3333+
3334+
/// Stop the messages audio if needed
3335+
messagesToDelete.forEach { cellViewModel in
3336+
self?.viewModel.stopAudioIfNeeded(for: cellViewModel)
3337+
}
3338+
3339+
/// Trigger the deletion behaviours
3340+
deletionBehaviours
3341+
.publisherForAction(at: selectedIndex, using: dependencies)
3342+
.showingBlockingLoading(
3343+
in: deletionBehaviours.requiresNetworkRequestForAction(at: selectedIndex) ?
3344+
self?.viewModel.navigatableState :
3345+
nil
3346+
)
3347+
.sinkUntilComplete(
3348+
receiveCompletion: { result in
3349+
DispatchQueue.main.async {
3350+
switch result {
3351+
case .finished:
3352+
modal.dismiss(animated: true) {
3353+
/// Dispatch after a delay because becoming the first responder can cause
3354+
/// an odd appearance animation
3355+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) {
3356+
self?.viewModel.showToast(
3357+
text: "deleteMessageDeleted"
3358+
.putNumber(messagesToDelete.count)
3359+
.localized(),
3360+
backgroundColor: .backgroundSecondary,
3361+
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
3362+
)
3363+
}
3364+
}
3365+
3366+
case .failure:
3367+
self?.viewModel.showToast(
3368+
text: "deleteMessageFailed"
3369+
.putNumber(messagesToDelete.count)
3370+
.localized(),
3371+
backgroundColor: .backgroundSecondary,
3372+
inset: (self?.inputAccessoryView?.frame.height ?? Values.mediumSpacing) + Values.smallSpacing
3373+
)
3374+
}
3375+
completion?()
3376+
}
3377+
}
3378+
)
3379+
},
3380+
afterClosed: { [weak self] in
3381+
self?.becomeFirstResponder()
3382+
}
3383+
)
3384+
)
3385+
3386+
/// Show the modal after a small delay so it doesn't look as weird with the context menu dismissal
3387+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(ContextMenuVC.dismissDurationPartOne * 1000))) { [weak self] in
3388+
topViewController.present(modal, animated: true)
3389+
self?.resignFirstResponder()
3390+
}
3391+
}
33513392
}

Session/Conversations/ConversationVC.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,11 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
557557
// conversation settings then we don't need to worry about the conversation getting marked as
558558
// when when the user returns back through this view controller
559559
self.viewModel.markAsRead(target: .thread, timestampMs: nil)
560+
561+
// Message deletion
562+
self.viewModel.dependencies[singleton: .attachmentDeleter].willTriggerDeleteOption = { [weak self] (items, completion) in
563+
self?.deleteAttachments(items, completion: completion)
564+
}
560565
}
561566

562567
override func viewWillAppear(_ animated: Bool) {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
2+
3+
import Foundation
4+
import SessionUtilitiesKit
5+
import SessionMessagingKit
6+
7+
public extension Singleton {
8+
static let attachmentDeleter: SingletonConfig<AttachmentDeleter> = Dependencies.create(
9+
identifier: "attachmentDeleter",
10+
createInstance: { _ in AttachmentDeleter() }
11+
)
12+
}
13+
14+
public class AttachmentDeleter {
15+
// Hook for the listener class
16+
public var willTriggerDeleteOption: (([MediaGalleryViewModel.Item], @escaping () -> Void) -> Void)? = nil
17+
18+
// Reusable delete function for all the media preview instances
19+
public func deleteAttachments(_ items: [MediaGalleryViewModel.Item], completion: @escaping () -> Void) {
20+
willTriggerDeleteOption?(items) {
21+
completion()
22+
}
23+
}
24+
}

Session/Media Viewing & Editing/AllMediaViewController.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ extension AllMediaViewController: MediaTileViewControllerDelegate {
231231
)
232232
}
233233
else {
234+
self.navigationItem.hidesBackButton = false
235+
234236
self.navigationItem.rightBarButtonItem = UIBarButtonItem(
235237
title: "select".localized(),
236238
style: .plain,

Session/Media Viewing & Editing/MediaPageViewController.swift

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -567,44 +567,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
567567

568568
@objc public func didPressDelete(_ sender: Any) {
569569
guard let itemToDelete: MediaGalleryViewModel.Item = self.currentItem else { return }
570-
571-
let actionSheet: UIAlertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
572-
let deleteAction = UIAlertAction(
573-
title: "clearMessagesForMe".localized(),
574-
style: .destructive
575-
) { [dependencies = viewModel.dependencies] _ in
576-
dependencies[singleton: .storage].writeAsync { db in
577-
_ = try Attachment
578-
.filter(id: itemToDelete.attachment.id)
579-
.deleteAll(db)
580-
581-
// Add the garbage collection job to delete orphaned attachment files
582-
dependencies[singleton: .jobRunner].add(
583-
db,
584-
job: Job(
585-
variant: .garbageCollection,
586-
behaviour: .runOnce,
587-
details: GarbageCollectionJob.Details(
588-
typesToCollect: [.orphanedAttachmentFiles]
589-
)
590-
),
591-
canStartJob: true
592-
)
593-
594-
// Delete any interactions which had all of their attachments removed
595-
try Interaction.deleteWhere(
596-
db,
597-
.filter(Interaction.Columns.id == itemToDelete.interactionId),
598-
.hasAttachments(false)
599-
)
600-
}
601-
}
602-
actionSheet.addAction(UIAlertAction(title: "cancel".localized(), style: .cancel))
603-
actionSheet.addAction(deleteAction)
604-
605-
Modal.setupForIPadIfNeeded(actionSheet, targetView: self.view)
606-
self.present(actionSheet, animated: true)
570+
viewModel.dependencies[singleton: .attachmentDeleter].deleteAttachments([itemToDelete]) {}
607571
}
572+
608573

609574
// MARK: - Video interaction
610575

0 commit comments

Comments
 (0)