Skip to content

Commit 930d311

Browse files
committed
Add a possibility to delete downloaded media to free up storage
1 parent 03b6a74 commit 930d311

File tree

12 files changed

+262
-6
lines changed

12 files changed

+262
-6
lines changed

ChatSecure.xcodeproj/project.pbxproj

+8
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
63F0CAFB1E60C1B40045359C /* OTRYapViewTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F0CAFA1E60C1B40045359C /* OTRYapViewTest.swift */; };
3434
63F614DC1BB214660083A06A /* ChatSecureModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F614DB1BB214660083A06A /* ChatSecureModelTest.swift */; };
3535
7CD871CB705CA365E0755104 /* libPods-ChatSecureCorePods-ChatSecureTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5179DA87B83F57EEA9589733 /* libPods-ChatSecureCorePods-ChatSecureTests.a */; };
36+
8F56C60D22313225DC3E3E4E /* OTRStorageUsageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F56C0C0D2783FF60F293884 /* OTRStorageUsageViewController.swift */; };
37+
8F56CEB16F4C0412C383BCF8 /* OTRStorageUsageSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F56CDC5F0CD5BA670188689 /* OTRStorageUsageSetting.swift */; };
3638
D9108AA023F9ABDF00B1280D /* AESGCMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9108A9F23F9ABDF00B1280D /* AESGCMTests.swift */; };
3739
D91F9EFE1ED645F100AEA62C /* FileTransferIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D91F9EFD1ED645F100AEA62C /* FileTransferIntegrationTests.swift */; };
3840
D9365E7A1A1EB0050006434A /* torrc in Resources */ = {isa = PBXBuildFile; fileRef = D9365E791A1EB0050006434A /* torrc */; };
@@ -635,7 +637,9 @@
635637
6C1E59A7F629602AA386C2B8 /* Pods-ChatSecureCorePods-ChatSecure.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatSecureCorePods-ChatSecure.debug.xcconfig"; path = "Target Support Files/Pods-ChatSecureCorePods-ChatSecure/Pods-ChatSecureCorePods-ChatSecure.debug.xcconfig"; sourceTree = "<group>"; };
636638
83C35A70D105953D80691D31 /* libPods-ChatSecureCorePods-ChatSecureCore.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ChatSecureCorePods-ChatSecureCore.a"; sourceTree = BUILT_PRODUCTS_DIR; };
637639
8B0F7D8477AAAE9D06628430 /* Pods-ChatSecureCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatSecureCore.release.xcconfig"; path = "Target Support Files/Pods-ChatSecureCore/Pods-ChatSecureCore.release.xcconfig"; sourceTree = "<group>"; };
640+
8F56C0C0D2783FF60F293884 /* OTRStorageUsageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTRStorageUsageViewController.swift; sourceTree = "<group>"; };
638641
8F56C50436DA64774EBB16E3 /* OTRMessagesLoadingView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OTRMessagesLoadingView.xib; sourceTree = "<group>"; };
642+
8F56CDC5F0CD5BA670188689 /* OTRStorageUsageSetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTRStorageUsageSetting.swift; sourceTree = "<group>"; };
639643
9093D0A3DB37442CFB9718F8 /* Pods-ChatSecureCorePods-ChatSecureTests.macos_release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ChatSecureCorePods-ChatSecureTests.macos_release.xcconfig"; path = "Target Support Files/Pods-ChatSecureCorePods-ChatSecureTests/Pods-ChatSecureCorePods-ChatSecureTests.macos_release.xcconfig"; sourceTree = "<group>"; };
640644
9224A5F2207E3BD800A044BF /* JoinRoomView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = JoinRoomView.xib; path = Interface/JoinRoomView.xib; sourceTree = "<group>"; };
641645
924F67C41EA5541C00528FB6 /* MigrationInfoHeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MigrationInfoHeaderView.xib; path = Interface/MigrationInfoHeaderView.xib; sourceTree = "<group>"; };
@@ -1431,6 +1435,7 @@
14311435
D9C52842235CB580002B213A /* OTRCertificateDomainViewController.h */,
14321436
D9C52843235CB580002B213A /* OTRComposeViewController.m */,
14331437
D9C52844235CB580002B213A /* OTRLogListViewController.swift */,
1438+
8F56C0C0D2783FF60F293884 /* OTRStorageUsageViewController.swift */,
14341439
);
14351440
path = "View Controllers";
14361441
sourceTree = "<group>";
@@ -1538,6 +1543,7 @@
15381543
D9C5286C235CB580002B213A /* OTRCertificateSetting.h */,
15391544
D9C5286D235CB580002B213A /* OTRListSettingValue.m */,
15401545
D9C5286E235CB580002B213A /* OTRFeedbackSetting.m */,
1546+
8F56CDC5F0CD5BA670188689 /* OTRStorageUsageSetting.swift */,
15411547
);
15421548
path = Settings;
15431549
sourceTree = "<group>";
@@ -2854,6 +2860,8 @@
28542860
D9C52A17235CB580002B213A /* PushMessage.swift in Sources */,
28552861
D9C529D5235CB580002B213A /* OTRSettingsViewController.m in Sources */,
28562862
D9C52A1F235CB581002B213A /* BuddySubscriptions.swift in Sources */,
2863+
8F56C60D22313225DC3E3E4E /* OTRStorageUsageViewController.swift in Sources */,
2864+
8F56CEB16F4C0412C383BCF8 /* OTRStorageUsageSetting.swift in Sources */,
28572865
);
28582866
runOnlyForDeploymentPostprocessing = 0;
28592867
};

