Fixed a number of issues with the Notification Service Extension
Fixed an issue where receiving a PN for outgoing messages could break the NotificationServiceExtension Fixed an issue where the NotificationServiceExtension could startup in an invalid way resulting in subsequent PNs failing to process Fixed an issue where you could incorrectly receive multiple generic notifications after receiving an incoming call notification Fixed an issue where the read state syncing might not clear notifications from the notification center Fixed an issue with parsing Bencoded data Updated the PN subscription to subscribe to CONVO_INFO_VOLATILE notifications (update read state) Updated the NotificationServiceExtension to use standard message processing where possible Updated the NotificationServiceExtension to update the app badge based on a database query
This commit is contained in:
parent
ab610578e6
commit
a2f1f36d2c
|
@ -740,6 +740,7 @@
|
|||
FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; };
|
||||
FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; };
|
||||
FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */; };
|
||||
FD9AECA52AAA9609009B3406 /* NotificationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9AECA42AAA9609009B3406 /* NotificationError.swift */; };
|
||||
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; };
|
||||
FD9BDE002A5D22B7005F1EBC /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; };
|
||||
FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
|
||||
|
@ -1856,6 +1857,7 @@
|
|||
FD96F3A629DBD43D00401309 /* MockJobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockJobRunner.swift; sourceTree = "<group>"; };
|
||||
FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = "<group>"; };
|
||||
FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMarkerCell.swift; sourceTree = "<group>"; };
|
||||
FD9AECA42AAA9609009B3406 /* NotificationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationError.swift; sourceTree = "<group>"; };
|
||||
FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; };
|
||||
FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSessionUtil.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = "<group>"; };
|
||||
|
@ -2408,6 +2410,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
C31C219B255BC92200EC2D66 /* Meta */,
|
||||
FD9AECA42AAA9609009B3406 /* NotificationError.swift */,
|
||||
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */,
|
||||
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */,
|
||||
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */,
|
||||
|
@ -5454,6 +5457,7 @@
|
|||
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */,
|
||||
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */,
|
||||
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */,
|
||||
FD9AECA52AAA9609009B3406 /* NotificationError.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -6445,7 +6449,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 422;
|
||||
CURRENT_PROJECT_VERSION = 423;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6469,7 +6473,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -6517,7 +6521,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 422;
|
||||
CURRENT_PROJECT_VERSION = 423;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -6546,7 +6550,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -6582,7 +6586,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 422;
|
||||
CURRENT_PROJECT_VERSION = 423;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -6605,7 +6609,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||
|
@ -6656,7 +6660,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 422;
|
||||
CURRENT_PROJECT_VERSION = 423;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -6684,7 +6688,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||
|
@ -7616,7 +7620,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 422;
|
||||
CURRENT_PROJECT_VERSION = 423;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7654,7 +7658,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
|
@ -7687,7 +7691,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 422;
|
||||
CURRENT_PROJECT_VERSION = 423;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7725,7 +7729,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
PRODUCT_NAME = Session;
|
||||
|
|
|
@ -93,7 +93,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
|
||||
public func reportOutgoingCall(_ call: SessionCall) {
|
||||
AssertIsOnMainThread()
|
||||
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
|
||||
UserDefaults.sharedLokiProject?[.isCallOngoing] = true
|
||||
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
|
||||
|
||||
call.stateDidChange = {
|
||||
if call.hasStartedConnecting {
|
||||
|
@ -123,7 +124,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
completion(error)
|
||||
return
|
||||
}
|
||||
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
|
||||
UserDefaults.sharedLokiProject?[.isCallOngoing] = true
|
||||
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
|
@ -138,7 +140,9 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
|||
|
||||
func handleCallEnded() {
|
||||
WebRTCSession.current = nil
|
||||
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
|
||||
UserDefaults.sharedLokiProject?[.isCallOngoing] = false
|
||||
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil
|
||||
|
||||
if CurrentAppContext().isInBackground() {
|
||||
(UIApplication.shared.delegate as? AppDelegate)?.stopPollers()
|
||||
DDLog.flushLog()
|
||||
|
|
|
@ -89,7 +89,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
)
|
||||
|
||||
if Environment.shared?.callManager.wrappedValue?.currentCall == nil {
|
||||
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
|
||||
UserDefaults.sharedLokiProject?[.isCallOngoing] = false
|
||||
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil
|
||||
}
|
||||
|
||||
// No point continuing if we are running tests
|
||||
|
@ -416,7 +417,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
// Don't offer the 'Restore' option if it was a 'startupFailed' error as a restore is unlikely to
|
||||
// resolve it (most likely the database is locked or the key was somehow lost - safer to get them
|
||||
// to restart and manually reinstall/restore)
|
||||
case .databaseError(StorageError.startupFailed): break
|
||||
case .databaseError(StorageError.startupFailed), .databaseError(DatabaseError.SQLITE_LOCKED): break
|
||||
|
||||
// Offer the 'Restore' option if it was a migration error
|
||||
case .databaseError:
|
||||
|
@ -663,39 +664,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
/// we don't block user interaction while it's running
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
let unreadCount: Int = Storage.shared
|
||||
.read { db in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
|
||||
return try Interaction
|
||||
.filter(Interaction.Columns.wasRead == false)
|
||||
.filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant))
|
||||
.filter(
|
||||
// Only count mentions if 'onlyNotifyForMentions' is set
|
||||
thread[.onlyNotifyForMentions] == false ||
|
||||
Interaction.Columns.hasMention == true
|
||||
)
|
||||
.joining(
|
||||
required: Interaction.thread
|
||||
.aliased(thread)
|
||||
.joining(optional: SessionThread.contact)
|
||||
.filter(
|
||||
// Ignore muted threads
|
||||
SessionThread.Columns.mutedUntilTimestamp == nil ||
|
||||
SessionThread.Columns.mutedUntilTimestamp < Date().timeIntervalSince1970
|
||||
)
|
||||
.filter(
|
||||
// Ignore message request threads
|
||||
SessionThread.Columns.variant != SessionThread.Variant.contact ||
|
||||
!SessionThread.isMessageRequest(userPublicKey: userPublicKey)
|
||||
)
|
||||
)
|
||||
.fetchCount(db)
|
||||
}
|
||||
.read { db in try Interaction.fetchUnreadCount(db) }
|
||||
.defaulting(to: 0)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
CurrentAppContext().setMainAppBadgeNumber(unreadCount)
|
||||
UIApplication.shared.applicationIconBadgeNumber = unreadCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -904,7 +877,9 @@ private enum StartupError: Error {
|
|||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .databaseError(StorageError.startupFailed): return "Database startup failed"
|
||||
case .databaseError(StorageError.startupFailed), .databaseError(DatabaseError.SQLITE_LOCKED):
|
||||
return "Database startup failed"
|
||||
|
||||
case .failedToRestore: return "Failed to restore"
|
||||
case .databaseError: return "Database error"
|
||||
case .startupTimeout: return "Startup timeout"
|
||||
|
@ -913,7 +888,9 @@ private enum StartupError: Error {
|
|||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .databaseError(StorageError.startupFailed): return "DATABASE_STARTUP_FAILED".localized()
|
||||
case .databaseError(StorageError.startupFailed), .databaseError(DatabaseError.SQLITE_LOCKED):
|
||||
return "DATABASE_STARTUP_FAILED".localized()
|
||||
|
||||
case .failedToRestore: return "DATABASE_RESTORE_FAILED".localized()
|
||||
case .databaseError: return "DATABASE_MIGRATION_FAILED".localized()
|
||||
case .startupTimeout: return "APP_STARTUP_TIMEOUT".localized()
|
||||
|
|
|
@ -179,11 +179,6 @@ final class MainAppContext: NSObject, AppContext {
|
|||
UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking
|
||||
}
|
||||
|
||||
func setMainAppBadgeNumber(_ value: Int) {
|
||||
UIApplication.shared.applicationIconBadgeNumber = value
|
||||
UserDefaults.sharedLokiProject?.setValue(value, forKey: "currentBadgeNumber")
|
||||
}
|
||||
|
||||
func frontmostViewController() -> UIViewController? {
|
||||
UIApplication.shared.frontmostViewControllerIgnoringAlerts
|
||||
}
|
||||
|
|
|
@ -240,7 +240,7 @@ final class NukeDataModal: Modal {
|
|||
|
||||
// Clear the app badge and notifications
|
||||
AppEnvironment.shared.notificationPresenter.clearAllNotifications()
|
||||
CurrentAppContext().setMainAppBadgeNumber(0)
|
||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||
|
||||
// Clear out the user defaults
|
||||
UserDefaults.removeAll()
|
||||
|
|
|
@ -465,6 +465,46 @@ public extension Interaction {
|
|||
// MARK: - GRDB Interactions
|
||||
|
||||
public extension Interaction {
|
||||
struct ReadInfo: Decodable, FetchableRecord {
|
||||
let id: Int64
|
||||
let variant: Interaction.Variant
|
||||
let timestampMs: Int64
|
||||
let wasRead: Bool
|
||||
}
|
||||
|
||||
static func fetchUnreadCount(
|
||||
_ db: Database,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
) throws -> Int {
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
|
||||
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
|
||||
|
||||
return try Interaction
|
||||
.filter(Interaction.Columns.wasRead == false)
|
||||
.filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant))
|
||||
.filter(
|
||||
// Only count mentions if 'onlyNotifyForMentions' is set
|
||||
thread[.onlyNotifyForMentions] == false ||
|
||||
Interaction.Columns.hasMention == true
|
||||
)
|
||||
.joining(
|
||||
required: Interaction.thread
|
||||
.aliased(thread)
|
||||
.joining(optional: SessionThread.contact)
|
||||
.filter(
|
||||
// Ignore muted threads
|
||||
SessionThread.Columns.mutedUntilTimestamp == nil ||
|
||||
SessionThread.Columns.mutedUntilTimestamp < dependencies.dateNow.timeIntervalSince1970
|
||||
)
|
||||
.filter(
|
||||
// Ignore message request threads
|
||||
SessionThread.Columns.variant != SessionThread.Variant.contact ||
|
||||
!SessionThread.isMessageRequest(userPublicKey: userPublicKey)
|
||||
)
|
||||
)
|
||||
.fetchCount(db)
|
||||
}
|
||||
|
||||
/// This will update the `wasRead` state the the interaction
|
||||
///
|
||||
/// - Parameters
|
||||
|
@ -482,83 +522,16 @@ public extension Interaction {
|
|||
) throws {
|
||||
guard let interactionId: Int64 = interactionId else { return }
|
||||
|
||||
struct InteractionReadInfo: Decodable, FetchableRecord {
|
||||
let id: Int64
|
||||
let variant: Interaction.Variant
|
||||
let timestampMs: Int64
|
||||
let wasRead: Bool
|
||||
}
|
||||
|
||||
// Once all of the below is done schedule the jobs
|
||||
func scheduleJobs(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
interactionInfo: [InteractionReadInfo],
|
||||
lastReadTimestampMs: Int64
|
||||
) throws {
|
||||
// Update the last read timestamp if needed
|
||||
try SessionUtil.syncThreadLastReadIfNeeded(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
lastReadTimestampMs: lastReadTimestampMs
|
||||
)
|
||||
|
||||
// Add the 'DisappearingMessagesJob' if needed - this will update any expiring
|
||||
// messages `expiresStartedAtMs` values
|
||||
JobRunner.upsert(
|
||||
db,
|
||||
job: DisappearingMessagesJob.updateNextRunIfNeeded(
|
||||
db,
|
||||
interactionIds: interactionInfo.map { $0.id },
|
||||
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
|
||||
)
|
||||
)
|
||||
|
||||
// Clear out any notifications for the interactions we mark as read
|
||||
Environment.shared?.notificationsManager.wrappedValue?.cancelNotifications(
|
||||
identifiers: interactionInfo
|
||||
.map { interactionInfo in
|
||||
Interaction.notificationIdentifier(
|
||||
for: interactionInfo.id,
|
||||
threadId: threadId,
|
||||
shouldGroupMessagesForThread: false
|
||||
)
|
||||
}
|
||||
.appending(Interaction.notificationIdentifier(
|
||||
for: 0,
|
||||
threadId: threadId,
|
||||
shouldGroupMessagesForThread: true
|
||||
))
|
||||
)
|
||||
|
||||
// If we want to send read receipts and it's a contact thread then try to add the
|
||||
// 'SendReadReceiptsJob' for and unread messages that weren't outgoing
|
||||
if trySendReadReceipt && threadVariant == .contact {
|
||||
JobRunner.upsert(
|
||||
db,
|
||||
job: SendReadReceiptsJob.createOrUpdateIfNeeded(
|
||||
db,
|
||||
threadId: threadId,
|
||||
interactionIds: interactionInfo
|
||||
.filter { !$0.wasRead && $0.variant != .standardOutgoing }
|
||||
.map { $0.id }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Since there is no guarantee on the order messages are inserted into the database
|
||||
// fetch the timestamp for the interaction and set everything before that as read
|
||||
let maybeInteractionInfo: InteractionReadInfo? = try Interaction
|
||||
let maybeInteractionInfo: Interaction.ReadInfo? = try Interaction
|
||||
.select(.id, .variant, .timestampMs, .wasRead)
|
||||
.filter(id: interactionId)
|
||||
.asRequest(of: InteractionReadInfo.self)
|
||||
.asRequest(of: Interaction.ReadInfo.self)
|
||||
.fetchOne(db)
|
||||
|
||||
// If we aren't including older interactions then update and save the current one
|
||||
guard includingOlder, let interactionInfo: InteractionReadInfo = maybeInteractionInfo else {
|
||||
guard includingOlder, let interactionInfo: Interaction.ReadInfo = maybeInteractionInfo else {
|
||||
// Only mark as read and trigger the subsequent jobs if the interaction is
|
||||
// actually not read (no point updating and triggering db changes otherwise)
|
||||
guard
|
||||
|
@ -575,19 +548,21 @@ public extension Interaction {
|
|||
.filter(id: interactionId)
|
||||
.updateAll(db, Columns.wasRead.set(to: true))
|
||||
|
||||
try scheduleJobs(
|
||||
try Interaction.scheduleReadJobs(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
interactionInfo: [
|
||||
InteractionReadInfo(
|
||||
Interaction.ReadInfo(
|
||||
id: interactionId,
|
||||
variant: variant,
|
||||
timestampMs: 0,
|
||||
wasRead: false
|
||||
)
|
||||
],
|
||||
lastReadTimestampMs: timestampMs
|
||||
lastReadTimestampMs: timestampMs,
|
||||
trySendReadReceipt: trySendReadReceipt,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
return
|
||||
}
|
||||
|
@ -596,21 +571,23 @@ public extension Interaction {
|
|||
.filter(Interaction.Columns.threadId == threadId)
|
||||
.filter(Interaction.Columns.timestampMs <= interactionInfo.timestampMs)
|
||||
.filter(Interaction.Columns.wasRead == false)
|
||||
let interactionInfoToMarkAsRead: [InteractionReadInfo] = try interactionQuery
|
||||
let interactionInfoToMarkAsRead: [Interaction.ReadInfo] = try interactionQuery
|
||||
.select(.id, .variant, .timestampMs, .wasRead)
|
||||
.asRequest(of: InteractionReadInfo.self)
|
||||
.asRequest(of: Interaction.ReadInfo.self)
|
||||
.fetchAll(db)
|
||||
|
||||
// If there are no other interactions to mark as read then just schedule the jobs
|
||||
// for this interaction (need to ensure the disapeparing messages run for sync'ed
|
||||
// outgoing messages which will always have 'wasRead' as false)
|
||||
guard !interactionInfoToMarkAsRead.isEmpty else {
|
||||
try scheduleJobs(
|
||||
try Interaction.scheduleReadJobs(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
interactionInfo: [interactionInfo],
|
||||
lastReadTimestampMs: interactionInfo.timestampMs
|
||||
lastReadTimestampMs: interactionInfo.timestampMs,
|
||||
trySendReadReceipt: trySendReadReceipt,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
return
|
||||
}
|
||||
|
@ -619,12 +596,14 @@ public extension Interaction {
|
|||
try interactionQuery.updateAll(db, Columns.wasRead.set(to: true))
|
||||
|
||||
// Retrieve the interaction ids we want to update
|
||||
try scheduleJobs(
|
||||
try Interaction.scheduleReadJobs(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
interactionInfo: interactionInfoToMarkAsRead,
|
||||
lastReadTimestampMs: interactionInfo.timestampMs
|
||||
lastReadTimestampMs: interactionInfo.timestampMs,
|
||||
trySendReadReceipt: trySendReadReceipt,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -691,6 +670,71 @@ public extension Interaction {
|
|||
.asSet()
|
||||
.subtracting(timestampsUpdated)
|
||||
}
|
||||
|
||||
static func scheduleReadJobs(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
interactionInfo: [Interaction.ReadInfo],
|
||||
lastReadTimestampMs: Int64,
|
||||
trySendReadReceipt: Bool,
|
||||
calledFromConfigHandling: Bool
|
||||
) throws {
|
||||
guard !interactionInfo.isEmpty else { return }
|
||||
|
||||
// Update the last read timestamp if needed
|
||||
if !calledFromConfigHandling {
|
||||
try SessionUtil.syncThreadLastReadIfNeeded(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
lastReadTimestampMs: lastReadTimestampMs
|
||||
)
|
||||
}
|
||||
|
||||
// Add the 'DisappearingMessagesJob' if needed - this will update any expiring
|
||||
// messages `expiresStartedAtMs` values
|
||||
JobRunner.upsert(
|
||||
db,
|
||||
job: DisappearingMessagesJob.updateNextRunIfNeeded(
|
||||
db,
|
||||
interactionIds: interactionInfo.map { $0.id },
|
||||
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
|
||||
)
|
||||
)
|
||||
|
||||
// Clear out any notifications for the interactions we mark as read
|
||||
Environment.shared?.notificationsManager.wrappedValue?.cancelNotifications(
|
||||
identifiers: interactionInfo
|
||||
.map { interactionInfo in
|
||||
Interaction.notificationIdentifier(
|
||||
for: interactionInfo.id,
|
||||
threadId: threadId,
|
||||
shouldGroupMessagesForThread: false
|
||||
)
|
||||
}
|
||||
.appending(Interaction.notificationIdentifier(
|
||||
for: 0,
|
||||
threadId: threadId,
|
||||
shouldGroupMessagesForThread: true
|
||||
))
|
||||
)
|
||||
|
||||
/// If we want to send read receipts and it's a contact thread then try to add the `SendReadReceiptsJob` for and unread
|
||||
/// messages that weren't outgoing
|
||||
if trySendReadReceipt && threadVariant == .contact {
|
||||
JobRunner.upsert(
|
||||
db,
|
||||
job: SendReadReceiptsJob.createOrUpdateIfNeeded(
|
||||
db,
|
||||
threadId: threadId,
|
||||
interactionIds: interactionInfo
|
||||
.filter { !$0.wasRead && $0.variant != .standardOutgoing }
|
||||
.map { $0.id }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Search Queries
|
||||
|
|
|
@ -41,7 +41,7 @@ extension PushNotificationAPI.NotificationMetadata {
|
|||
hash: try container.decode(String.self, forKey: .hash),
|
||||
namespace: try container.decode(Int.self, forKey: .namespace),
|
||||
dataLength: try container.decode(Int.self, forKey: .dataLength),
|
||||
dataTooLong: ((try? container.decode(Bool.self, forKey: .dataTooLong)) ?? false)
|
||||
dataTooLong: ((try? container.decode(Int.self, forKey: .dataTooLong) != 0) ?? false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ public enum PushNotificationAPI {
|
|||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
|
||||
let request: SubscribeRequest = SubscribeRequest(
|
||||
pubkey: currentUserPublicKey,
|
||||
namespaces: [.default],
|
||||
namespaces: [.default, .configConvoInfoVolatile],
|
||||
// Note: Unfortunately we always need the message content because without the content
|
||||
// control messages can't be distinguished from visible messages which results in the
|
||||
// 'generic' notification being shown when receiving things like typing indicator updates
|
||||
|
@ -372,8 +372,11 @@ public enum PushNotificationAPI {
|
|||
return (envelope, .legacySuccess)
|
||||
}
|
||||
|
||||
guard let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String else {
|
||||
return (nil, .failureNoContent)
|
||||
}
|
||||
|
||||
guard
|
||||
let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String,
|
||||
let encData: Data = Data(base64Encoded: base64EncodedEncString),
|
||||
let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies),
|
||||
encData.count > dependencies.crypto.size(.aeadXChaCha20NonceBytes)
|
||||
|
@ -401,7 +404,7 @@ public enum PushNotificationAPI {
|
|||
|
||||
// If the metadata says that the message was too large then we should show the generic
|
||||
// notification (this is a valid case)
|
||||
guard !notification.info.dataTooLong else { return (nil, .success) }
|
||||
guard !notification.info.dataTooLong else { return (nil, .successTooLong) }
|
||||
|
||||
// Check that the body we were given is valid
|
||||
guard
|
||||
|
|
|
@ -5,7 +5,9 @@ import Foundation
|
|||
public extension PushNotificationAPI {
|
||||
enum ProcessResult {
|
||||
case success
|
||||
case successTooLong
|
||||
case failure
|
||||
case failureNoContent
|
||||
case legacySuccess
|
||||
case legacyFailure
|
||||
case legacyForceSilent
|
||||
|
|
|
@ -77,16 +77,28 @@ internal extension SessionUtil {
|
|||
}
|
||||
|
||||
// Mark all older interactions as read
|
||||
try Interaction
|
||||
.filter(
|
||||
Interaction.Columns.threadId == threadId &&
|
||||
Interaction.Columns.timestampMs <= lastReadTimestampMs &&
|
||||
Interaction.Columns.wasRead == false
|
||||
)
|
||||
let interactionQuery = Interaction
|
||||
.filter(Interaction.Columns.threadId == threadId)
|
||||
.filter(Interaction.Columns.timestampMs <= lastReadTimestampMs)
|
||||
.filter(Interaction.Columns.wasRead == false)
|
||||
let interactionInfoToMarkAsRead: [Interaction.ReadInfo] = try interactionQuery
|
||||
.select(.id, .variant, .timestampMs, .wasRead)
|
||||
.asRequest(of: Interaction.ReadInfo.self)
|
||||
.fetchAll(db)
|
||||
try interactionQuery
|
||||
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||
db,
|
||||
Interaction.Columns.wasRead.set(to: true)
|
||||
)
|
||||
try Interaction.scheduleReadJobs(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadInfo.variant,
|
||||
interactionInfo: interactionInfoToMarkAsRead,
|
||||
lastReadTimestampMs: lastReadTimestampMs,
|
||||
trySendReadReceipt: false, // Interactions already read, no need to send
|
||||
calledFromConfigHandling: true
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -56,11 +56,9 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
notificationContent.sound = thread.notificationSound
|
||||
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
|
||||
.notificationSound(isQuiet: false)
|
||||
|
||||
// Badge Number
|
||||
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
||||
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
||||
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
||||
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
|
||||
.map { NSNumber(value: $0) }
|
||||
.defaulting(to: NSNumber(value: 0))
|
||||
|
||||
// Title & body
|
||||
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||
|
@ -157,16 +155,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
|||
let notificationContent = UNMutableNotificationContent()
|
||||
notificationContent.userInfo = userInfo
|
||||
notificationContent.sound = thread.notificationSound
|
||||
.defaulting(
|
||||
to: db[.defaultNotificationSound]
|
||||
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
||||
)
|
||||
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
|
||||
.notificationSound(isQuiet: false)
|
||||
|
||||
// Badge Number
|
||||
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
||||
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
||||
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
||||
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
|
||||
.map { NSNumber(value: $0) }
|
||||
.defaulting(to: NSNumber(value: 0))
|
||||
|
||||
notificationContent.title = "Session"
|
||||
notificationContent.body = ""
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionMessagingKit
|
||||
|
||||
enum NotificationError: LocalizedError {
|
||||
case processing(PushNotificationAPI.ProcessResult)
|
||||
case messageProcessing
|
||||
case messageHandling(MessageReceiverError)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .processing(let result): return "Failed to process notification (\(result))"
|
||||
case .messageProcessing: return "Failed to process message"
|
||||
case .messageHandling(let error): return "Failed to handle message (\(error))"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,11 +15,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
private var didPerformSetup = false
|
||||
private var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
private var request: UNNotificationRequest?
|
||||
private var openGroupPollCancellable: AnyCancellable?
|
||||
|
||||
public static let isFromRemoteKey = "remote"
|
||||
public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId"
|
||||
public static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw"
|
||||
public static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter"
|
||||
private static let callPreOfferLargeNotificationSupressionDuration: TimeInterval = 30
|
||||
|
||||
// MARK: Did receive a remote push notification request
|
||||
|
||||
|
@ -43,6 +45,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
|
||||
let isCallOngoing: Bool = (UserDefaults.sharedLokiProject?[.isCallOngoing])
|
||||
.defaulting(to: false)
|
||||
let lastCallPreOffer: Date? = UserDefaults.sharedLokiProject?[.lastCallPreOffer]
|
||||
|
||||
// Perform main setup
|
||||
Storage.resumeDatabaseAccess()
|
||||
|
@ -52,14 +55,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
||||
let openGroupPollingPublishers: [AnyPublisher<Void, Error>] = self.pollForOpenGroups()
|
||||
defer {
|
||||
Publishers
|
||||
self.openGroupPollCancellable = Publishers
|
||||
.MergeMany(openGroupPollingPublishers)
|
||||
.subscribe(on: DispatchQueue.global(qos: .background))
|
||||
.subscribe(on: DispatchQueue.main)
|
||||
.sinkUntilComplete(
|
||||
receiveCompletion: { _ in
|
||||
self.completeSilenty()
|
||||
}
|
||||
.sink(
|
||||
receiveCompletion: { [weak self] _ in self?.completeSilenty() },
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -75,9 +77,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
// If we got an explicit failure, or we got a success but no content then show
|
||||
// the fallback notification
|
||||
case .success, .legacySuccess, .failure, .legacyFailure:
|
||||
return self.handleFailure(for: notificationContent)
|
||||
|
||||
case .legacyForceSilent: return
|
||||
return self.handleFailure(for: notificationContent, error: .processing(result))
|
||||
|
||||
case .successTooLong:
|
||||
/// If the notification is too long and there is an ongoing call or a recent call pre-offer then we assume the notification
|
||||
/// is a call `ICE_CANDIDATES` message and just complete silently (because the fallback would be annoying), if not
|
||||
/// then we do want to show the fallback notification
|
||||
guard
|
||||
isCallOngoing ||
|
||||
(lastCallPreOffer ?? Date.distantPast).timeIntervalSinceNow < NotificationServiceExtension.callPreOfferLargeNotificationSupressionDuration
|
||||
else { return self.handleFailure(for: notificationContent, error: .processing(result)) }
|
||||
|
||||
NSLog("[NotificationServiceExtension] Suppressing large notification too close to a call.")
|
||||
return
|
||||
|
||||
case .legacyForceSilent, .failureNoContent: return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,10 +101,26 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
Storage.shared.write { db in
|
||||
do {
|
||||
guard let processedMessage: ProcessedMessage = try Message.processRawReceivedMessageAsNotification(db, envelope: envelope) else {
|
||||
self.handleFailure(for: notificationContent)
|
||||
self.handleFailure(for: notificationContent, error: .messageProcessing)
|
||||
return
|
||||
}
|
||||
|
||||
/// Due to the way the `CallMessage` and `SharedConfigMessage` work we need to custom
|
||||
/// handle their behaviours, for all other message types we want to just use standard messages
|
||||
switch processedMessage.messageInfo.message {
|
||||
case is CallMessage, is SharedConfigMessage: break
|
||||
default:
|
||||
try MessageReceiver.handle(
|
||||
db,
|
||||
threadId: processedMessage.threadId,
|
||||
threadVariant: processedMessage.threadVariant,
|
||||
message: processedMessage.messageInfo.message,
|
||||
serverExpirationTimestamp: processedMessage.messageInfo.serverExpirationTimestamp,
|
||||
associatedWithProto: processedMessage.proto
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Throw if the message is outdated and shouldn't be processed
|
||||
try MessageReceiver.throwIfMessageOutdated(
|
||||
db,
|
||||
|
@ -100,47 +130,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
)
|
||||
|
||||
switch processedMessage.messageInfo.message {
|
||||
case let visibleMessage as VisibleMessage:
|
||||
let interactionId: Int64 = try MessageReceiver.handleVisibleMessage(
|
||||
db,
|
||||
threadId: processedMessage.threadId,
|
||||
threadVariant: processedMessage.threadVariant,
|
||||
message: visibleMessage,
|
||||
associatedWithProto: processedMessage.proto
|
||||
)
|
||||
|
||||
// Remove the notifications if there is an outgoing messages from a linked device
|
||||
if
|
||||
let interaction: Interaction = try? Interaction.fetchOne(db, id: interactionId),
|
||||
interaction.variant == .standardOutgoing
|
||||
{
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.getDeliveredNotifications { notifications in
|
||||
let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == interaction.threadId })
|
||||
center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier }))
|
||||
// Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() }
|
||||
}
|
||||
semaphore.wait()
|
||||
}
|
||||
|
||||
case let unsendRequest as UnsendRequest:
|
||||
try MessageReceiver.handleUnsendRequest(
|
||||
db,
|
||||
threadId: processedMessage.threadId,
|
||||
threadVariant: processedMessage.threadVariant,
|
||||
message: unsendRequest
|
||||
)
|
||||
|
||||
case let closedGroupControlMessage as ClosedGroupControlMessage:
|
||||
try MessageReceiver.handleClosedGroupControlMessage(
|
||||
db,
|
||||
threadId: processedMessage.threadId,
|
||||
threadVariant: processedMessage.threadVariant,
|
||||
message: closedGroupControlMessage
|
||||
)
|
||||
|
||||
case let callMessage as CallMessage:
|
||||
try MessageReceiver.handleCallMessage(
|
||||
db,
|
||||
|
@ -187,6 +176,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
break
|
||||
}
|
||||
|
||||
try MessageReceiver.insertCallInfoMessage(db, for: callMessage)
|
||||
self.handleSuccessForIncomingCall(db, for: callMessage)
|
||||
|
||||
case let sharedConfigMessage as SharedConfigMessage:
|
||||
|
@ -210,7 +200,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
if let error = error as? MessageReceiverError, error.isRetryable {
|
||||
switch error {
|
||||
case .invalidGroupPublicKey, .noGroupKeyPair, .outdatedMessage: self.completeSilenty()
|
||||
default: self.handleFailure(for: notificationContent)
|
||||
default: self.handleFailure(for: notificationContent, error: .messageHandling(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -236,6 +226,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
Cryptography.seedRandom()
|
||||
|
||||
AppSetup.setupEnvironment(
|
||||
retrySetupIfDatabaseInvalid: true,
|
||||
appSpecificBlock: {
|
||||
Environment.shared?.notificationsManager.mutate {
|
||||
$0 = NSENotificationPresenter()
|
||||
|
@ -307,14 +298,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
override public func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
NSLog("[NotificationServiceExtension] Execution time expired")
|
||||
openGroupPollCancellable?.cancel()
|
||||
completeSilenty()
|
||||
}
|
||||
|
||||
private func completeSilenty() {
|
||||
NSLog("[NotificationServiceExtension] Complete silently")
|
||||
let silentContent: UNMutableNotificationContent = UNMutableNotificationContent()
|
||||
silentContent.badge = Storage.shared
|
||||
.read { db in try Interaction.fetchUnreadCount(db) }
|
||||
.map { NSNumber(value: $0) }
|
||||
.defaulting(to: NSNumber(value: 0))
|
||||
Storage.suspendDatabaseAccess()
|
||||
|
||||
self.contentHandler!(.init())
|
||||
self.contentHandler!(silentContent)
|
||||
}
|
||||
|
||||
private func handleSuccessForIncomingCall(_ db: Database, for callMessage: CallMessage) {
|
||||
|
@ -330,11 +328,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in
|
||||
if let error = error {
|
||||
self.handleFailureForVoIP(db, for: callMessage)
|
||||
SNLog("Failed to notify main app of call message: \(error)")
|
||||
NSLog("[NotificationServiceExtension] Failed to notify main app of call message: \(error)")
|
||||
}
|
||||
else {
|
||||
NSLog("[NotificationServiceExtension] Successfully notified main app of call message.")
|
||||
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
|
||||
self.completeSilenty()
|
||||
SNLog("Successfully notified main app of call message.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -347,11 +346,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
let notificationContent = UNMutableNotificationContent()
|
||||
notificationContent.userInfo = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
||||
notificationContent.title = "Session"
|
||||
|
||||
// Badge Number
|
||||
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
||||
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
||||
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
||||
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
|
||||
.map { NSNumber(value: $0) }
|
||||
.defaulting(to: NSNumber(value: 0))
|
||||
|
||||
if let sender: String = callMessage.sender {
|
||||
let senderDisplayName: String = Profile.displayName(db, id: sender, threadVariant: .contact)
|
||||
|
@ -367,20 +364,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
|||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
SNLog("Failed to add notification request due to error:\(error)")
|
||||
NSLog("[NotificationServiceExtension] Failed to add notification request due to error: \(error)")
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
semaphore.wait()
|
||||
SNLog("Add remote notification request")
|
||||
NSLog("[NotificationServiceExtension] Add remote notification request")
|
||||
}
|
||||
|
||||
private func handleFailure(for content: UNMutableNotificationContent) {
|
||||
private func handleFailure(for content: UNMutableNotificationContent, error: NotificationError) {
|
||||
NSLog("[NotificationServiceExtension] Show generic failure message due to error: \(error)")
|
||||
Storage.suspendDatabaseAccess()
|
||||
|
||||
content.body = "You've got a new message"
|
||||
content.title = "Session"
|
||||
let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
||||
content.body = "APN_Message".localized()
|
||||
let userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ]
|
||||
content.userInfo = userInfo
|
||||
contentHandler!(content)
|
||||
}
|
||||
|
|
|
@ -71,7 +71,6 @@ final class NotificationServiceExtensionContext : NSObject, AppContext {
|
|||
func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjectsDescription: String) { }
|
||||
func frontmostViewController() -> UIViewController? { nil }
|
||||
func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { }
|
||||
func setMainAppBadgeNumber(_ value: Int) { }
|
||||
func setNetworkActivityIndicatorVisible(_ value: Bool) { }
|
||||
func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { }
|
||||
}
|
||||
|
|
|
@ -186,10 +186,6 @@ final class ShareAppExtensionContext: NSObject, AppContext {
|
|||
OWSLogger.debug("Ignoring request to block sleep.")
|
||||
}
|
||||
|
||||
func setMainAppBadgeNumber(_ value: Int) {
|
||||
owsFailDebug("")
|
||||
}
|
||||
|
||||
func setNetworkActivityIndicatorVisible(_ value: Bool) {
|
||||
owsFailDebug("")
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@ open class Storage {
|
|||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
|
||||
let error: Error = (startupError ?? StorageError.startupFailed)
|
||||
SNLog("[Database Error] Statup failed with error: \(error)")
|
||||
onComplete(.failure(StorageError.startupFailed), false)
|
||||
onComplete(.failure(error), false)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -430,12 +430,16 @@ open class Storage {
|
|||
try? deleteDbKeys()
|
||||
}
|
||||
|
||||
public static func reconfigureDatabase() {
|
||||
Storage.shared.configureDatabase()
|
||||
}
|
||||
|
||||
public static func resetForCleanMigration() {
|
||||
// Clear existing content
|
||||
resetAllStorage()
|
||||
|
||||
// Reconfigure
|
||||
Storage.shared.configureDatabase()
|
||||
reconfigureDatabase()
|
||||
}
|
||||
|
||||
private static func deleteDatabaseFiles() {
|
||||
|
|
|
@ -73,9 +73,6 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
|
|||
// Should be a NOOP if isMainApp is NO.
|
||||
- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray<id> *)blockingObjects;
|
||||
|
||||
// Should only be called if isMainApp is YES.
|
||||
- (void)setMainAppBadgeNumber:(NSInteger)value;
|
||||
|
||||
- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated;
|
||||
|
||||
@property (nonatomic, readonly) CGFloat statusBarHeight;
|
||||
|
|
|
@ -42,6 +42,7 @@ public enum SNUserDefaults {
|
|||
case lastOpen
|
||||
case lastGarbageCollection
|
||||
case lastPushNotificationSync
|
||||
case lastCallPreOffer
|
||||
}
|
||||
|
||||
public enum Double: Swift.String {
|
||||
|
|
|
@ -2,13 +2,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public protocol BencodableType {
|
||||
associatedtype ValueType: BencodableType
|
||||
|
||||
static var isCollection: Bool { get }
|
||||
static var isDictionary: Bool { get }
|
||||
}
|
||||
|
||||
public struct BencodeResponse<T: Codable> {
|
||||
public let info: T
|
||||
public let data: Data?
|
||||
|
@ -58,23 +51,61 @@ public enum Bencode {
|
|||
using dependencies: Dependencies = Dependencies()
|
||||
) throws -> BencodeResponse<T> where T: Decodable {
|
||||
guard
|
||||
let result: [Data] = try? decode([Data].self, from: data),
|
||||
let responseData: Data = result.first
|
||||
let decodedData: (value: Any, remainingData: Data) = decodeData(data),
|
||||
decodedData.remainingData.isEmpty == true, // Ensure there is no left over data
|
||||
let resultArray: [Any] = decodedData.value as? [Any],
|
||||
resultArray.count > 0
|
||||
else { throw HTTPError.parsingFailed }
|
||||
|
||||
return BencodeResponse(
|
||||
info: try responseData.decoded(as: T.self, using: dependencies),
|
||||
data: (result.count > 1 ? result.last : nil)
|
||||
info: try Bencode.decode(T.self, decodedValue: resultArray[0], using: dependencies),
|
||||
data: {
|
||||
guard resultArray.count > 1 else { return nil }
|
||||
|
||||
switch resultArray.last {
|
||||
case let bencodeString as BencodeString: return bencodeString.rawValue
|
||||
default: return resultArray.last as? Data
|
||||
}
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
public static func decode<T: BencodableType>(_ type: T.Type, from data: Data) throws -> T {
|
||||
public static func decode<T: Decodable>(
|
||||
_ type: T.Type,
|
||||
from data: Data,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
) throws -> T {
|
||||
guard
|
||||
let decodedData: (value: Any, remainingData: Data) = decodeData(data),
|
||||
decodedData.remainingData.isEmpty == true // Ensure there is no left over data
|
||||
else { throw HTTPError.parsingFailed }
|
||||
|
||||
return try recursiveCast(type, from: decodedData.value)
|
||||
return try Bencode.decode(T.self, decodedValue: decodedData.value, using: dependencies)
|
||||
}
|
||||
|
||||
private static func decode<T: Decodable>(
|
||||
_ type: T.Type,
|
||||
decodedValue: Any,
|
||||
using dependencies: Dependencies = Dependencies()
|
||||
) throws -> T {
|
||||
switch (decodedValue, T.self) {
|
||||
case (let directResult as T, _): return directResult
|
||||
case
|
||||
(let bencodeString as BencodeString, is String.Type),
|
||||
(let bencodeString as BencodeString, is Optional<String>.Type):
|
||||
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
|
||||
|
||||
case (let bencodeString as BencodeString, _):
|
||||
return try bencodeString.rawValue.decoded(as: T.self, using: dependencies)
|
||||
|
||||
default:
|
||||
guard
|
||||
let jsonifiedInfo: Any = try? jsonify(decodedValue),
|
||||
let infoData: Data = try? JSONSerialization.data(withJSONObject: jsonifiedInfo)
|
||||
else { throw HTTPError.parsingFailed }
|
||||
|
||||
return try infoData.decoded(as: T.self, using: dependencies)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Logic
|
||||
|
@ -190,74 +221,12 @@ public enum Bencode {
|
|||
|
||||
// MARK: - Internal Functions
|
||||
|
||||
private static func recursiveCast<T: BencodableType>(_ type: T.Type, from value: Any) throws -> T {
|
||||
switch (type.isCollection, type.isDictionary) {
|
||||
case (_, true):
|
||||
guard let dictValue: [String: Any] = value as? [String: Any] else { throw HTTPError.parsingFailed }
|
||||
|
||||
return try (
|
||||
dictValue.mapValues { try recursiveCast(type.ValueType.self, from: $0) } as? T ??
|
||||
{ throw HTTPError.parsingFailed }()
|
||||
)
|
||||
|
||||
case (true, _):
|
||||
guard let arrayValue: [Any] = value as? [Any] else { throw HTTPError.parsingFailed }
|
||||
|
||||
return try (
|
||||
arrayValue.map { try recursiveCast(type.ValueType.self, from: $0) } as? T ??
|
||||
{ throw HTTPError.parsingFailed }()
|
||||
)
|
||||
|
||||
default:
|
||||
switch (value, type) {
|
||||
case (let bencodeString as BencodeString, is String.Type):
|
||||
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
|
||||
|
||||
case (let bencodeString as BencodeString, is Optional<String>.Type):
|
||||
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
|
||||
|
||||
case (let bencodeString as BencodeString, _):
|
||||
return try (bencodeString.rawValue as? T ?? { throw HTTPError.parsingFailed }())
|
||||
|
||||
default: return try (value as? T ?? { throw HTTPError.parsingFailed }())
|
||||
}
|
||||
private static func jsonify(_ value: Any) throws -> Any {
|
||||
switch value {
|
||||
case let arrayValue as [Any]: return try arrayValue.map { try jsonify($0) } as Any
|
||||
case let dictValue as [String: Any]: return try dictValue.mapValues { try jsonify($0) } as Any
|
||||
case let bencodeString as BencodeString: return bencodeString.value as Any
|
||||
default: return value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BencodableType Extensions
|
||||
|
||||
extension Data: BencodableType {
|
||||
public typealias ValueType = Data
|
||||
|
||||
public static var isCollection: Bool { false }
|
||||
public static var isDictionary: Bool { false }
|
||||
}
|
||||
|
||||
extension Int: BencodableType {
|
||||
public typealias ValueType = Int
|
||||
|
||||
public static var isCollection: Bool { false }
|
||||
public static var isDictionary: Bool { false }
|
||||
}
|
||||
|
||||
extension String: BencodableType {
|
||||
public typealias ValueType = String
|
||||
|
||||
public static var isCollection: Bool { false }
|
||||
public static var isDictionary: Bool { false }
|
||||
}
|
||||
|
||||
extension Array: BencodableType where Element: BencodableType {
|
||||
public typealias ValueType = Element
|
||||
|
||||
public static var isCollection: Bool { true }
|
||||
public static var isDictionary: Bool { false }
|
||||
}
|
||||
|
||||
extension Dictionary: BencodableType where Key == String, Value: BencodableType {
|
||||
public typealias ValueType = Value
|
||||
|
||||
public static var isCollection: Bool { false }
|
||||
public static var isDictionary: Bool { true }
|
||||
}
|
||||
|
|
|
@ -13,11 +13,37 @@ class BencodeSpec: QuickSpec {
|
|||
let stringValue: String
|
||||
}
|
||||
|
||||
struct TestType2: Codable, Equatable {
|
||||
let stringValue: String
|
||||
let boolValue: Bool
|
||||
}
|
||||
|
||||
struct TestType3: Codable, Equatable {
|
||||
let stringValue: String
|
||||
let boolValue: Bool
|
||||
|
||||
init(_ stringValue: String, _ boolValue: Bool) {
|
||||
self.stringValue = stringValue
|
||||
self.boolValue = boolValue
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self = TestType3(
|
||||
try container.decode(String.self, forKey: .stringValue),
|
||||
((try? container.decode(Bool.self, forKey: .boolValue)) ?? false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Spec
|
||||
|
||||
override func spec() {
|
||||
describe("Bencode") {
|
||||
// MARK: - when decoding
|
||||
context("when decoding") {
|
||||
// MARK: -- should decode a basic string
|
||||
it("should decode a basic string") {
|
||||
let basicStringData: Data = "5:howdy".data(using: .utf8)!
|
||||
let result = try? Bencode.decode(String.self, from: basicStringData)
|
||||
|
@ -25,6 +51,7 @@ class BencodeSpec: QuickSpec {
|
|||
expect(result).to(equal("howdy"))
|
||||
}
|
||||
|
||||
// MARK: -- should decode a basic integer
|
||||
it("should decode a basic integer") {
|
||||
let basicIntegerData: Data = "i3e".data(using: .utf8)!
|
||||
let result = try? Bencode.decode(Int.self, from: basicIntegerData)
|
||||
|
@ -32,6 +59,7 @@ class BencodeSpec: QuickSpec {
|
|||
expect(result).to(equal(3))
|
||||
}
|
||||
|
||||
// MARK: -- should decode a list of integers
|
||||
it("should decode a list of integers") {
|
||||
let basicIntListData: Data = "li1ei2ee".data(using: .utf8)!
|
||||
let result = try? Bencode.decode([Int].self, from: basicIntListData)
|
||||
|
@ -39,57 +67,246 @@ class BencodeSpec: QuickSpec {
|
|||
expect(result).to(equal([1, 2]))
|
||||
}
|
||||
|
||||
// MARK: -- should decode a basic dict
|
||||
it("should decode a basic dict") {
|
||||
let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)!
|
||||
let result = try? Bencode.decode([String: [String]].self, from: basicDictData)
|
||||
|
||||
expect(result).to(equal(["spam": ["a", "b"]]))
|
||||
}
|
||||
|
||||
// MARK: -- decodes a decodable type
|
||||
it("decodes a decodable type") {
|
||||
let data: Data = "d8:intValuei100e11:stringValue4:Test".data(using: .utf8)!
|
||||
let result: TestType? = try? Bencode.decode(TestType.self, from: data)
|
||||
|
||||
expect(result).to(equal(TestType(intValue: 100, stringValue: "Test")))
|
||||
}
|
||||
|
||||
// MARK: -- decodes a stringified decodable type
|
||||
it("decodes a stringified decodable type") {
|
||||
let data: Data = "37:{\"intValue\":100,\"stringValue\":\"Test\"}".data(using: .utf8)!
|
||||
let result: TestType? = try? Bencode.decode(TestType.self, from: data)
|
||||
|
||||
expect(result).to(equal(TestType(intValue: 100, stringValue: "Test")))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - when decoding a response
|
||||
context("when decoding a response") {
|
||||
it("decodes successfully") {
|
||||
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
|
||||
.data(using: .utf8)!
|
||||
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
||||
// MARK: -- with a decodable type
|
||||
context("with a decodable type") {
|
||||
// MARK: ---- decodes successfully
|
||||
it("decodes successfully") {
|
||||
let data: Data = "ld8:intValuei100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
|
||||
.data(using: .utf8)!
|
||||
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: TestType(
|
||||
intValue: 100,
|
||||
stringValue: "Test"
|
||||
),
|
||||
data: Data([1, 2, 3, 4, 5])
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: TestType(
|
||||
intValue: 100,
|
||||
stringValue: "Test"
|
||||
),
|
||||
data: Data([1, 2, 3, 4, 5])
|
||||
)
|
||||
))
|
||||
// MARK: -- decodes successfully with no body
|
||||
it("decodes successfully with no body") {
|
||||
let data: Data = "ld8:intValuei100e11:stringValue4:Teste"
|
||||
.data(using: .utf8)!
|
||||
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: TestType(
|
||||
intValue: 100,
|
||||
stringValue: "Test"
|
||||
),
|
||||
data: nil
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// MARK: ---- throws a parsing error when given an invalid length
|
||||
it("throws a parsing error when given an invalid length") {
|
||||
let data: Data = "ld12:intValuei100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
|
||||
.data(using: .utf8)!
|
||||
|
||||
expect {
|
||||
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
|
||||
_ = result
|
||||
}.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
|
||||
// MARK: ---- throws a parsing error when given an invalid key
|
||||
it("throws a parsing error when given an invalid key") {
|
||||
let data: Data = "ld7:INVALIDi100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
|
||||
.data(using: .utf8)!
|
||||
|
||||
expect {
|
||||
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
|
||||
_ = result
|
||||
}.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
|
||||
// MARK: ---- decodes correctly when trying to decode an int to a bool with custom handling
|
||||
it("decodes correctly when trying to decode an int to a bool with custom handling") {
|
||||
let data: Data = "ld9:boolValuei1e11:stringValue4:testee"
|
||||
.data(using: .utf8)!
|
||||
|
||||
expect {
|
||||
let result: BencodeResponse<TestType3> = try Bencode.decodeResponse(from: data)
|
||||
_ = result
|
||||
}.toNot(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
|
||||
// MARK: ---- throws a parsing error when trying to decode an int to a bool
|
||||
it("throws a parsing error when trying to decode an int to a bool") {
|
||||
let data: Data = "ld9:boolValuei1e11:stringValue4:testee"
|
||||
.data(using: .utf8)!
|
||||
|
||||
expect {
|
||||
let result: BencodeResponse<TestType2> = try Bencode.decodeResponse(from: data)
|
||||
_ = result
|
||||
}.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
}
|
||||
|
||||
it("decodes successfully with no body") {
|
||||
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e"
|
||||
.data(using: .utf8)!
|
||||
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
||||
// MARK: -- with stringified json info
|
||||
context("with stringified json info") {
|
||||
// MARK: -- decodes successfully
|
||||
it("decodes successfully") {
|
||||
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
|
||||
.data(using: .utf8)!
|
||||
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: TestType(
|
||||
intValue: 100,
|
||||
stringValue: "Test"
|
||||
),
|
||||
data: Data([1, 2, 3, 4, 5])
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: TestType(
|
||||
intValue: 100,
|
||||
stringValue: "Test"
|
||||
),
|
||||
data: nil
|
||||
)
|
||||
))
|
||||
// MARK: -- decodes successfully with no body
|
||||
it("decodes successfully with no body") {
|
||||
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e"
|
||||
.data(using: .utf8)!
|
||||
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: TestType(
|
||||
intValue: 100,
|
||||
stringValue: "Test"
|
||||
),
|
||||
data: nil
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// MARK: -- throws a parsing error when invalid
|
||||
it("throws a parsing error when invalid") {
|
||||
let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
|
||||
.data(using: .utf8)!
|
||||
|
||||
expect {
|
||||
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
|
||||
_ = result
|
||||
}.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
}
|
||||
|
||||
it("throws a parsing error when invalid") {
|
||||
let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
|
||||
.data(using: .utf8)!
|
||||
|
||||
expect {
|
||||
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
|
||||
_ = result
|
||||
}.to(throwError(HTTPError.parsingFailed))
|
||||
// MARK: -- with a string value
|
||||
context("with a string value") {
|
||||
// MARK: ---- decodes successfully
|
||||
it("decodes successfully") {
|
||||
let data: Data = "l4:Test5:\u{01}\u{02}\u{03}\u{04}\u{05}e".data(using: .utf8)!
|
||||
let result: BencodeResponse<String>? = try? Bencode.decodeResponse(from: data)
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: "Test",
|
||||
data: Data([1, 2, 3, 4, 5])
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// MARK: ---- decodes successfully with no body
|
||||
it("decodes successfully with no body") {
|
||||
let data: Data = "l4:Teste".data(using: .utf8)!
|
||||
let result: BencodeResponse<String>? = try? Bencode.decodeResponse(from: data)
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: "Test",
|
||||
data: nil
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// MARK: ---- throws a parsing error when invalid
|
||||
it("throws a parsing error when invalid") {
|
||||
let data: Data = "l10:Teste".data(using: .utf8)!
|
||||
|
||||
expect {
|
||||
let result: BencodeResponse<String> = try Bencode.decodeResponse(from: data)
|
||||
_ = result
|
||||
}.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -- with an int value
|
||||
context("with an int value") {
|
||||
// MARK: ---- decodes successfully
|
||||
it("decodes successfully") {
|
||||
let data: Data = "li100e5:\u{01}\u{02}\u{03}\u{04}\u{05}e".data(using: .utf8)!
|
||||
let result: BencodeResponse<Int>? = try? Bencode.decodeResponse(from: data)
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: 100,
|
||||
data: Data([1, 2, 3, 4, 5])
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// MARK: ---- decodes successfully with no body
|
||||
it("decodes successfully with no body") {
|
||||
let data: Data = "li100ee".data(using: .utf8)!
|
||||
let result: BencodeResponse<Int>? = try? Bencode.decodeResponse(from: data)
|
||||
|
||||
expect(result)
|
||||
.to(equal(
|
||||
BencodeResponse(
|
||||
info: 100,
|
||||
data: nil
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// MARK: ---- throws a parsing error when invalid
|
||||
it("throws a parsing error when invalid") {
|
||||
let data: Data = "l4:Teste".data(using: .utf8)!
|
||||
|
||||
expect {
|
||||
let result: BencodeResponse<Int> = try Bencode.decodeResponse(from: data)
|
||||
_ = result
|
||||
}.to(throwError(HTTPError.parsingFailed))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,34 @@ public enum AppSetup {
|
|||
private static let hasRun: Atomic<Bool> = Atomic(false)
|
||||
|
||||
public static func setupEnvironment(
|
||||
retrySetupIfDatabaseInvalid: Bool = false,
|
||||
appSpecificBlock: @escaping () -> (),
|
||||
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
||||
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> ()
|
||||
) {
|
||||
guard !AppSetup.hasRun.wrappedValue else { return }
|
||||
// If we've already run the app setup then only continue under certain circumstances
|
||||
guard !AppSetup.hasRun.wrappedValue else {
|
||||
let storageIsValid: Bool = Storage.shared.isValid
|
||||
|
||||
switch (retrySetupIfDatabaseInvalid, storageIsValid) {
|
||||
case (true, false):
|
||||
Storage.reconfigureDatabase()
|
||||
AppSetup.hasRun.mutate { $0 = false }
|
||||
AppSetup.setupEnvironment(
|
||||
retrySetupIfDatabaseInvalid: false, // Don't want to get stuck in a loop
|
||||
appSpecificBlock: appSpecificBlock,
|
||||
migrationProgressChanged: migrationProgressChanged,
|
||||
migrationsCompletion: migrationsCompletion
|
||||
)
|
||||
|
||||
default:
|
||||
migrationsCompletion(
|
||||
(storageIsValid ? .success(()) : .failure(StorageError.startupFailed)),
|
||||
false
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
AppSetup.hasRun.mutate { $0 = true }
|
||||
|
||||
|
@ -64,14 +87,6 @@ public enum AppSetup {
|
|||
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
||||
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> ()
|
||||
) {
|
||||
// If the database can't be initialised into a valid state then error
|
||||
guard Storage.shared.isValid else {
|
||||
DispatchQueue.main.async {
|
||||
migrationsCompletion(Result.failure(StorageError.databaseInvalid), false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function))
|
||||
|
||||
Storage.shared.perform(
|
||||
|
|
Loading…
Reference in New Issue