Add a possibility to delete downloaded media to free up storage
This commit is contained in:
parent
03b6a74cda
commit
930d31161f
|
@ -33,6 +33,8 @@
|
|||
63F0CAFB1E60C1B40045359C /* OTRYapViewTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F0CAFA1E60C1B40045359C /* OTRYapViewTest.swift */; };
|
||||
63F614DC1BB214660083A06A /* ChatSecureModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63F614DB1BB214660083A06A /* ChatSecureModelTest.swift */; };
|
||||
7CD871CB705CA365E0755104 /* libPods-ChatSecureCorePods-ChatSecureTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5179DA87B83F57EEA9589733 /* libPods-ChatSecureCorePods-ChatSecureTests.a */; };
|
||||
8F56C60D22313225DC3E3E4E /* OTRStorageUsageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F56C0C0D2783FF60F293884 /* OTRStorageUsageViewController.swift */; };
|
||||
8F56CEB16F4C0412C383BCF8 /* OTRStorageUsageSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F56CDC5F0CD5BA670188689 /* OTRStorageUsageSetting.swift */; };
|
||||
D9108AA023F9ABDF00B1280D /* AESGCMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9108A9F23F9ABDF00B1280D /* AESGCMTests.swift */; };
|
||||
D91F9EFE1ED645F100AEA62C /* FileTransferIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D91F9EFD1ED645F100AEA62C /* FileTransferIntegrationTests.swift */; };
|
||||
D9365E7A1A1EB0050006434A /* torrc in Resources */ = {isa = PBXBuildFile; fileRef = D9365E791A1EB0050006434A /* torrc */; };
|
||||
|
@ -635,7 +637,9 @@
|
|||
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>"; };
|
||||
83C35A70D105953D80691D31 /* libPods-ChatSecureCorePods-ChatSecureCore.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ChatSecureCorePods-ChatSecureCore.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
8F56C0C0D2783FF60F293884 /* OTRStorageUsageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTRStorageUsageViewController.swift; sourceTree = "<group>"; };
|
||||
8F56C50436DA64774EBB16E3 /* OTRMessagesLoadingView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OTRMessagesLoadingView.xib; sourceTree = "<group>"; };
|
||||
8F56CDC5F0CD5BA670188689 /* OTRStorageUsageSetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OTRStorageUsageSetting.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
9224A5F2207E3BD800A044BF /* JoinRoomView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = JoinRoomView.xib; path = Interface/JoinRoomView.xib; sourceTree = "<group>"; };
|
||||
924F67C41EA5541C00528FB6 /* MigrationInfoHeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MigrationInfoHeaderView.xib; path = Interface/MigrationInfoHeaderView.xib; sourceTree = "<group>"; };
|
||||
|
@ -1431,6 +1435,7 @@
|
|||
D9C52842235CB580002B213A /* OTRCertificateDomainViewController.h */,
|
||||
D9C52843235CB580002B213A /* OTRComposeViewController.m */,
|
||||
D9C52844235CB580002B213A /* OTRLogListViewController.swift */,
|
||||
8F56C0C0D2783FF60F293884 /* OTRStorageUsageViewController.swift */,
|
||||
);
|
||||
path = "View Controllers";
|
||||
sourceTree = "<group>";
|
||||
|
@ -1538,6 +1543,7 @@
|
|||
D9C5286C235CB580002B213A /* OTRCertificateSetting.h */,
|
||||
D9C5286D235CB580002B213A /* OTRListSettingValue.m */,
|
||||
D9C5286E235CB580002B213A /* OTRFeedbackSetting.m */,
|
||||
8F56CDC5F0CD5BA670188689 /* OTRStorageUsageSetting.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2854,6 +2860,8 @@
|
|||
D9C52A17235CB580002B213A /* PushMessage.swift in Sources */,
|
||||
D9C529D5235CB580002B213A /* OTRSettingsViewController.m in Sources */,
|
||||
D9C52A1F235CB581002B213A /* BuddySubscriptions.swift in Sources */,
|
||||
8F56C60D22313225DC3E3E4E /* OTRStorageUsageViewController.swift in Sources */,
|
||||
8F56CEB16F4C0412C383BCF8 /* OTRStorageUsageSetting.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -134,7 +134,7 @@ completionQueue:(nullable dispatch_queue_t)completionQueue {
|
|||
//#865
|
||||
- (void)deleteDataForItem:(OTRMediaItem *)mediaItem
|
||||
buddyUniqueId:(NSString *)buddyUniqueId
|
||||
completion:(void (^)(BOOL success, NSError * _Nullable error))completion
|
||||
completion:(nullable void (^)(BOOL success, NSError * _Nullable error))completion
|
||||
completionQueue:(nullable dispatch_queue_t)completionQueue {
|
||||
if (!completionQueue) {
|
||||
completionQueue = dispatch_get_main_queue();
|
||||
|
@ -284,4 +284,12 @@ completionQueue:(nullable dispatch_queue_t)completionQueue {
|
|||
}
|
||||
}
|
||||
|
||||
- (void)vacuum:(dispatch_block_t)completion {
|
||||
[self performAsyncWrite:^{
|
||||
[self.ioCipher vacuum];
|
||||
if (completion != nil) {
|
||||
dispatch_async(dispatch_get_main_queue(), completion);
|
||||
}
|
||||
}];
|
||||
}
|
||||
@end
|
||||
|
|
|
@ -108,8 +108,11 @@
|
|||
[settingsGroups addObject:pushGroup];
|
||||
}
|
||||
|
||||
|
||||
NSArray *chatSettings = @[deletedDisconnectedConversations];
|
||||
OTRStorageUsageSetting *storageUsageSetting = [[OTRStorageUsageSetting alloc] initWithTitle:STORAGE_USAGE_TITLE()
|
||||
description:STORAGE_USAGE_DESCRIPTION()];
|
||||
storageUsageSetting.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
|
||||
NSArray *chatSettings = @[deletedDisconnectedConversations, storageUsageSetting];
|
||||
OTRSettingsGroup *chatSettingsGroup = [[OTRSettingsGroup alloc] initWithTitle:CHAT_STRING() settings:chatSettings];
|
||||
[settingsGroups addObject:chatSettingsGroup];
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// Created by Vyacheslav Karpukhin on 20.02.20.
|
||||
// Copyright (c) 2020 Chris Ballinger. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
open class OTRStorageUsageSetting : OTRViewSetting {
|
||||
public override init!(title newTitle: String!, description newDescription: String!) {
|
||||
super.init(title: newTitle, description: newDescription, viewControllerClass: OTRStorageUsageViewController.self)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
//
|
||||
// Created by Vyacheslav Karpukhin on 19.02.20.
|
||||
// Copyright (c) 2020 Chris Ballinger. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import OTRAssets
|
||||
import MBProgressHUD
|
||||
|
||||
class OTRStorageUsageViewController : XLFormViewController {
|
||||
private let ROOT_SECTION_TAG = "rootSection"
|
||||
private let DELETE_ALL_TAG = "deleteAll"
|
||||
private let NO_MEDIA_FOUND_TAG = "noMediaFound"
|
||||
|
||||
private let connections = OTRDatabaseManager.shared.connections
|
||||
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.form = self.formDescriptor()
|
||||
}
|
||||
|
||||
required public init!(coder aDecoder: NSCoder!) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func formDescriptor() -> XLFormDescriptor {
|
||||
let form = XLFormDescriptor(title: STORAGE_USAGE_TITLE())
|
||||
|
||||
let firstSection = XLFormSectionDescriptor()
|
||||
firstSection.multivaluedTag = ROOT_SECTION_TAG
|
||||
form.addFormSection(firstSection)
|
||||
|
||||
let deleteAll = XLFormRowDescriptor(tag: DELETE_ALL_TAG, rowType: XLFormRowDescriptorTypeButton, title: STORAGE_USAGE_DELETE_ALL_BUTTON())
|
||||
deleteAll.action.formBlock = { row in
|
||||
self.deselectFormRow(row)
|
||||
self.deleteMedia()
|
||||
}
|
||||
firstSection.addFormRow(deleteAll)
|
||||
|
||||
let noMediaFound = XLFormRowDescriptor(tag: NO_MEDIA_FOUND_TAG, rowType: XLFormRowDescriptorTypeText)
|
||||
noMediaFound.value = STORAGE_USAGE_NO_MEDIA_FOUND()
|
||||
noMediaFound.disabled = true
|
||||
firstSection.addFormRow(noMediaFound)
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.tableView.isEditing = false
|
||||
DispatchQueue.global().async {
|
||||
self.processAllMedia()
|
||||
}
|
||||
}
|
||||
|
||||
private func processAllMedia() {
|
||||
var empty = true
|
||||
connections?.read.read { (transaction: YapDatabaseReadTransaction) in
|
||||
transaction.enumerateKeysAndObjects(inCollection: OTRMediaItem.collection, using: { (key, object, stop) in
|
||||
if let mediaItem = object as? OTRMediaItem {
|
||||
let parentMessage = mediaItem.parentMessage(with: transaction) as? OTRBaseMessage
|
||||
|
||||
if let threadOwner = parentMessage?.threadOwner(with: transaction),
|
||||
let account = threadOwner.account(with: transaction) {
|
||||
do {
|
||||
let length = try OTRMediaFileManager.shared.dataLength(for: mediaItem, buddyUniqueId: threadOwner.threadIdentifier)
|
||||
empty = false
|
||||
|
||||
let section = sectionForAccount(account)
|
||||
let row = rowForThreadOwner(threadOwner, section)
|
||||
let value = row.value as? Int ?? 0
|
||||
row.value = value + length.intValue
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.updateFormRow(row)
|
||||
}
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if let deleteAll = self.form.formRow(withTag: DELETE_ALL_TAG),
|
||||
let noMediaFound = self.form.formRow(withTag: NO_MEDIA_FOUND_TAG){
|
||||
DispatchQueue.main.async {
|
||||
deleteAll.hidden = empty
|
||||
noMediaFound.hidden = !empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rowForThreadOwner(_ threadOwner: OTRThreadOwner, _ section: XLFormSectionDescriptor) -> XLFormRowDescriptor {
|
||||
var row = self.form.formRow(withTag: threadOwner.threadIdentifier)
|
||||
if row == nil {
|
||||
row = XLFormRowDescriptor(tag: threadOwner.threadIdentifier, rowType: XLFormRowDescriptorTypeInfo, title: threadOwner.threadName)
|
||||
row?.valueFormatter = ByteCountFormatter()
|
||||
DispatchQueue.main.sync {
|
||||
section.addFormRow(row!)
|
||||
}
|
||||
}
|
||||
return row!
|
||||
}
|
||||
|
||||
private func sectionForAccount(_ account: OTRAccount) -> XLFormSectionDescriptor {
|
||||
var section = (self.form.formSections as! [XLFormSectionDescriptor]).first { $0.title == account.displayName }
|
||||
if section == nil {
|
||||
section = XLFormSectionDescriptor.formSection(withTitle: account.displayName, sectionOptions: .canDelete)
|
||||
DispatchQueue.main.sync {
|
||||
self.form.addFormSection(section!)
|
||||
}
|
||||
}
|
||||
return section!
|
||||
}
|
||||
|
||||
override func formRowHasBeenRemoved(_ formRow: XLFormRowDescriptor!, at indexPath: IndexPath!) {
|
||||
super.formRowHasBeenRemoved(formRow, at: indexPath)
|
||||
deleteMedia(formRow.tag)
|
||||
}
|
||||
|
||||
private func deleteMedia(_ threadIdentifier: String? = nil) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
MBProgressHUD.showAdded(to: strongSelf.view, animated: true)
|
||||
}
|
||||
connections?.write.asyncReadWrite { [weak self] (transaction: YapDatabaseReadWriteTransaction) in
|
||||
guard let strongSelf = self else { return }
|
||||
let mediaItemsToDelete = strongSelf.findItemsToDelete(transaction, threadIdentifier)
|
||||
strongSelf.doDelete(transaction, mediaItemsToDelete)
|
||||
DispatchQueue.global().async {
|
||||
strongSelf.vacuumAndUpdateUI(deleteAll: threadIdentifier == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func findItemsToDelete(_ transaction: YapDatabaseReadWriteTransaction, _ threadIdentifier: String?) -> [OTRMediaItem] {
|
||||
var mediaItemsToDelete: [OTRMediaItem] = []
|
||||
transaction.enumerateKeysAndObjects(inCollection: OTRMediaItem.collection, using: { (key, object, stop) in
|
||||
if let mediaItem = object as? OTRMediaItem {
|
||||
let parentMessage = mediaItem.parentMessage(with: transaction) as? OTRBaseMessage
|
||||
if threadIdentifier != nil,
|
||||
let threadOwner = parentMessage?.threadOwner(with: transaction),
|
||||
threadOwner.threadIdentifier != threadIdentifier {
|
||||
return
|
||||
}
|
||||
if (parentMessage?.mediaItemUniqueId != nil) {
|
||||
mediaItemsToDelete.append(mediaItem)
|
||||
}
|
||||
}
|
||||
})
|
||||
return mediaItemsToDelete
|
||||
}
|
||||
|
||||
private func doDelete(_ transaction: YapDatabaseReadWriteTransaction, _ mediaItemsToDelete: [OTRMediaItem]) {
|
||||
mediaItemsToDelete.forEach { mediaItem in
|
||||
if let parentMessage = mediaItem.parentMessage(with: transaction) as? OTRBaseMessage,
|
||||
let threadOwner = parentMessage.threadOwner(with: transaction) {
|
||||
|
||||
mediaItem.remove(with: transaction)
|
||||
|
||||
let media = OTRMediaItem.incomingItem(withFilename: mediaItem.filename, mimeType: nil)
|
||||
media.parentObjectKey = parentMessage.uniqueId
|
||||
media.parentObjectCollection = parentMessage.messageCollection
|
||||
media.save(with: transaction)
|
||||
|
||||
parentMessage.mediaItemUniqueId = media.uniqueId
|
||||
parentMessage.messageError = FileTransferError.automaticDownloadsDisabled
|
||||
parentMessage.save(with: transaction)
|
||||
|
||||
OTRMediaFileManager.shared.deleteData(for: mediaItem,
|
||||
buddyUniqueId: threadOwner.threadIdentifier, completion: nil, completionQueue: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func vacuumAndUpdateUI(deleteAll: Bool) {
|
||||
OTRMediaFileManager.shared.vacuum { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
if deleteAll {
|
||||
strongSelf.form.formSections.forEach {
|
||||
let element = $0 as! XLFormSectionDescriptor
|
||||
if element.multivaluedTag != strongSelf.ROOT_SECTION_TAG {
|
||||
strongSelf.form.removeFormSection(element)
|
||||
}
|
||||
}
|
||||
if let deleteAll = strongSelf.form.formRow(withTag: strongSelf.DELETE_ALL_TAG),
|
||||
let noMediaFound = strongSelf.form.formRow(withTag: strongSelf.NO_MEDIA_FOUND_TAG){
|
||||
deleteAll.hidden = true
|
||||
noMediaFound.hidden = false
|
||||
}
|
||||
}
|
||||
MBProgressHUD.hide(for: strongSelf.view, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ completionQueue:(nullable dispatch_queue_t)completionQueue;
|
|||
//#865
|
||||
- (void)deleteDataForItem:(OTRMediaItem *)mediaItem
|
||||
buddyUniqueId:(NSString *)buddyUniqueId
|
||||
completion:(void (^)(BOOL success, NSError * _Nullable error))completion
|
||||
completion:(nullable void (^)(BOOL success, NSError * _Nullable error))completion
|
||||
completionQueue:(nullable dispatch_queue_t)completionQueue;
|
||||
|
||||
- (nullable NSData*)dataForItem:(OTRMediaItem *)mediaItem
|
||||
|
@ -46,6 +46,8 @@ completionQueue:(nullable dispatch_queue_t)completionQueue;
|
|||
+ (nullable NSString *)pathForMediaItem:(OTRMediaItem *)mediaItem buddyUniqueId:(NSString *)buddyUniqueId;
|
||||
+ (nullable NSString *)pathForMediaItem:(OTRMediaItem *)mediaItem buddyUniqueId:(NSString *)buddyUniqueId withLeadingSlash:(BOOL)includeLeadingSlash;
|
||||
|
||||
- (void)vacuum:(dispatch_block_t)completion;
|
||||
|
||||
@property (class, nonatomic, readonly) OTRMediaFileManager *shared;
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
|
|
@ -476,6 +476,14 @@ FOUNDATION_EXPORT NSString* SOMEONE_IS_TYPING_STRING();
|
|||
FOUNDATION_EXPORT NSString* SOMEONE_STRING();
|
||||
/** "Check out the source here on Github", let users know source is on Github */
|
||||
FOUNDATION_EXPORT NSString* SOURCE_STRING();
|
||||
/** "Delete All", Delete all media button */
|
||||
FOUNDATION_EXPORT NSString* STORAGE_USAGE_DELETE_ALL_BUTTON();
|
||||
/** "Manage downloaded media", Storage usage setting description */
|
||||
FOUNDATION_EXPORT NSString* STORAGE_USAGE_DESCRIPTION();
|
||||
/** "No downloaded media found in chats", No media found clarification */
|
||||
FOUNDATION_EXPORT NSString* STORAGE_USAGE_NO_MEDIA_FOUND();
|
||||
/** "Storage Usage", Storage usage setting title */
|
||||
FOUNDATION_EXPORT NSString* STORAGE_USAGE_TITLE();
|
||||
/** "Server", server selection section title */
|
||||
FOUNDATION_EXPORT NSString* Server_String();
|
||||
/** "Choose from a selection of public servers, or use your own.", server selection footer */
|
||||
|
|
|
@ -476,6 +476,14 @@ NSString* SOMEONE_IS_TYPING_STRING() { return [OTRLanguageManager translatedStri
|
|||
NSString* SOMEONE_STRING() { return [OTRLanguageManager translatedString:@"Someone"]; }
|
||||
/** "Check out the source here on Github", let users know source is on Github */
|
||||
NSString* SOURCE_STRING() { return [OTRLanguageManager translatedString:@"Check out the source here on Github"]; }
|
||||
/** "Delete All", Delete all media button */
|
||||
NSString* STORAGE_USAGE_DELETE_ALL_BUTTON() { return [OTRLanguageManager translatedString:@"Delete All"]; }
|
||||
/** "Manage downloaded media", Storage usage setting description */
|
||||
NSString* STORAGE_USAGE_DESCRIPTION() { return [OTRLanguageManager translatedString:@"Manage downloaded media"]; }
|
||||
/** "No downloaded media found in chats", No media found clarification */
|
||||
NSString* STORAGE_USAGE_NO_MEDIA_FOUND() { return [OTRLanguageManager translatedString:@"No downloaded media found in chats"]; }
|
||||
/** "Storage Usage", Storage usage setting title */
|
||||
NSString* STORAGE_USAGE_TITLE() { return [OTRLanguageManager translatedString:@"Storage Usage"]; }
|
||||
/** "Server", server selection section title */
|
||||
NSString* Server_String() { return [OTRLanguageManager translatedString:@"Server"]; }
|
||||
/** "Choose from a selection of public servers, or use your own.", server selection footer */
|
||||
|
|
|
@ -1245,5 +1245,17 @@
|
|||
}, "ADD_FRIEND_TO_AUTO_DOWNLOAD": {
|
||||
"comment": "Shown in chat view to prompt user to add friend for auto-download of group media messages.",
|
||||
"string": "Is %@ your friend? Add him/her to auto-download pictures in the future."
|
||||
}, "STORAGE_USAGE_TITLE": {
|
||||
"comment": "Storage usage setting title",
|
||||
"string": "Storage Usage"
|
||||
}, "STORAGE_USAGE_DESCRIPTION": {
|
||||
"comment": "Storage usage setting description",
|
||||
"string": "Manage downloaded media"
|
||||
}, "STORAGE_USAGE_DELETE_ALL_BUTTON": {
|
||||
"comment": "Delete all media button",
|
||||
"string": "Delete All"
|
||||
}, "STORAGE_USAGE_NO_MEDIA_FOUND": {
|
||||
"comment": "No media found clarification",
|
||||
"string": "No downloaded media found in chats"
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit 7a68be1dbed27fc39b8f5888d95d8a2e0cb4f16d
|
||||
Subproject commit 89aee7f8c7f0e23a484d17e70cd8ea2836d22edd
|
|
@ -1 +1 @@
|
|||
Subproject commit 4456f8a83c7af7dc6b8cf6456dae94fe7979c24c
|
||||
Subproject commit 973f8a61a5486064f33ebd2bd7fa59c142d3bf02
|
Loading…
Reference in New Issue