mirror of https://github.com/oxen-io/session-ios
Merge branch 'dev' into feature/database-refactor
# Conflicts: # Session.xcodeproj/project.pbxproj # Session/Meta/AppDelegate.m # SessionMessagingKit/Utilities/OWSIdentityManager.h # SessionMessagingKit/Utilities/OWSIdentityManager.m # SignalUtilitiesKit/Database/Storage+Conformances.swift # SignalUtilitiesKit/Database/TSStorageHeaders.h # SignalUtilitiesKit/To Do/OWSPrimaryStorage+Loki.mpull/612/head
commit
0f4df804ed
@ -1,23 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Notes:
|
||||
//
|
||||
// * On disk, we only bother cleaning up files, not directories.
|
||||
@interface OWSOrphanDataCleaner : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
// This is exposed for the debug UI.
|
||||
+ (void)auditAndCleanup:(BOOL)shouldCleanup;
|
||||
// This is exposed for the tests.
|
||||
+ (void)auditAndCleanup:(BOOL)shouldCleanup completion:(dispatch_block_t)completion;
|
||||
|
||||
+ (void)auditOnLaunchIfNecessary;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,737 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSOrphanDataCleaner.h"
|
||||
#import "DateUtil.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SessionMessagingKit/OWSUserProfile.h>
|
||||
#import <SessionMessagingKit/AppReadiness.h>
|
||||
#import <SignalUtilitiesKit/AppVersion.h>
|
||||
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
#import <SessionMessagingKit/TSInteraction.h>
|
||||
#import <SessionMessagingKit/TSMessage.h>
|
||||
#import <SessionMessagingKit/TSQuotedMessage.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionMessagingKit/YapDatabaseTransaction+OWS.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// LOG_ALL_FILE_PATHS can be used to determine if there are other kinds of files
|
||||
// that we're not cleaning up.
|
||||
//#define LOG_ALL_FILE_PATHS
|
||||
|
||||
#define ENABLE_ORPHAN_DATA_CLEANER
|
||||
|
||||
NSString *const OWSOrphanDataCleaner_Collection = @"OWSOrphanDataCleaner_Collection";
|
||||
NSString *const OWSOrphanDataCleaner_LastCleaningVersionKey = @"OWSOrphanDataCleaner_LastCleaningVersionKey";
|
||||
NSString *const OWSOrphanDataCleaner_LastCleaningDateKey = @"OWSOrphanDataCleaner_LastCleaningDateKey";
|
||||
|
||||
@interface OWSOrphanData : NSObject
|
||||
|
||||
@property (nonatomic) NSSet<NSString *> *interactionIds;
|
||||
@property (nonatomic) NSSet<NSString *> *attachmentIds;
|
||||
@property (nonatomic) NSSet<NSString *> *filePaths;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSOrphanData
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
typedef void (^OrphanDataBlock)(OWSOrphanData *);
|
||||
|
||||
@implementation OWSOrphanDataCleaner
|
||||
|
||||
// Unlike CurrentAppContext().isMainAppAndActive, this method can be safely
|
||||
// invoked off the main thread.
|
||||
+ (BOOL)isMainAppAndActive
|
||||
{
|
||||
return CurrentAppContext().reportedApplicationState == UIApplicationStateActive;
|
||||
}
|
||||
|
||||
+ (void)printPaths:(NSArray<NSString *> *)paths label:(NSString *)label
|
||||
{
|
||||
for (NSString *path in [paths sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
OWSLogDebug(@"%@: %@", label, path);
|
||||
}
|
||||
}
|
||||
|
||||
+ (long long)fileSizeOfFilePath:(NSString *)filePath
|
||||
{
|
||||
NSError *error;
|
||||
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize];
|
||||
if (error) {
|
||||
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == 260) {
|
||||
OWSLogWarn(@"can't find size of missing file.");
|
||||
OWSLogDebug(@"can't find size of missing file: %@", filePath);
|
||||
} else {
|
||||
OWSFailDebug(@"attributesOfItemAtPath: %@ error: %@", filePath, error);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return fileSize.longLongValue;
|
||||
}
|
||||
|
||||
+ (nullable NSNumber *)fileSizeOfFilePathsSafe:(NSArray<NSString *> *)filePaths
|
||||
{
|
||||
long long result = 0;
|
||||
for (NSString *filePath in filePaths) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
result += [self fileSizeOfFilePath:filePath];
|
||||
}
|
||||
return @(result);
|
||||
}
|
||||
|
||||
+ (nullable NSSet<NSString *> *)filePathsInDirectorySafe:(NSString *)dirPath
|
||||
{
|
||||
NSMutableSet *filePaths = [NSMutableSet new];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:dirPath]) {
|
||||
return filePaths;
|
||||
}
|
||||
NSError *error;
|
||||
NSArray<NSString *> *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error];
|
||||
if (error) {
|
||||
OWSFailDebug(@"contentsOfDirectoryAtPath error: %@", error);
|
||||
return [NSSet new];
|
||||
}
|
||||
for (NSString *fileName in fileNames) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];
|
||||
BOOL isDirectory;
|
||||
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory];
|
||||
if (isDirectory) {
|
||||
NSSet<NSString *> *_Nullable dirPaths = [self filePathsInDirectorySafe:filePath];
|
||||
if (!dirPaths) {
|
||||
return nil;
|
||||
}
|
||||
[filePaths unionSet:dirPaths];
|
||||
} else {
|
||||
[filePaths addObject:filePath];
|
||||
}
|
||||
}
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// This method finds (but does not delete):
|
||||
//
|
||||
// * Orphan TSInteractions (with no thread).
|
||||
// * Orphan TSAttachments (with no message).
|
||||
// * Orphan attachment files (with no corresponding TSAttachment).
|
||||
// * Orphan profile avatars.
|
||||
// * Temporary files (all).
|
||||
//
|
||||
// It also finds (we don't clean these up).
|
||||
//
|
||||
// * Missing attachment files (cannot be cleaned up).
|
||||
// These are attachments which have no file on disk. They should be extremely rare -
|
||||
// the only cases I have seen are probably due to debugging.
|
||||
// They can't be cleaned up - we don't want to delete the TSAttachmentStream or
|
||||
// its corresponding message. Better that the broken message shows up in the
|
||||
// conversation view.
|
||||
+ (void)findOrphanDataWithRetries:(NSInteger)remainingRetries
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
success:(OrphanDataBlock)success
|
||||
failure:(dispatch_block_t)failure
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
|
||||
if (remainingRetries < 1) {
|
||||
OWSLogInfo(@"Aborting orphan data search.");
|
||||
dispatch_async(self.workQueue, ^{
|
||||
failure();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until the app is active...
|
||||
[CurrentAppContext() runNowOrWhenMainAppIsActive:^{
|
||||
// ...but perform the work off the main thread.
|
||||
dispatch_async(self.workQueue, ^{
|
||||
OWSOrphanData *_Nullable orphanData = [self findOrphanDataSync:databaseConnection];
|
||||
if (orphanData) {
|
||||
success(orphanData);
|
||||
} else {
|
||||
[self findOrphanDataWithRetries:remainingRetries - 1
|
||||
databaseConnection:databaseConnection
|
||||
success:success
|
||||
failure:failure];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns nil on failure, usually indicating that the search
|
||||
// aborted due to the app resigning active. This method is extremely careful to
|
||||
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
|
||||
+ (nullable OWSOrphanData *)findOrphanDataSync:(YapDatabaseConnection *)databaseConnection
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
|
||||
__block BOOL shouldAbort = NO;
|
||||
|
||||
#ifdef LOG_ALL_FILE_PATHS
|
||||
{
|
||||
NSString *documentDirPath = [OWSFileSystem appDocumentDirectoryPath];
|
||||
NSArray<NSString *> *_Nullable allDocumentFilePaths =
|
||||
[self filePathsInDirectorySafe:documentDirPath].allObjects;
|
||||
allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)];
|
||||
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
|
||||
for (NSString *filePath in allDocumentFilePaths) {
|
||||
if ([filePath hasPrefix:attachmentsFolder]) {
|
||||
continue;
|
||||
}
|
||||
OWSLogVerbose(@"non-attachment file: %@", filePath);
|
||||
}
|
||||
}
|
||||
{
|
||||
NSString *documentDirPath = [OWSFileSystem appSharedDataDirectoryPath];
|
||||
NSArray<NSString *> *_Nullable allDocumentFilePaths =
|
||||
[self filePathsInDirectorySafe:documentDirPath].allObjects;
|
||||
allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)];
|
||||
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
|
||||
for (NSString *filePath in allDocumentFilePaths) {
|
||||
if ([filePath hasPrefix:attachmentsFolder]) {
|
||||
continue;
|
||||
}
|
||||
OWSLogVerbose(@"non-attachment file: %@", filePath);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// We treat _all_ temp files as orphan files. This is safe
|
||||
// because temp files only need to be retained for the
|
||||
// a single launch of the app. Since our "date threshold"
|
||||
// for deletion is relative to the current launch time,
|
||||
// all temp files currently in use should be safe.
|
||||
NSArray<NSString *> *_Nullable tempFilePaths = [self getTempFilePaths];
|
||||
if (!tempFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#ifdef LOG_ALL_FILE_PATHS
|
||||
{
|
||||
NSDateFormatter *dateFormatter = [NSDateFormatter new];
|
||||
[dateFormatter setDateStyle:NSDateFormatterLongStyle];
|
||||
[dateFormatter setTimeStyle:NSDateFormatterLongStyle];
|
||||
|
||||
tempFilePaths = [tempFilePaths sortedArrayUsingSelector:@selector(compare:)];
|
||||
for (NSString *filePath in tempFilePaths) {
|
||||
NSError *error;
|
||||
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
|
||||
if (!attributes || error) {
|
||||
OWSLogDebug(@"Could not get attributes of file at: %@", filePath);
|
||||
OWSFailDebug(@"Could not get attributes of file");
|
||||
continue;
|
||||
}
|
||||
OWSLogVerbose(
|
||||
@"temp file: %@, %@", filePath, [dateFormatter stringFromDate:attributes.fileModificationDate]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
NSString *legacyAttachmentsDirPath = TSAttachmentStream.legacyAttachmentsDirPath;
|
||||
NSString *sharedDataAttachmentsDirPath = TSAttachmentStream.sharedDataAttachmentsDirPath;
|
||||
NSSet<NSString *> *_Nullable legacyAttachmentFilePaths = [self filePathsInDirectorySafe:legacyAttachmentsDirPath];
|
||||
if (!legacyAttachmentFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
NSSet<NSString *> *_Nullable sharedDataAttachmentFilePaths =
|
||||
[self filePathsInDirectorySafe:sharedDataAttachmentsDirPath];
|
||||
if (!sharedDataAttachmentFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *legacyProfileAvatarsDirPath = OWSUserProfile.legacyProfileAvatarsDirPath;
|
||||
NSString *sharedDataProfileAvatarsDirPath = OWSUserProfile.sharedDataProfileAvatarsDirPath;
|
||||
NSSet<NSString *> *_Nullable legacyProfileAvatarsFilePaths =
|
||||
[self filePathsInDirectorySafe:legacyProfileAvatarsDirPath];
|
||||
if (!legacyProfileAvatarsFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
NSSet<NSString *> *_Nullable sharedDataProfileAvatarFilePaths =
|
||||
[self filePathsInDirectorySafe:sharedDataProfileAvatarsDirPath];
|
||||
if (!sharedDataProfileAvatarFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableSet<NSString *> *allOnDiskFilePaths = [NSMutableSet new];
|
||||
[allOnDiskFilePaths unionSet:legacyAttachmentFilePaths];
|
||||
[allOnDiskFilePaths unionSet:sharedDataAttachmentFilePaths];
|
||||
[allOnDiskFilePaths unionSet:legacyProfileAvatarsFilePaths];
|
||||
[allOnDiskFilePaths unionSet:sharedDataProfileAvatarFilePaths];
|
||||
[allOnDiskFilePaths addObjectsFromArray:tempFilePaths];
|
||||
|
||||
NSSet<NSString *> *profileAvatarFilePaths = [OWSUserProfile allProfileAvatarFilePaths];
|
||||
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSNumber *_Nullable totalFileSize = [self fileSizeOfFilePathsSafe:allOnDiskFilePaths.allObjects];
|
||||
|
||||
if (!totalFileSize || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSUInteger fileCount = allOnDiskFilePaths.count;
|
||||
|
||||
// Attachments
|
||||
__block int attachmentStreamCount = 0;
|
||||
NSMutableSet<NSString *> *allAttachmentFilePaths = [NSMutableSet new];
|
||||
NSMutableSet<NSString *> *allAttachmentIds = [NSMutableSet new];
|
||||
// Threads
|
||||
__block NSSet *threadIds;
|
||||
// Messages
|
||||
NSMutableSet<NSString *> *orphanInteractionIds = [NSMutableSet new];
|
||||
NSMutableSet<NSString *> *allMessageAttachmentIds = [NSMutableSet new];
|
||||
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
[transaction
|
||||
enumerateKeysAndObjectsInCollection:TSAttachmentStream.collection
|
||||
usingBlock:^(NSString *key, TSAttachment *attachment, BOOL *stop) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
return;
|
||||
}
|
||||
[allAttachmentIds addObject:attachment.uniqueId];
|
||||
|
||||
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
||||
attachmentStreamCount++;
|
||||
NSString *_Nullable filePath = [attachmentStream originalFilePath];
|
||||
if (filePath) {
|
||||
[allAttachmentFilePaths addObject:filePath];
|
||||
} else {
|
||||
OWSFailDebug(@"attachment has no file path.");
|
||||
}
|
||||
|
||||
[allAttachmentFilePaths
|
||||
addObjectsFromArray:attachmentStream.allThumbnailPaths];
|
||||
}];
|
||||
|
||||
if (shouldAbort) {
|
||||
return;
|
||||
}
|
||||
|
||||
threadIds = [NSSet setWithArray:[transaction allKeysInCollection:TSThread.collection]];
|
||||
|
||||
[transaction
|
||||
enumerateKeysAndObjectsInCollection:TSMessage.collection
|
||||
usingBlock:^(NSString *key, TSInteraction *interaction, BOOL *stop) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
if (interaction.uniqueThreadId.length < 1
|
||||
|| ![threadIds containsObject:interaction.uniqueThreadId]) {
|
||||
[orphanInteractionIds addObject:interaction.uniqueId];
|
||||
}
|
||||
|
||||
if (![interaction isKindOfClass:[TSMessage class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
TSMessage *message = (TSMessage *)interaction;
|
||||
[allMessageAttachmentIds addObjectsFromArray:message.allAttachmentIds];
|
||||
}];
|
||||
}];
|
||||
if (shouldAbort) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
OWSLogDebug(@"fileCount: %zu", fileCount);
|
||||
OWSLogDebug(@"totalFileSize: %lld", totalFileSize.longLongValue);
|
||||
OWSLogDebug(@"attachmentStreams: %d", attachmentStreamCount);
|
||||
OWSLogDebug(@"attachmentStreams with file paths: %zu", allAttachmentFilePaths.count);
|
||||
|
||||
NSMutableSet<NSString *> *orphanFilePaths = [allOnDiskFilePaths mutableCopy];
|
||||
[orphanFilePaths minusSet:allAttachmentFilePaths];
|
||||
[orphanFilePaths minusSet:profileAvatarFilePaths];
|
||||
NSMutableSet<NSString *> *missingAttachmentFilePaths = [allAttachmentFilePaths mutableCopy];
|
||||
[missingAttachmentFilePaths minusSet:allOnDiskFilePaths];
|
||||
|
||||
OWSLogDebug(@"orphan file paths: %zu", orphanFilePaths.count);
|
||||
OWSLogDebug(@"missing attachment file paths: %zu", missingAttachmentFilePaths.count);
|
||||
|
||||
[self printPaths:orphanFilePaths.allObjects label:@"orphan file paths"];
|
||||
[self printPaths:missingAttachmentFilePaths.allObjects label:@"missing attachment file paths"];
|
||||
|
||||
OWSLogDebug(@"attachmentIds: %zu", allAttachmentIds.count);
|
||||
OWSLogDebug(@"allMessageAttachmentIds: %zu", allMessageAttachmentIds.count);
|
||||
|
||||
NSMutableSet<NSString *> *orphanAttachmentIds = [allAttachmentIds mutableCopy];
|
||||
[orphanAttachmentIds minusSet:allMessageAttachmentIds];
|
||||
NSMutableSet<NSString *> *missingAttachmentIds = [allMessageAttachmentIds mutableCopy];
|
||||
[missingAttachmentIds minusSet:allAttachmentIds];
|
||||
|
||||
OWSLogDebug(@"orphan attachmentIds: %zu", orphanAttachmentIds.count);
|
||||
OWSLogDebug(@"missing attachmentIds: %zu", missingAttachmentIds.count);
|
||||
OWSLogDebug(@"orphan interactions: %zu", orphanInteractionIds.count);
|
||||
|
||||
OWSOrphanData *result = [OWSOrphanData new];
|
||||
result.interactionIds = [orphanInteractionIds copy];
|
||||
result.attachmentIds = [orphanAttachmentIds copy];
|
||||
result.filePaths = [orphanFilePaths copy];
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (BOOL)shouldAuditOnLaunch:(YapDatabaseConnection *)databaseConnection {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
#ifndef ENABLE_ORPHAN_DATA_CLEANER
|
||||
return NO;
|
||||
#endif
|
||||
|
||||
__block NSString *_Nullable lastCleaningVersion;
|
||||
__block NSDate *_Nullable lastCleaningDate;
|
||||
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
lastCleaningVersion = [transaction stringForKey:OWSOrphanDataCleaner_LastCleaningVersionKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
lastCleaningDate = [transaction dateForKey:OWSOrphanDataCleaner_LastCleaningDateKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
}];
|
||||
|
||||
// Clean up once per app version.
|
||||
NSString *currentAppVersion = AppVersion.sharedInstance.currentAppVersion;
|
||||
if (!lastCleaningVersion || ![lastCleaningVersion isEqualToString:currentAppVersion]) {
|
||||
OWSLogVerbose(@"Performing orphan data cleanup; new version: %@.", currentAppVersion);
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Clean up once per N days.
|
||||
if (lastCleaningDate) {
|
||||
#ifdef DEBUG
|
||||
BOOL shouldAudit = [DateUtil dateIsOlderThanToday:lastCleaningDate];
|
||||
#else
|
||||
BOOL shouldAudit = [DateUtil dateIsOlderThanOneWeek:lastCleaningDate];
|
||||
#endif
|
||||
|
||||
if (shouldAudit) {
|
||||
OWSLogVerbose(@"Performing orphan data cleanup; time has passed.");
|
||||
}
|
||||
return shouldAudit;
|
||||
}
|
||||
|
||||
// Has never audited before.
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (void)auditOnLaunchIfNecessary {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
|
||||
YapDatabaseConnection *databaseConnection = [primaryStorage newDatabaseConnection];
|
||||
|
||||
if (![self shouldAuditOnLaunch:databaseConnection]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we want to be cautious, we can disable orphan deletion using
|
||||
// flag - the cleanup will just be a dry run with logging.
|
||||
BOOL shouldRemoveOrphans = YES;
|
||||
[self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection completion:nil];
|
||||
}
|
||||
|
||||
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans
|
||||
{
|
||||
[self auditAndCleanup:shouldRemoveOrphans
|
||||
completion:^ {
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans completion:(dispatch_block_t)completion
|
||||
{
|
||||
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
|
||||
YapDatabaseConnection *databaseConnection = [primaryStorage newDatabaseConnection];
|
||||
|
||||
[self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection completion:completion];
|
||||
}
|
||||
|
||||
// We use the lowest priority possible.
|
||||
+ (dispatch_queue_t)workQueue
|
||||
{
|
||||
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
|
||||
}
|
||||
|
||||
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
completion:(nullable dispatch_block_t)completion
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(databaseConnection);
|
||||
|
||||
if (!AppReadiness.isAppReady) {
|
||||
OWSFailDebug(@"can't audit orphan data until app is ready.");
|
||||
return;
|
||||
}
|
||||
if (!CurrentAppContext().isMainApp) {
|
||||
OWSFailDebug(@"can't audit orphan data in app extensions.");
|
||||
return;
|
||||
}
|
||||
if (CurrentAppContext().isRunningTests) {
|
||||
OWSLogVerbose(@"Ignoring audit orphan data in tests.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Orphan cleanup has two risks:
|
||||
//
|
||||
// * As a long-running process that involves access to the
|
||||
// shared data container, it could cause 0xdead10cc.
|
||||
// * It could accidentally delete data still in use,
|
||||
// e.g. a profile avatar which has been saved to disk
|
||||
// but whose OWSUserProfile hasn't been saved yet.
|
||||
//
|
||||
// To prevent 0xdead10cc, the cleaner continually checks
|
||||
// whether the app has resigned active. If so, it aborts.
|
||||
// Each phase (search, re-search, processing) retries N times,
|
||||
// then gives up until the next app launch.
|
||||
//
|
||||
// To prevent accidental data deletion, we take the following
|
||||
// measures:
|
||||
//
|
||||
// * Only cleanup data of the following types (which should
|
||||
// include all relevant app data): profile avatar,
|
||||
// attachment, temporary files (including temporary
|
||||
// attachments).
|
||||
// * We don't delete any data created more recently than N seconds
|
||||
// _before_ when the app launched. This prevents any stray data
|
||||
// currently in use by the app from being accidentally cleaned
|
||||
// up.
|
||||
const NSInteger kMaxRetries = 3;
|
||||
[self findOrphanDataWithRetries:kMaxRetries
|
||||
databaseConnection:databaseConnection
|
||||
success:^(OWSOrphanData *orphanData) {
|
||||
[self processOrphans:orphanData
|
||||
remainingRetries:kMaxRetries
|
||||
databaseConnection:databaseConnection
|
||||
shouldRemoveOrphans:shouldRemoveOrphans
|
||||
success:^{
|
||||
OWSLogInfo(@"Completed orphan data cleanup.");
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[transaction setObject:AppVersion.sharedInstance.currentAppVersion
|
||||
forKey:OWSOrphanDataCleaner_LastCleaningVersionKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
[transaction setDate:[NSDate new]
|
||||
forKey:OWSOrphanDataCleaner_LastCleaningDateKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
}];
|
||||
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}
|
||||
failure:^{
|
||||
OWSLogInfo(@"Aborting orphan data cleanup.");
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
}
|
||||
failure:^{
|
||||
OWSLogInfo(@"Aborting orphan data cleanup.");
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns NO on failure, usually indicating that orphan processing
|
||||
// aborted due to the app resigning active. This method is extremely careful to
|
||||
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
|
||||
+ (void)processOrphans:(OWSOrphanData *)orphanData
|
||||
remainingRetries:(NSInteger)remainingRetries
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
shouldRemoveOrphans:(BOOL)shouldRemoveOrphans
|
||||
success:(dispatch_block_t)success
|
||||
failure:(dispatch_block_t)failure
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
OWSAssertDebug(orphanData);
|
||||
|
||||
if (remainingRetries < 1) {
|
||||
OWSLogInfo(@"Aborting orphan data audit.");
|
||||
dispatch_async(self.workQueue, ^{
|
||||
failure();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until the app is active...
|
||||
[CurrentAppContext() runNowOrWhenMainAppIsActive:^{
|
||||
// ...but perform the work off the main thread.
|
||||
dispatch_async(self.workQueue, ^{
|
||||
if ([self processOrphansSync:orphanData
|
||||
databaseConnection:databaseConnection
|
||||
shouldRemoveOrphans:shouldRemoveOrphans]) {
|
||||
success();
|
||||
return;
|
||||
} else {
|
||||
[self processOrphans:orphanData
|
||||
remainingRetries:remainingRetries - 1
|
||||
databaseConnection:databaseConnection
|
||||
shouldRemoveOrphans:shouldRemoveOrphans
|
||||
success:success
|
||||
failure:failure];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns NO on failure, usually indicating that orphan processing
|
||||
// aborted due to the app resigning active. This method is extremely careful to
|
||||
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
|
||||
+ (BOOL)processOrphansSync:(OWSOrphanData *)orphanData
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
shouldRemoveOrphans:(BOOL)shouldRemoveOrphans
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
OWSAssertDebug(orphanData);
|
||||
|
||||
__block BOOL shouldAbort = NO;
|
||||
|
||||
// We need to avoid cleaning up new files that are still in the process of
|
||||
// being created/written, so we don't clean up anything recent.
|
||||
const NSTimeInterval kMinimumOrphanAgeSeconds = CurrentAppContext().isRunningTests ? 0.f : 15 * kMinuteInterval;
|
||||
NSDate *appLaunchTime = CurrentAppContext().appLaunchTime;
|
||||
NSTimeInterval thresholdTimestamp = appLaunchTime.timeIntervalSince1970 - kMinimumOrphanAgeSeconds;
|
||||
NSDate *thresholdDate = [NSDate dateWithTimeIntervalSince1970:thresholdTimestamp];
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSUInteger interactionsRemoved = 0;
|
||||
for (NSString *interactionId in orphanData.interactionIds) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
return;
|
||||
}
|
||||
TSInteraction *_Nullable interaction =
|
||||
[TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction];
|
||||
if (!interaction) {
|
||||
// This could just be a race condition, but it should be very unlikely.
|
||||
OWSLogWarn(@"Could not load interaction: %@", interactionId);
|
||||
continue;
|
||||
}
|
||||
// Don't delete interactions which were created in the last N minutes.
|
||||
NSDate *creationDate = [NSDate ows_dateWithMillisecondsSince1970:interaction.timestamp];
|
||||
if ([creationDate isAfterDate:thresholdDate]) {
|
||||
OWSLogInfo(@"Skipping orphan interaction due to age: %f", fabs(creationDate.timeIntervalSinceNow));
|
||||
continue;
|
||||
}
|
||||
OWSLogInfo(@"Removing orphan message: %@", interaction.uniqueId);
|
||||
interactionsRemoved++;
|
||||
if (!shouldRemoveOrphans) {
|
||||
continue;
|
||||
}
|
||||
[interaction removeWithTransaction:transaction];
|
||||
}
|
||||
OWSLogInfo(@"Deleted orphan interactions: %zu", interactionsRemoved);
|
||||
|
||||
NSUInteger attachmentsRemoved = 0;
|
||||
for (NSString *attachmentId in orphanData.attachmentIds) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
return;
|
||||
}
|
||||
TSAttachment *_Nullable attachment =
|
||||
[TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
|
||||
if (!attachment) {
|
||||
// This can happen on launch since we sync contacts/groups, especially if you have a lot of attachments
|
||||
// to churn through, it's likely it's been deleted since starting this job.
|
||||
OWSLogWarn(@"Could not load attachment: %@", attachmentId);
|
||||
continue;
|
||||
}
|
||||
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
continue;
|
||||
}
|
||||
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
||||
// Don't delete attachments which were created in the last N minutes.
|
||||
NSDate *creationDate = attachmentStream.creationTimestamp;
|
||||
if ([creationDate isAfterDate:thresholdDate]) {
|
||||
OWSLogInfo(@"Skipping orphan attachment due to age: %f", fabs(creationDate.timeIntervalSinceNow));
|
||||
continue;
|
||||
}
|
||||
OWSLogInfo(@"Removing orphan attachmentStream: %@", attachmentStream.uniqueId);
|
||||
attachmentsRemoved++;
|
||||
if (!shouldRemoveOrphans) {
|
||||
continue;
|
||||
}
|
||||
[attachmentStream removeWithTransaction:transaction];
|
||||
}
|
||||
OWSLogInfo(@"Deleted orphan attachments: %zu", attachmentsRemoved);
|
||||
}];
|
||||
|
||||
if (shouldAbort) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSUInteger filesRemoved = 0;
|
||||
NSArray<NSString *> *filePaths = [orphanData.filePaths.allObjects sortedArrayUsingSelector:@selector(compare:)];
|
||||
for (NSString *filePath in filePaths) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
|
||||
if (!attributes || error) {
|
||||
// This is fine; the file may have been deleted since we found it.
|
||||
OWSLogWarn(@"Could not get attributes of file at: %@", filePath);
|
||||
continue;
|
||||
}
|
||||
// Don't delete files which were created in the last N minutes.
|
||||
NSDate *creationDate = attributes.fileModificationDate;
|
||||
if ([creationDate isAfterDate:thresholdDate]) {
|
||||
OWSLogInfo(@"Skipping file due to age: %f", fabs([creationDate timeIntervalSinceNow]));
|
||||
continue;
|
||||
}
|
||||
OWSLogInfo(@"Deleting file: %@", filePath);
|
||||
filesRemoved++;
|
||||
if (!shouldRemoveOrphans) {
|
||||
continue;
|
||||
}
|
||||
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
|
||||
if (error) {
|
||||
OWSLogDebug(@"Could not remove orphan file at: %@", filePath);
|
||||
OWSFailDebug(@"Could not remove orphan file");
|
||||
}
|
||||
}
|
||||
OWSLogInfo(@"Deleted orphan files: %zu", filesRemoved);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (nullable NSArray<NSString *> *)getTempFilePaths
|
||||
{
|
||||
NSString *dir1 = OWSTemporaryDirectory();
|
||||
NSArray<NSString *> *_Nullable paths1 = [[self filePathsInDirectorySafe:dir1].allObjects mutableCopy];
|
||||
|
||||
NSString *dir2 = OWSTemporaryDirectoryAccessibleAfterFirstAuth();
|
||||
NSArray<NSString *> *_Nullable paths2 = [[self filePathsInDirectorySafe:dir2].allObjects mutableCopy];
|
||||
|
||||
if (paths1 && paths2) {
|
||||
return [paths1 arrayByAddingObjectsFromArray:paths2];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,37 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
public class AppPreferences: NSObject {
|
||||
// Never instantiate this class.
|
||||
private override init() {}
|
||||
|
||||
private static let collection = "AppPreferences"
|
||||
|
||||
// MARK: -
|
||||
|
||||
private static let hasDimissedFirstConversationCueKey = "hasDimissedFirstConversationCue"
|
||||
|
||||
@objc
|
||||
public static var hasDimissedFirstConversationCue: Bool {
|
||||
get {
|
||||
return getBool(key: hasDimissedFirstConversationCueKey)
|
||||
}
|
||||
set {
|
||||
setBool(newValue, key: hasDimissedFirstConversationCueKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class func getBool(key: String, defaultValue: Bool = false) -> Bool {
|
||||
return OWSPrimaryStorage.dbReadConnection().bool(forKey: key, inCollection: collection, defaultValue: defaultValue)
|
||||
}
|
||||
|
||||
private class func setBool(_ value: Bool, key: String) {
|
||||
OWSPrimaryStorage.dbReadWriteConnection().setBool(value, forKey: key, inCollection: collection)
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Storage : SessionMessagingKitStorageProtocol {
|
||||
|
||||
public func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set<String>, using transaction: Any) {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
OWSPrimaryStorage.shared().updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, in: transaction)
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Signal_TSStorageHeaders_h
|
||||
#define Signal_TSStorageHeaders_h
|
||||
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+keyFromIntLong.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
|
||||
#endif
|
@ -1,32 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#ifndef TextSecureKit_TSStorageKeys_h
|
||||
#define TextSecureKit_TSStorageKeys_h
|
||||
|
||||
/**
|
||||
* Preferences exposed to the user
|
||||
*/
|
||||
|
||||
#pragma mark User Preferences
|
||||
|
||||
#define TSStorageUserPreferencesCollection @"TSStorageUserPreferencesCollection"
|
||||
|
||||
|
||||
/**
|
||||
* Internal settings of the application, not exposed to the user.
|
||||
*/
|
||||
|
||||
#pragma mark Internal Settings
|
||||
|
||||
#define TSStorageInternalSettingsCollection @"TSStorageInternalSettingsCollection"
|
||||
#define TSStorageInternalSettingsVersion @"TSLastLaunchedVersion"
|
||||
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,13 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSSearchBar : UISearchBar
|
||||
|
||||
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,118 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSSearchBar.h"
|
||||
#import "Theme.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation OWSSearchBar
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
[self ows_configure];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self ows_configure];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
if (self = [super initWithCoder:aDecoder]) {
|
||||
[self ows_configure];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)ows_configure
|
||||
{
|
||||
[self ows_applyTheme];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(themeDidChange:)
|
||||
name:ThemeDidChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)ows_applyTheme
|
||||
{
|
||||
[self.class applyThemeToSearchBar:self];
|
||||
}
|
||||
|
||||
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
UIColor *foregroundColor = UIColor.lokiLightestGray;
|
||||
searchBar.barTintColor = Theme.backgroundColor;
|
||||
searchBar.barStyle = Theme.barStyle;
|
||||
searchBar.tintColor = UIColor.lokiGreen;
|
||||
|
||||
// Hide searchBar border.
|
||||
// Alternatively we could hide the border by using `UISearchBarStyleMinimal`, but that causes an issue when toggling
|
||||
// from light -> dark -> light theme wherein the textField background color appears darker than it should
|
||||
// (regardless of our re-setting textfield.backgroundColor below).
|
||||
searchBar.backgroundImage = [UIImage new];
|
||||
|
||||
if (Theme.isDarkThemeEnabled) {
|
||||
UIImage *clearImage = [UIImage imageNamed:@"searchbar_clear"];
|
||||
[searchBar setImage:[clearImage asTintedImageWithColor:foregroundColor]
|
||||
forSearchBarIcon:UISearchBarIconClear
|
||||
state:UIControlStateNormal];
|
||||
|
||||
UIImage *searchImage = [UIImage imageNamed:@"searchbar_search"];
|
||||
[searchBar setImage:[searchImage asTintedImageWithColor:foregroundColor]
|
||||
forSearchBarIcon:UISearchBarIconSearch
|
||||
state:UIControlStateNormal];
|
||||
} else {
|
||||
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal];
|
||||
|
||||
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
|
||||
}
|
||||
|
||||
[searchBar traverseViewHierarchyWithVisitor:^(UIView *view) {
|
||||
if ([view isKindOfClass:[UITextField class]]) {
|
||||
UITextField *textField = (UITextField *)view;
|
||||
textField.backgroundColor = Theme.searchFieldBackgroundColor;
|
||||
textField.textColor = Theme.primaryColor;
|
||||
NSString *placeholder = textField.placeholder;
|
||||
if (placeholder != nil) {
|
||||
NSMutableAttributedString *attributedPlaceholder = [[NSMutableAttributedString alloc] initWithString:placeholder];
|
||||
[attributedPlaceholder addAttribute:NSForegroundColorAttributeName value:foregroundColor range:NSMakeRange(0, placeholder.length)];
|
||||
textField.attributedPlaceholder = attributedPlaceholder;
|
||||
}
|
||||
textField.keyboardAppearance = LKAppModeUtilities.isLightMode ? UIKeyboardAppearanceDefault : UIKeyboardAppearanceDark;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)themeDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self ows_applyTheme];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,13 +0,0 @@
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <Curve25519Kit/Ed25519.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSPrimaryStorage (Loki)
|
||||
|
||||
- (void)updateMessageIDCollectionByPruningMessagesWithIDs:(NSSet<NSString *> *)targetMessageIDs in:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(updateMessageIDCollectionByPruningMessagesWithIDs(_:in:));
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,25 +0,0 @@
|
||||
#import "OWSPrimaryStorage+Loki.h"
|
||||
#import "OWSPrimaryStorage+keyFromIntLong.h"
|
||||
#import "NSDate+OWS.h"
|
||||
#import "TSAccountManager.h"
|
||||
#import "YapDatabaseConnection+OWS.h"
|
||||
#import "YapDatabaseTransaction+OWS.h"
|
||||
#import "NSObject+Casting.h"
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
#define LKMessageIDCollection @"LKMessageIDCollection"
|
||||
|
||||
@implementation OWSPrimaryStorage (Loki)
|
||||
|
||||
- (void)updateMessageIDCollectionByPruningMessagesWithIDs:(NSSet<NSString *> *)targetMessageIDs in:(YapDatabaseReadWriteTransaction *)transaction {
|
||||
NSMutableArray<NSString *> *serverIDs = [NSMutableArray new];
|
||||
[transaction enumerateRowsInCollection:LKMessageIDCollection usingBlock:^(NSString *key, id object, id metadata, BOOL *stop) {
|
||||
if (![object isKindOfClass:NSString.class]) { return; }
|
||||
NSString *messageID = (NSString *)object;
|
||||
if (![targetMessageIDs containsObject:messageID]) { return; }
|
||||
[serverIDs addObject:key];
|
||||
}];
|
||||
[transaction removeObjectsForKeys:serverIDs inCollection:LKMessageIDCollection];
|
||||
}
|
||||
|
||||
@end
|
@ -1,68 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const ThemeDidChangeNotification;
|
||||
|
||||
@interface Theme : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@property (class, readonly, atomic) BOOL isDarkThemeEnabled;
|
||||
|
||||
+ (void)setIsDarkThemeEnabled:(BOOL)value;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *backgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *primaryColor;
|
||||
@property (class, readonly, nonatomic) UIColor *secondaryColor;
|
||||
@property (class, readonly, nonatomic) UIColor *boldColor;
|
||||
@property (class, readonly, nonatomic) UIColor *offBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *middleGrayColor;
|
||||
@property (class, readonly, nonatomic) UIColor *placeholderColor;
|
||||
@property (class, readonly, nonatomic) UIColor *hairlineColor;
|
||||
|
||||
#pragma mark - Global App Colors
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *navbarBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *navbarIconColor;
|
||||
@property (class, readonly, nonatomic) UIColor *navbarTitleColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *toolbarBackgroundColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *conversationButtonBackgroundColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *cellSelectedColor;
|
||||
@property (class, readonly, nonatomic) UIColor *cellSeparatorColor;
|
||||
|
||||
// In some contexts, e.g. media viewing/sending, we always use "dark theme" UI regardless of the
|
||||
// users chosen theme.
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemeNavbarIconColor;
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemeNavbarBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemeBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemePrimaryColor;
|
||||
@property (class, readonly, nonatomic) UIBlurEffect *darkThemeBarBlurEffect;
|
||||
@property (class, readonly, nonatomic) UIColor *galleryHighlightColor;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@property (class, readonly, nonatomic) UIBarStyle barStyle;
|
||||
@property (class, readonly, nonatomic) UIColor *searchFieldBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIBlurEffect *barBlurEffect;
|
||||
@property (class, readonly, nonatomic) UIKeyboardAppearance keyboardAppearance;
|
||||
@property (class, readonly, nonatomic) UIKeyboardAppearance darkThemeKeyboardAppearance;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *toastForegroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *toastBackgroundColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *scrollButtonBackgroundColor;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,217 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Theme.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIUtil.h"
|
||||
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/YapDatabaseConnection+OWS.h>
|
||||
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const ThemeDidChangeNotification = @"ThemeDidChangeNotification";
|
||||
|
||||
NSString *const ThemeCollection = @"ThemeCollection";
|
||||
NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled";
|
||||
|
||||
|
||||
@interface Theme ()
|
||||
|
||||
@property (nonatomic) NSNumber *isDarkThemeEnabledNumber;
|
||||
|
||||
@end
|
||||
|
||||
@implementation Theme
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static Theme *instance;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [Theme new];
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
+ (BOOL)isDarkThemeEnabled
|
||||
{
|
||||
return LKAppModeUtilities.isDarkMode;
|
||||
}
|
||||
|
||||
- (BOOL)isDarkThemeEnabled
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
return LKAppModeUtilities.isDarkMode;
|
||||
}
|
||||
|
||||
+ (void)setIsDarkThemeEnabled:(BOOL)value
|
||||
{
|
||||
return [self.sharedInstance setIsDarkThemeEnabled:value];
|
||||
}
|
||||
|
||||
- (void)setIsDarkThemeEnabled:(BOOL)value
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
+ (UIColor *)backgroundColor
|
||||
{
|
||||
return LKColors.navigationBarBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)offBackgroundColor
|
||||
{
|
||||
return LKColors.unimportant;
|
||||
}
|
||||
|
||||
+ (UIColor *)primaryColor
|
||||
{
|
||||
return LKColors.text;
|
||||
}
|
||||
|
||||
+ (UIColor *)secondaryColor
|
||||
{
|
||||
return LKColors.separator;
|
||||
}
|
||||
|
||||
+ (UIColor *)boldColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.blackColor);
|
||||
}
|
||||
|
||||
+ (UIColor *)middleGrayColor
|
||||
{
|
||||
return [UIColor colorWithWhite:0.5f alpha:1.f];
|
||||
}
|
||||
|
||||
+ (UIColor *)placeholderColor
|
||||
{
|
||||
return LKColors.navigationBarBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)hairlineColor
|
||||
{
|
||||
return LKColors.separator;
|
||||
}
|
||||
|
||||
#pragma mark - Global App Colors
|
||||
|
||||
+ (UIColor *)navbarBackgroundColor
|
||||
{
|
||||
return UIColor.lokiDarkestGray;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemeNavbarBackgroundColor
|
||||
{
|
||||
return UIColor.ows_blackColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)navbarIconColor
|
||||
{
|
||||
return UIColor.lokiGreen;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemeNavbarIconColor;
|
||||
{
|
||||
return LKColors.text;
|
||||
}
|
||||
|
||||
+ (UIColor *)navbarTitleColor
|
||||
{
|
||||
return Theme.primaryColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)toolbarBackgroundColor
|
||||
{
|
||||
return self.navbarBackgroundColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)cellSelectedColor
|
||||
{
|
||||
return UIColor.lokiDarkGray;
|
||||
}
|
||||
|
||||
+ (UIColor *)cellSeparatorColor
|
||||
{
|
||||
return Theme.hairlineColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemeBackgroundColor
|
||||
{
|
||||
return LKColors.navigationBarBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemePrimaryColor
|
||||
{
|
||||
return LKColors.text;
|
||||
}
|
||||
|
||||
+ (UIColor *)galleryHighlightColor
|
||||
{
|
||||
return UIColor.lokiGreen;
|
||||
}
|
||||
|
||||
+ (UIColor *)conversationButtonBackgroundColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? [UIColor colorWithWhite:0.35f alpha:1.f] : UIColor.ows_gray02Color);
|
||||
}
|
||||
|
||||
+ (UIBlurEffect *)barBlurEffect
|
||||
{
|
||||
return Theme.isDarkThemeEnabled ? self.darkThemeBarBlurEffect
|
||||
: [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||
}
|
||||
|
||||
+ (UIBlurEffect *)darkThemeBarBlurEffect
|
||||
{
|
||||
return [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
|
||||
}
|
||||
|
||||
+ (UIKeyboardAppearance)keyboardAppearance
|
||||
{
|
||||
return LKAppModeUtilities.isLightMode ? UIKeyboardAppearanceDefault : UIKeyboardAppearanceDark;
|
||||
}
|
||||
|
||||
+ (UIKeyboardAppearance)darkThemeKeyboardAppearance;
|
||||
{
|
||||
return UIKeyboardAppearanceDark;
|
||||
}
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
+ (UIBarStyle)barStyle
|
||||
{
|
||||
return Theme.isDarkThemeEnabled ? UIBarStyleBlack : UIBarStyleDefault;
|
||||
}
|
||||
|
||||
+ (UIColor *)searchFieldBackgroundColor
|
||||
{
|
||||
return Theme.isDarkThemeEnabled ? Theme.offBackgroundColor : UIColor.ows_gray05Color;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (UIColor *)toastForegroundColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.ows_whiteColor);
|
||||
}
|
||||
|
||||
+ (UIColor *)toastBackgroundColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? UIColor.ows_gray75Color : UIColor.ows_gray60Color);
|
||||
}
|
||||
|
||||
+ (UIColor *)scrollButtonBackgroundColor
|
||||
{
|
||||
return UIColor.lokiDarkerGray;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
Loading…
Reference in New Issue