ChatSecureCore/Classes/Controllers/OTRMediaFileManager.m

+9-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ - (void)setData:(NSData *)data
134134
//#865
135135
- (void)deleteDataForItem:(OTRMediaItem *)mediaItem
136136
buddyUniqueId:(NSString *)buddyUniqueId
137-
completion:(void (^)(BOOL success, NSError * _Nullable error))completion
137+
completion:(nullable void (^)(BOOL success, NSError * _Nullable error))completion
138138
completionQueue:(nullable dispatch_queue_t)completionQueue {
139139
if (!completionQueue) {
140140
completionQueue = dispatch_get_main_queue();
@@ -284,4 +284,12 @@ - (void) performAsyncWrite:(dispatch_block_t)block {
284284
}
285285
}
286286

287+
- (void)vacuum:(dispatch_block_t)completion {
288+
[self performAsyncWrite:^{
289+
[self.ioCipher vacuum];
290+
if (completion != nil) {
291+
dispatch_async(dispatch_get_main_queue(), completion);
292+
}
293+
}];
294+
}
287295
@end

ChatSecureCore/Classes/Controllers/OTRSettingsManager.m

+5-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,11 @@ - (void) populateSettings
108108
[settingsGroups addObject:pushGroup];
109109
}
110110

111-
112-
NSArray *chatSettings = @[deletedDisconnectedConversations];
111+
OTRStorageUsageSetting *storageUsageSetting = [[OTRStorageUsageSetting alloc] initWithTitle:STORAGE_USAGE_TITLE()
112+
description:STORAGE_USAGE_DESCRIPTION()];
113+
storageUsageSetting.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
114+
115+
NSArray *chatSettings = @[deletedDisconnectedConversations, storageUsageSetting];
113116
OTRSettingsGroup *chatSettingsGroup = [[OTRSettingsGroup alloc] initWithTitle:CHAT_STRING() settings:chatSettings];
114117
[settingsGroups addObject:chatSettingsGroup];
115118

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// Created by Vyacheslav Karpukhin on 20.02.20.
3+
// Copyright (c) 2020 Chris Ballinger. All rights reserved.
4+
//
5+
6+
import Foundation
7+
8+
open class OTRStorageUsageSetting : OTRViewSetting {
9+
public override init!(title newTitle: String!, description newDescription: String!) {
10+
super.init(title: newTitle, description: newDescription, viewControllerClass: OTRStorageUsageViewController.self)
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//
2+
// Created by Vyacheslav Karpukhin on 19.02.20.
3+
// Copyright (c) 2020 Chris Ballinger. All rights reserved.
4+
//
5+
6+
import Foundation
7+
import OTRAssets
8+
import MBProgressHUD
9+
10+
class OTRStorageUsageViewController : XLFormViewController {
11+
private let ROOT_SECTION_TAG = "rootSection"
12+
private let DELETE_ALL_TAG = "deleteAll"
13+
private let NO_MEDIA_FOUND_TAG = "noMediaFound"
14+
15+
private let connections = OTRDatabaseManager.shared.connections
16+
17+
init() {
18+
super.init(nibName: nil, bundle: nil)
19+
self.form = self.formDescriptor()
20+
}
21+
22+
required public init!(coder aDecoder: NSCoder!) {
23+
fatalError("init(coder:) has not been implemented")
24+
}
25+
26+
func formDescriptor() -> XLFormDescriptor {
27+
let form = XLFormDescriptor(title: STORAGE_USAGE_TITLE())
28+
29+
let firstSection = XLFormSectionDescriptor()
30+
firstSection.multivaluedTag = ROOT_SECTION_TAG
31+
form.addFormSection(firstSection)
32+
33+
let deleteAll = XLFormRowDescriptor(tag: DELETE_ALL_TAG, rowType: XLFormRowDescriptorTypeButton, title: STORAGE_USAGE_DELETE_ALL_BUTTON())
34+
deleteAll.action.formBlock = { row in
35+
self.deselectFormRow(row)
36+
self.deleteMedia()
37+
}
38+
firstSection.addFormRow(deleteAll)
39+
40+
let noMediaFound = XLFormRowDescriptor(tag: NO_MEDIA_FOUND_TAG, rowType: XLFormRowDescriptorTypeText)
41+
noMediaFound.value = STORAGE_USAGE_NO_MEDIA_FOUND()
42+
noMediaFound.disabled = true
43+
firstSection.addFormRow(noMediaFound)
44+
45+
return form
46+
}
47+
48+
override func viewDidLoad() {
49+
super.viewDidLoad()
50+
self.tableView.isEditing = false
51+
DispatchQueue.global().async {
52+
self.processAllMedia()
53+
}
54+
}
55+
56+
private func processAllMedia() {
57+
var empty = true
58+
connections?.read.read { (transaction: YapDatabaseReadTransaction) in
59+
transaction.enumerateKeysAndObjects(inCollection: OTRMediaItem.collection, using: { (key, object, stop) in
60+
if let mediaItem = object as? OTRMediaItem {
61+
let parentMessage = mediaItem.parentMessage(with: transaction) as? OTRBaseMessage
62+
63+
if let threadOwner = parentMessage?.threadOwner(with: transaction),
64+
let account = threadOwner.account(with: transaction) {
65+
do {
66+
let length = try OTRMediaFileManager.shared.dataLength(for: mediaItem, buddyUniqueId: threadOwner.threadIdentifier)
67+
empty = false
68+
69+
let section = sectionForAccount(account)
70+
let row = rowForThreadOwner(threadOwner, section)
71+
let value = row.value as? Int ?? 0
72+
row.value = value + length.intValue
73+
74+
DispatchQueue.main.async {
75+
self.updateFormRow(row)
76+
}
77+
} catch {
78+
return
79+
}
80+
}
81+
}
82+
})
83+
}
84+
if let deleteAll = self.form.formRow(withTag: DELETE_ALL_TAG),
85+
let noMediaFound = self.form.formRow(withTag: NO_MEDIA_FOUND_TAG){
86+
DispatchQueue.main.async {
87+
deleteAll.hidden = empty
88+
noMediaFound.hidden = !empty
89+
}
90+
}
91+
}
92+
93+
private func rowForThreadOwner(_ threadOwner: OTRThreadOwner, _ section: XLFormSectionDescriptor) -> XLFormRowDescriptor {
94+
var row = self.form.formRow(withTag: threadOwner.threadIdentifier)
95+
if row == nil {
96+
row = XLFormRowDescriptor(tag: threadOwner.threadIdentifier, rowType: XLFormRowDescriptorTypeInfo, title: threadOwner.threadName)
97+
row?.valueFormatter = ByteCountFormatter()
98+
DispatchQueue.main.sync {
99+
section.addFormRow(row!)
100+
}
101+
}
102+
return row!
103+
}
104+
105+
private func sectionForAccount(_ account: OTRAccount) -> XLFormSectionDescriptor {
106+
var section = (self.form.formSections as! [XLFormSectionDescriptor]).first { $0.title == account.displayName }
107+
if section == nil {
108+
section = XLFormSectionDescriptor.formSection(withTitle: account.displayName, sectionOptions: .canDelete)
109+
DispatchQueue.main.sync {
110+
self.form.addFormSection(section!)
111+
}
112+
}
113+
return section!
114+
}
115+
116+
override func formRowHasBeenRemoved(_ formRow: XLFormRowDescriptor!, at indexPath: IndexPath!) {
117+
super.formRowHasBeenRemoved(formRow, at: indexPath)
118+
deleteMedia(formRow.tag)
119+
}
120+
121+
private func deleteMedia(_ threadIdentifier: String? = nil) {
122+
DispatchQueue.main.async { [weak self] in
123+
guard let strongSelf = self else { return }
124+
MBProgressHUD.showAdded(to: strongSelf.view, animated: true)
125+
}
126+
connections?.write.asyncReadWrite { [weak self] (transaction: YapDatabaseReadWriteTransaction) in
127+
guard let strongSelf = self else { return }
128+
let mediaItemsToDelete = strongSelf.findItemsToDelete(transaction, threadIdentifier)
129+
strongSelf.doDelete(transaction, mediaItemsToDelete)
130+
DispatchQueue.global().async {
131+
strongSelf.vacuumAndUpdateUI(deleteAll: threadIdentifier == nil)
132+
}
133+
}
134+
}
135+
136+
private func findItemsToDelete(_ transaction: YapDatabaseReadWriteTransaction, _ threadIdentifier: String?) -> [OTRMediaItem] {
137+
var mediaItemsToDelete: [OTRMediaItem] = []
138+
transaction.enumerateKeysAndObjects(inCollection: OTRMediaItem.collection, using: { (key, object, stop) in
139+
if let mediaItem = object as? OTRMediaItem {
140+
let parentMessage = mediaItem.parentMessage(with: transaction) as? OTRBaseMessage
141+
if threadIdentifier != nil,
142+
let threadOwner = parentMessage?.threadOwner(with: transaction),
143+
threadOwner.threadIdentifier != threadIdentifier {
144+
return
145+
}
146+
if (parentMessage?.mediaItemUniqueId != nil) {
147+
mediaItemsToDelete.append(mediaItem)
148+
}
149+
}
150+
})
151+
return mediaItemsToDelete
152+
}
153+
154+
private func doDelete(_ transaction: YapDatabaseReadWriteTransaction, _ mediaItemsToDelete: [OTRMediaItem]) {
155+
mediaItemsToDelete.forEach { mediaItem in
156+
if let parentMessage = mediaItem.parentMessage(with: transaction) as? OTRBaseMessage,
157+
let threadOwner = parentMessage.threadOwner(with: transaction) {
158+
159+
mediaItem.remove(with: transaction)
160+
161+
let media = OTRMediaItem.incomingItem(withFilename: mediaItem.filename, mimeType: nil)
162+
media.parentObjectKey = parentMessage.uniqueId
163+
media.parentObjectCollection = parentMessage.messageCollection
164+
media.save(with: transaction)
165+
166+
parentMessage.mediaItemUniqueId = media.uniqueId
167+
parentMessage.messageError = FileTransferError.automaticDownloadsDisabled
168+
parentMessage.save(with: transaction)
169+
170+
OTRMediaFileManager.shared.deleteData(for: mediaItem,
171+
buddyUniqueId: threadOwner.threadIdentifier, completion: nil, completionQueue: nil)
172+
}
173+
}
174+
}
175+
176+
private func vacuumAndUpdateUI(deleteAll: Bool) {
177+
OTRMediaFileManager.shared.vacuum { [weak self] in
178+
guard let strongSelf = self else { return }
179+
if deleteAll {
180+
strongSelf.form.formSections.forEach {
181+
let element = $0 as! XLFormSectionDescriptor
182+
if element.multivaluedTag != strongSelf.ROOT_SECTION_TAG {
183+
strongSelf.form.removeFormSection(element)
184+
}
185+
}
186+
if let deleteAll = strongSelf.form.formRow(withTag: strongSelf.DELETE_ALL_TAG),
187+
let noMediaFound = strongSelf.form.formRow(withTag: strongSelf.NO_MEDIA_FOUND_TAG){
188+
deleteAll.hidden = true
189+
noMediaFound.hidden = false
190+
}
191+
}
192+
MBProgressHUD.hide(for: strongSelf.view, animated: true)
193+
}
194+
}
195+
}

ChatSecureCore/Public/OTRMediaFileManager.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ completionQueue:(nullable dispatch_queue_t)completionQueue;
3333
//#865
3434
- (void)deleteDataForItem:(OTRMediaItem *)mediaItem
3535
buddyUniqueId:(NSString *)buddyUniqueId
36-
completion:(void (^)(BOOL success, NSError * _Nullable error))completion
36+
completion:(nullable void (^)(BOOL success, NSError * _Nullable error))completion
3737
completionQueue:(nullable dispatch_queue_t)completionQueue;
3838

3939
- (nullable NSData*)dataForItem:(OTRMediaItem *)mediaItem
@@ -46,6 +46,8 @@ completionQueue:(nullable dispatch_queue_t)completionQueue;
4646
+ (nullable NSString *)pathForMediaItem:(OTRMediaItem *)mediaItem buddyUniqueId:(NSString *)buddyUniqueId;
4747
+ (nullable NSString *)pathForMediaItem:(OTRMediaItem *)mediaItem buddyUniqueId:(NSString *)buddyUniqueId withLeadingSlash:(BOOL)includeLeadingSlash;
4848

49+
- (void)vacuum:(dispatch_block_t)completion;
50+
4951
@property (class, nonatomic, readonly) OTRMediaFileManager *shared;
5052

5153
+ (instancetype)sharedInstance;

OTRAssets/Strings/OTRStrings.h

+8
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,14 @@ FOUNDATION_EXPORT NSString* SOMEONE_IS_TYPING_STRING();
476476
FOUNDATION_EXPORT NSString* SOMEONE_STRING();
477477
/** "Check out the source here on Github", let users know source is on Github */
478478
FOUNDATION_EXPORT NSString* SOURCE_STRING();
479+
/** "Delete All", Delete all media button */
480+
FOUNDATION_EXPORT NSString* STORAGE_USAGE_DELETE_ALL_BUTTON();
481+
/** "Manage downloaded media", Storage usage setting description */
482+
FOUNDATION_EXPORT NSString* STORAGE_USAGE_DESCRIPTION();
483+
/** "No downloaded media found in chats", No media found clarification */
484+
FOUNDATION_EXPORT NSString* STORAGE_USAGE_NO_MEDIA_FOUND();
485+
/** "Storage Usage", Storage usage setting title */
486+
FOUNDATION_EXPORT NSString* STORAGE_USAGE_TITLE();
479487
/** "Server", server selection section title */
480488
FOUNDATION_EXPORT NSString* Server_String();
481489
/** "Choose from a selection of public servers, or use your own.", server selection footer */

OTRAssets/Strings/OTRStrings.m

+8
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,14 @@
476476
NSString* SOMEONE_STRING() { return [OTRLanguageManager translatedString:@"Someone"]; }
477477
/** "Check out the source here on Github", let users know source is on Github */
478478
NSString* SOURCE_STRING() { return [OTRLanguageManager translatedString:@"Check out the source here on Github"]; }
479+
/** "Delete All", Delete all media button */
480+
NSString* STORAGE_USAGE_DELETE_ALL_BUTTON() { return [OTRLanguageManager translatedString:@"Delete All"]; }
481+
/** "Manage downloaded media", Storage usage setting description */
482+
NSString* STORAGE_USAGE_DESCRIPTION() { return [OTRLanguageManager translatedString:@"Manage downloaded media"]; }
483+
/** "No downloaded media found in chats", No media found clarification */
484+
NSString* STORAGE_USAGE_NO_MEDIA_FOUND() { return [OTRLanguageManager translatedString:@"No downloaded media found in chats"]; }
485+
/** "Storage Usage", Storage usage setting title */
486+
NSString* STORAGE_USAGE_TITLE() { return [OTRLanguageManager translatedString:@"Storage Usage"]; }
479487
/** "Server", server selection section title */
480488
NSString* Server_String() { return [OTRLanguageManager translatedString:@"Server"]; }
481489
/** "Choose from a selection of public servers, or use your own.", server selection footer */

OTRAssets/Strings/strings.json

+12
Original file line numberDiff line numberDiff line change
@@ -1245,5 +1245,17 @@
12451245
}, "ADD_FRIEND_TO_AUTO_DOWNLOAD": {
12461246
"comment": "Shown in chat view to prompt user to add friend for auto-download of group media messages.",
12471247
"string": "Is %@ your friend? Add him/her to auto-download pictures in the future."
1248+
}, "STORAGE_USAGE_TITLE": {
1249+
"comment": "Storage usage setting title",
1250+
"string": "Storage Usage"
1251+
}, "STORAGE_USAGE_DESCRIPTION": {
1252+
"comment": "Storage usage setting description",
1253+
"string": "Manage downloaded media"
1254+
}, "STORAGE_USAGE_DELETE_ALL_BUTTON": {
1255+
"comment": "Delete all media button",
1256+
"string": "Delete All"
1257+
}, "STORAGE_USAGE_NO_MEDIA_FOUND": {
1258+
"comment": "No media found clarification",
1259+
"string": "No downloaded media found in chats"
12481260
}
12491261
}
Binary file not shown.

Submodules/libsqlfs

0 commit comments

Comments
 (0)