commit
3cbe749d3c
|
@ -740,6 +740,7 @@
|
||||||
FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; };
|
FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; };
|
||||||
FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; };
|
FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; };
|
||||||
FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.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 */; };
|
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; };
|
||||||
FD9BDE002A5D22B7005F1EBC /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; };
|
FD9BDE002A5D22B7005F1EBC /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; };
|
||||||
FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2408,6 +2410,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
C31C219B255BC92200EC2D66 /* Meta */,
|
C31C219B255BC92200EC2D66 /* Meta */,
|
||||||
|
FD9AECA42AAA9609009B3406 /* NotificationError.swift */,
|
||||||
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */,
|
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */,
|
||||||
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */,
|
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */,
|
||||||
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */,
|
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */,
|
||||||
|
@ -5454,6 +5457,7 @@
|
||||||
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */,
|
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */,
|
||||||
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */,
|
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */,
|
||||||
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */,
|
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */,
|
||||||
|
FD9AECA52AAA9609009B3406 /* NotificationError.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -6445,7 +6449,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 422;
|
CURRENT_PROJECT_VERSION = 424;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
|
@ -6469,7 +6473,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.4.0;
|
MARKETING_VERSION = 2.4.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -6517,7 +6521,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 422;
|
CURRENT_PROJECT_VERSION = 424;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
@ -6546,7 +6550,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.4.0;
|
MARKETING_VERSION = 2.4.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -6582,7 +6586,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 422;
|
CURRENT_PROJECT_VERSION = 424;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
|
@ -6605,7 +6609,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.4.0;
|
MARKETING_VERSION = 2.4.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||||
|
@ -6656,7 +6660,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 422;
|
CURRENT_PROJECT_VERSION = 424;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
@ -6684,7 +6688,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 2.4.0;
|
MARKETING_VERSION = 2.4.1;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
|
||||||
|
@ -7616,7 +7620,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CURRENT_PROJECT_VERSION = 422;
|
CURRENT_PROJECT_VERSION = 424;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -7654,7 +7658,7 @@
|
||||||
"$(SRCROOT)",
|
"$(SRCROOT)",
|
||||||
);
|
);
|
||||||
LLVM_LTO = NO;
|
LLVM_LTO = NO;
|
||||||
MARKETING_VERSION = 2.4.0;
|
MARKETING_VERSION = 2.4.1;
|
||||||
OTHER_LDFLAGS = "$(inherited)";
|
OTHER_LDFLAGS = "$(inherited)";
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||||
|
@ -7687,7 +7691,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CURRENT_PROJECT_VERSION = 422;
|
CURRENT_PROJECT_VERSION = 424;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -7725,7 +7729,7 @@
|
||||||
"$(SRCROOT)",
|
"$(SRCROOT)",
|
||||||
);
|
);
|
||||||
LLVM_LTO = NO;
|
LLVM_LTO = NO;
|
||||||
MARKETING_VERSION = 2.4.0;
|
MARKETING_VERSION = 2.4.1;
|
||||||
OTHER_LDFLAGS = "$(inherited)";
|
OTHER_LDFLAGS = "$(inherited)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||||
PRODUCT_NAME = Session;
|
PRODUCT_NAME = Session;
|
||||||
|
|
|
@ -93,7 +93,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
||||||
|
|
||||||
public func reportOutgoingCall(_ call: SessionCall) {
|
public func reportOutgoingCall(_ call: SessionCall) {
|
||||||
AssertIsOnMainThread()
|
AssertIsOnMainThread()
|
||||||
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
|
UserDefaults.sharedLokiProject?[.isCallOngoing] = true
|
||||||
|
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
|
||||||
|
|
||||||
call.stateDidChange = {
|
call.stateDidChange = {
|
||||||
if call.hasStartedConnecting {
|
if call.hasStartedConnecting {
|
||||||
|
@ -123,7 +124,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
||||||
completion(error)
|
completion(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
|
UserDefaults.sharedLokiProject?[.isCallOngoing] = true
|
||||||
|
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
|
||||||
completion(nil)
|
completion(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +140,9 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
|
||||||
|
|
||||||
func handleCallEnded() {
|
func handleCallEnded() {
|
||||||
WebRTCSession.current = nil
|
WebRTCSession.current = nil
|
||||||
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
|
UserDefaults.sharedLokiProject?[.isCallOngoing] = false
|
||||||
|
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil
|
||||||
|
|
||||||
if CurrentAppContext().isInBackground() {
|
if CurrentAppContext().isInBackground() {
|
||||||
(UIApplication.shared.delegate as? AppDelegate)?.stopPollers()
|
(UIApplication.shared.delegate as? AppDelegate)?.stopPollers()
|
||||||
DDLog.flushLog()
|
DDLog.flushLog()
|
||||||
|
|
|
@ -1951,14 +1951,22 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
.sorted()
|
.sorted()
|
||||||
.filter({ $0.section == messagesSection })
|
.filter({ $0.section == messagesSection })
|
||||||
.compactMap({ indexPath -> (frame: CGRect, cellViewModel: MessageViewModel)? in
|
.compactMap({ indexPath -> (frame: CGRect, cellViewModel: MessageViewModel)? in
|
||||||
guard let cell: VisibleMessageCell = tableView.cellForRow(at: indexPath) as? VisibleMessageCell else {
|
guard let cell: UITableViewCell = tableView.cellForRow(at: indexPath) else { return nil }
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
switch cell {
|
||||||
view.convert(cell.frame, from: tableView),
|
case is VisibleMessageCell, is CallMessageCell, is InfoMessageCell:
|
||||||
self.viewModel.interactionData[indexPath.section].elements[indexPath.row]
|
return (
|
||||||
)
|
view.convert(cell.frame, from: tableView),
|
||||||
|
self.viewModel.interactionData[indexPath.section].elements[indexPath.row]
|
||||||
|
)
|
||||||
|
|
||||||
|
case is TypingIndicatorCell, is DateHeaderCell, is UnreadMarkerCell:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
SNLog("[ConversationVC] Warning: Processing unhandled cell type when marking as read, this could result in intermittent failures")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// Exclude messages that are partially off the bottom of the screen
|
// Exclude messages that are partially off the bottom of the screen
|
||||||
.filter({ $0.frame.maxY <= tableVisualBottom })
|
.filter({ $0.frame.maxY <= tableVisualBottom })
|
||||||
|
|
|
@ -119,8 +119,7 @@ final class QuoteView: UIView {
|
||||||
// Content view
|
// Content view
|
||||||
let contentView = UIView()
|
let contentView = UIView()
|
||||||
addSubview(contentView)
|
addSubview(contentView)
|
||||||
contentView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: self)
|
contentView.pin(to: self)
|
||||||
contentView.rightAnchor.constraint(lessThanOrEqualTo: self.rightAnchor).isActive = true
|
|
||||||
|
|
||||||
if let attachment: Attachment = attachment {
|
if let attachment: Attachment = attachment {
|
||||||
let isAudio: Bool = MIMETypeUtil.isAudio(attachment.contentType)
|
let isAudio: Bool = MIMETypeUtil.isAudio(attachment.contentType)
|
||||||
|
|
|
@ -383,6 +383,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
|
||||||
|
|
||||||
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) {
|
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) {
|
||||||
photoCollectionContents = photoCollection.contents()
|
photoCollectionContents = photoCollection.contents()
|
||||||
|
collectionView?.reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - PhotoCollectionPicker Presentation
|
// MARK: - PhotoCollectionPicker Presentation
|
||||||
|
|
|
@ -469,8 +469,17 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
||||||
|
|
||||||
// If the screen wasn't presented or it was presented from a location which isn't the
|
// If the screen wasn't presented or it was presented from a location which isn't the
|
||||||
// MediaTileViewController then just pop/dismiss the screen
|
// MediaTileViewController then just pop/dismiss the screen
|
||||||
|
let parentNavController: UINavigationController? = {
|
||||||
|
switch self.presentingViewController {
|
||||||
|
case let topBannerController as TopBannerController:
|
||||||
|
return topBannerController.children.first as? UINavigationController
|
||||||
|
|
||||||
|
default: return self.presentingViewController as? UINavigationController
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let presentingNavController: UINavigationController = (self.presentingViewController as? UINavigationController),
|
let presentingNavController: UINavigationController = parentNavController,
|
||||||
!(presentingNavController.viewControllers.last is AllMediaViewController)
|
!(presentingNavController.viewControllers.last is AllMediaViewController)
|
||||||
else {
|
else {
|
||||||
guard self.navigationController?.viewControllers.count == 1 else {
|
guard self.navigationController?.viewControllers.count == 1 else {
|
||||||
|
|
|
@ -89,7 +89,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
)
|
)
|
||||||
|
|
||||||
if Environment.shared?.callManager.wrappedValue?.currentCall == nil {
|
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
|
// 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
|
// 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
|
// resolve it (most likely the database is locked or the key was somehow lost - safer to get them
|
||||||
// to restart and manually reinstall/restore)
|
// 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
|
// Offer the 'Restore' option if it was a migration error
|
||||||
case .databaseError:
|
case .databaseError:
|
||||||
|
@ -663,39 +664,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
/// we don't block user interaction while it's running
|
/// we don't block user interaction while it's running
|
||||||
DispatchQueue.global(qos: .default).async {
|
DispatchQueue.global(qos: .default).async {
|
||||||
let unreadCount: Int = Storage.shared
|
let unreadCount: Int = Storage.shared
|
||||||
.read { db in
|
.read { db in try Interaction.fetchUnreadCount(db) }
|
||||||
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)
|
|
||||||
}
|
|
||||||
.defaulting(to: 0)
|
.defaulting(to: 0)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
CurrentAppContext().setMainAppBadgeNumber(unreadCount)
|
UIApplication.shared.applicationIconBadgeNumber = unreadCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -904,7 +877,9 @@ private enum StartupError: Error {
|
||||||
|
|
||||||
var name: String {
|
var name: String {
|
||||||
switch self {
|
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 .failedToRestore: return "Failed to restore"
|
||||||
case .databaseError: return "Database error"
|
case .databaseError: return "Database error"
|
||||||
case .startupTimeout: return "Startup timeout"
|
case .startupTimeout: return "Startup timeout"
|
||||||
|
@ -913,7 +888,9 @@ private enum StartupError: Error {
|
||||||
|
|
||||||
var message: String {
|
var message: String {
|
||||||
switch self {
|
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 .failedToRestore: return "DATABASE_RESTORE_FAILED".localized()
|
||||||
case .databaseError: return "DATABASE_MIGRATION_FAILED".localized()
|
case .databaseError: return "DATABASE_MIGRATION_FAILED".localized()
|
||||||
case .startupTimeout: return "APP_STARTUP_TIMEOUT".localized()
|
case .startupTimeout: return "APP_STARTUP_TIMEOUT".localized()
|
||||||
|
|
|
@ -179,11 +179,6 @@ final class MainAppContext: NSObject, AppContext {
|
||||||
UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking
|
UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMainAppBadgeNumber(_ value: Int) {
|
|
||||||
UIApplication.shared.applicationIconBadgeNumber = value
|
|
||||||
UserDefaults.sharedLokiProject?.setValue(value, forKey: "currentBadgeNumber")
|
|
||||||
}
|
|
||||||
|
|
||||||
func frontmostViewController() -> UIViewController? {
|
func frontmostViewController() -> UIViewController? {
|
||||||
UIApplication.shared.frontmostViewControllerIgnoringAlerts
|
UIApplication.shared.frontmostViewControllerIgnoringAlerts
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,7 +240,7 @@ final class NukeDataModal: Modal {
|
||||||
|
|
||||||
// Clear the app badge and notifications
|
// Clear the app badge and notifications
|
||||||
AppEnvironment.shared.notificationPresenter.clearAllNotifications()
|
AppEnvironment.shared.notificationPresenter.clearAllNotifications()
|
||||||
CurrentAppContext().setMainAppBadgeNumber(0)
|
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||||
|
|
||||||
// Clear out the user defaults
|
// Clear out the user defaults
|
||||||
UserDefaults.removeAll()
|
UserDefaults.removeAll()
|
||||||
|
|
|
@ -465,6 +465,46 @@ public extension Interaction {
|
||||||
// MARK: - GRDB Interactions
|
// MARK: - GRDB Interactions
|
||||||
|
|
||||||
public extension Interaction {
|
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
|
/// This will update the `wasRead` state the the interaction
|
||||||
///
|
///
|
||||||
/// - Parameters
|
/// - Parameters
|
||||||
|
@ -482,83 +522,16 @@ public extension Interaction {
|
||||||
) throws {
|
) throws {
|
||||||
guard let interactionId: Int64 = interactionId else { return }
|
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
|
// 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
|
// 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)
|
.select(.id, .variant, .timestampMs, .wasRead)
|
||||||
.filter(id: interactionId)
|
.filter(id: interactionId)
|
||||||
.asRequest(of: InteractionReadInfo.self)
|
.asRequest(of: Interaction.ReadInfo.self)
|
||||||
.fetchOne(db)
|
.fetchOne(db)
|
||||||
|
|
||||||
// If we aren't including older interactions then update and save the current one
|
// 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
|
// Only mark as read and trigger the subsequent jobs if the interaction is
|
||||||
// actually not read (no point updating and triggering db changes otherwise)
|
// actually not read (no point updating and triggering db changes otherwise)
|
||||||
guard
|
guard
|
||||||
|
@ -575,19 +548,21 @@ public extension Interaction {
|
||||||
.filter(id: interactionId)
|
.filter(id: interactionId)
|
||||||
.updateAll(db, Columns.wasRead.set(to: true))
|
.updateAll(db, Columns.wasRead.set(to: true))
|
||||||
|
|
||||||
try scheduleJobs(
|
try Interaction.scheduleReadJobs(
|
||||||
db,
|
db,
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
threadVariant: threadVariant,
|
threadVariant: threadVariant,
|
||||||
interactionInfo: [
|
interactionInfo: [
|
||||||
InteractionReadInfo(
|
Interaction.ReadInfo(
|
||||||
id: interactionId,
|
id: interactionId,
|
||||||
variant: variant,
|
variant: variant,
|
||||||
timestampMs: 0,
|
timestampMs: 0,
|
||||||
wasRead: false
|
wasRead: false
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
lastReadTimestampMs: timestampMs
|
lastReadTimestampMs: timestampMs,
|
||||||
|
trySendReadReceipt: trySendReadReceipt,
|
||||||
|
calledFromConfigHandling: false
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -596,21 +571,23 @@ public extension Interaction {
|
||||||
.filter(Interaction.Columns.threadId == threadId)
|
.filter(Interaction.Columns.threadId == threadId)
|
||||||
.filter(Interaction.Columns.timestampMs <= interactionInfo.timestampMs)
|
.filter(Interaction.Columns.timestampMs <= interactionInfo.timestampMs)
|
||||||
.filter(Interaction.Columns.wasRead == false)
|
.filter(Interaction.Columns.wasRead == false)
|
||||||
let interactionInfoToMarkAsRead: [InteractionReadInfo] = try interactionQuery
|
let interactionInfoToMarkAsRead: [Interaction.ReadInfo] = try interactionQuery
|
||||||
.select(.id, .variant, .timestampMs, .wasRead)
|
.select(.id, .variant, .timestampMs, .wasRead)
|
||||||
.asRequest(of: InteractionReadInfo.self)
|
.asRequest(of: Interaction.ReadInfo.self)
|
||||||
.fetchAll(db)
|
.fetchAll(db)
|
||||||
|
|
||||||
// If there are no other interactions to mark as read then just schedule the jobs
|
// 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
|
// for this interaction (need to ensure the disapeparing messages run for sync'ed
|
||||||
// outgoing messages which will always have 'wasRead' as false)
|
// outgoing messages which will always have 'wasRead' as false)
|
||||||
guard !interactionInfoToMarkAsRead.isEmpty else {
|
guard !interactionInfoToMarkAsRead.isEmpty else {
|
||||||
try scheduleJobs(
|
try Interaction.scheduleReadJobs(
|
||||||
db,
|
db,
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
threadVariant: threadVariant,
|
threadVariant: threadVariant,
|
||||||
interactionInfo: [interactionInfo],
|
interactionInfo: [interactionInfo],
|
||||||
lastReadTimestampMs: interactionInfo.timestampMs
|
lastReadTimestampMs: interactionInfo.timestampMs,
|
||||||
|
trySendReadReceipt: trySendReadReceipt,
|
||||||
|
calledFromConfigHandling: false
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -619,12 +596,14 @@ public extension Interaction {
|
||||||
try interactionQuery.updateAll(db, Columns.wasRead.set(to: true))
|
try interactionQuery.updateAll(db, Columns.wasRead.set(to: true))
|
||||||
|
|
||||||
// Retrieve the interaction ids we want to update
|
// Retrieve the interaction ids we want to update
|
||||||
try scheduleJobs(
|
try Interaction.scheduleReadJobs(
|
||||||
db,
|
db,
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
threadVariant: threadVariant,
|
threadVariant: threadVariant,
|
||||||
interactionInfo: interactionInfoToMarkAsRead,
|
interactionInfo: interactionInfoToMarkAsRead,
|
||||||
lastReadTimestampMs: interactionInfo.timestampMs
|
lastReadTimestampMs: interactionInfo.timestampMs,
|
||||||
|
trySendReadReceipt: trySendReadReceipt,
|
||||||
|
calledFromConfigHandling: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -691,6 +670,71 @@ public extension Interaction {
|
||||||
.asSet()
|
.asSet()
|
||||||
.subtracting(timestampsUpdated)
|
.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
|
// MARK: - Search Queries
|
||||||
|
|
|
@ -41,7 +41,7 @@ extension PushNotificationAPI.NotificationMetadata {
|
||||||
hash: try container.decode(String.self, forKey: .hash),
|
hash: try container.decode(String.self, forKey: .hash),
|
||||||
namespace: try container.decode(Int.self, forKey: .namespace),
|
namespace: try container.decode(Int.self, forKey: .namespace),
|
||||||
dataLength: try container.decode(Int.self, forKey: .dataLength),
|
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 currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
|
||||||
let request: SubscribeRequest = SubscribeRequest(
|
let request: SubscribeRequest = SubscribeRequest(
|
||||||
pubkey: currentUserPublicKey,
|
pubkey: currentUserPublicKey,
|
||||||
namespaces: [.default],
|
namespaces: [.default, .configConvoInfoVolatile],
|
||||||
// Note: Unfortunately we always need the message content because without the content
|
// 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
|
// control messages can't be distinguished from visible messages which results in the
|
||||||
// 'generic' notification being shown when receiving things like typing indicator updates
|
// 'generic' notification being shown when receiving things like typing indicator updates
|
||||||
|
@ -372,8 +372,11 @@ public enum PushNotificationAPI {
|
||||||
return (envelope, .legacySuccess)
|
return (envelope, .legacySuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String else {
|
||||||
|
return (nil, .failureNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String,
|
|
||||||
let encData: Data = Data(base64Encoded: base64EncodedEncString),
|
let encData: Data = Data(base64Encoded: base64EncodedEncString),
|
||||||
let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies),
|
let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies),
|
||||||
encData.count > dependencies.crypto.size(.aeadXChaCha20NonceBytes)
|
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
|
// If the metadata says that the message was too large then we should show the generic
|
||||||
// notification (this is a valid case)
|
// 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
|
// Check that the body we were given is valid
|
||||||
guard
|
guard
|
||||||
|
|
|
@ -5,7 +5,9 @@ import Foundation
|
||||||
public extension PushNotificationAPI {
|
public extension PushNotificationAPI {
|
||||||
enum ProcessResult {
|
enum ProcessResult {
|
||||||
case success
|
case success
|
||||||
|
case successTooLong
|
||||||
case failure
|
case failure
|
||||||
|
case failureNoContent
|
||||||
case legacySuccess
|
case legacySuccess
|
||||||
case legacyFailure
|
case legacyFailure
|
||||||
case legacyForceSilent
|
case legacyForceSilent
|
||||||
|
|
|
@ -77,16 +77,28 @@ internal extension SessionUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark all older interactions as read
|
// Mark all older interactions as read
|
||||||
try Interaction
|
let interactionQuery = Interaction
|
||||||
.filter(
|
.filter(Interaction.Columns.threadId == threadId)
|
||||||
Interaction.Columns.threadId == threadId &&
|
.filter(Interaction.Columns.timestampMs <= lastReadTimestampMs)
|
||||||
Interaction.Columns.timestampMs <= lastReadTimestampMs &&
|
.filter(Interaction.Columns.wasRead == false)
|
||||||
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`
|
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||||
db,
|
db,
|
||||||
Interaction.Columns.wasRead.set(to: true)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,11 +56,9 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
notificationContent.sound = thread.notificationSound
|
notificationContent.sound = thread.notificationSound
|
||||||
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
|
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
|
||||||
.notificationSound(isQuiet: false)
|
.notificationSound(isQuiet: false)
|
||||||
|
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
|
||||||
// Badge Number
|
.map { NSNumber(value: $0) }
|
||||||
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
.defaulting(to: NSNumber(value: 0))
|
||||||
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
|
||||||
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
|
||||||
|
|
||||||
// Title & body
|
// Title & body
|
||||||
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||||
|
@ -157,16 +155,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
|
||||||
let notificationContent = UNMutableNotificationContent()
|
let notificationContent = UNMutableNotificationContent()
|
||||||
notificationContent.userInfo = userInfo
|
notificationContent.userInfo = userInfo
|
||||||
notificationContent.sound = thread.notificationSound
|
notificationContent.sound = thread.notificationSound
|
||||||
.defaulting(
|
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
|
||||||
to: db[.defaultNotificationSound]
|
|
||||||
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
|
||||||
)
|
|
||||||
.notificationSound(isQuiet: false)
|
.notificationSound(isQuiet: false)
|
||||||
|
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
|
||||||
// Badge Number
|
.map { NSNumber(value: $0) }
|
||||||
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
.defaulting(to: NSNumber(value: 0))
|
||||||
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
|
||||||
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
|
||||||
|
|
||||||
notificationContent.title = "Session"
|
notificationContent.title = "Session"
|
||||||
notificationContent.body = ""
|
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 didPerformSetup = false
|
||||||
private var contentHandler: ((UNNotificationContent) -> Void)?
|
private var contentHandler: ((UNNotificationContent) -> Void)?
|
||||||
private var request: UNNotificationRequest?
|
private var request: UNNotificationRequest?
|
||||||
|
private var openGroupPollCancellable: AnyCancellable?
|
||||||
|
|
||||||
public static let isFromRemoteKey = "remote"
|
public static let isFromRemoteKey = "remote"
|
||||||
public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId"
|
public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId"
|
||||||
public static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw"
|
public static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw"
|
||||||
public static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter"
|
public static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter"
|
||||||
|
private static let callPreOfferLargeNotificationSupressionDuration: TimeInterval = 30
|
||||||
|
|
||||||
// MARK: Did receive a remote push notification request
|
// MARK: Did receive a remote push notification request
|
||||||
|
|
||||||
|
@ -43,6 +45,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
|
|
||||||
let isCallOngoing: Bool = (UserDefaults.sharedLokiProject?[.isCallOngoing])
|
let isCallOngoing: Bool = (UserDefaults.sharedLokiProject?[.isCallOngoing])
|
||||||
.defaulting(to: false)
|
.defaulting(to: false)
|
||||||
|
let lastCallPreOffer: Date? = UserDefaults.sharedLokiProject?[.lastCallPreOffer]
|
||||||
|
|
||||||
// Perform main setup
|
// Perform main setup
|
||||||
Storage.resumeDatabaseAccess()
|
Storage.resumeDatabaseAccess()
|
||||||
|
@ -52,14 +55,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
||||||
let openGroupPollingPublishers: [AnyPublisher<Void, Error>] = self.pollForOpenGroups()
|
let openGroupPollingPublishers: [AnyPublisher<Void, Error>] = self.pollForOpenGroups()
|
||||||
defer {
|
defer {
|
||||||
Publishers
|
self.openGroupPollCancellable = Publishers
|
||||||
.MergeMany(openGroupPollingPublishers)
|
.MergeMany(openGroupPollingPublishers)
|
||||||
.subscribe(on: DispatchQueue.global(qos: .background))
|
.subscribe(on: DispatchQueue.global(qos: .background))
|
||||||
.subscribe(on: DispatchQueue.main)
|
.subscribe(on: DispatchQueue.main)
|
||||||
.sinkUntilComplete(
|
.sink(
|
||||||
receiveCompletion: { _ in
|
receiveCompletion: { [weak self] _ in self?.completeSilenty() },
|
||||||
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
|
// If we got an explicit failure, or we got a success but no content then show
|
||||||
// the fallback notification
|
// the fallback notification
|
||||||
case .success, .legacySuccess, .failure, .legacyFailure:
|
case .success, .legacySuccess, .failure, .legacyFailure:
|
||||||
return self.handleFailure(for: notificationContent)
|
return self.handleFailure(for: notificationContent, error: .processing(result))
|
||||||
|
|
||||||
case .legacyForceSilent: return
|
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
|
Storage.shared.write { db in
|
||||||
do {
|
do {
|
||||||
guard let processedMessage: ProcessedMessage = try Message.processRawReceivedMessageAsNotification(db, envelope: envelope) else {
|
guard let processedMessage: ProcessedMessage = try Message.processRawReceivedMessageAsNotification(db, envelope: envelope) else {
|
||||||
self.handleFailure(for: notificationContent)
|
self.handleFailure(for: notificationContent, error: .messageProcessing)
|
||||||
return
|
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
|
// Throw if the message is outdated and shouldn't be processed
|
||||||
try MessageReceiver.throwIfMessageOutdated(
|
try MessageReceiver.throwIfMessageOutdated(
|
||||||
db,
|
db,
|
||||||
|
@ -100,47 +130,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
)
|
)
|
||||||
|
|
||||||
switch processedMessage.messageInfo.message {
|
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:
|
case let callMessage as CallMessage:
|
||||||
try MessageReceiver.handleCallMessage(
|
try MessageReceiver.handleCallMessage(
|
||||||
db,
|
db,
|
||||||
|
@ -187,6 +176,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try MessageReceiver.insertCallInfoMessage(db, for: callMessage)
|
||||||
self.handleSuccessForIncomingCall(db, for: callMessage)
|
self.handleSuccessForIncomingCall(db, for: callMessage)
|
||||||
|
|
||||||
case let sharedConfigMessage as SharedConfigMessage:
|
case let sharedConfigMessage as SharedConfigMessage:
|
||||||
|
@ -210,7 +200,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
if let error = error as? MessageReceiverError, error.isRetryable {
|
if let error = error as? MessageReceiverError, error.isRetryable {
|
||||||
switch error {
|
switch error {
|
||||||
case .invalidGroupPublicKey, .noGroupKeyPair, .outdatedMessage: self.completeSilenty()
|
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()
|
Cryptography.seedRandom()
|
||||||
|
|
||||||
AppSetup.setupEnvironment(
|
AppSetup.setupEnvironment(
|
||||||
|
retrySetupIfDatabaseInvalid: true,
|
||||||
appSpecificBlock: {
|
appSpecificBlock: {
|
||||||
Environment.shared?.notificationsManager.mutate {
|
Environment.shared?.notificationsManager.mutate {
|
||||||
$0 = NSENotificationPresenter()
|
$0 = NSENotificationPresenter()
|
||||||
|
@ -307,14 +298,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
override public func serviceExtensionTimeWillExpire() {
|
override public func serviceExtensionTimeWillExpire() {
|
||||||
// Called just before the extension will be terminated by the system.
|
// 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.
|
// 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()
|
completeSilenty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func completeSilenty() {
|
private func completeSilenty() {
|
||||||
NSLog("[NotificationServiceExtension] Complete silently")
|
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()
|
Storage.suspendDatabaseAccess()
|
||||||
|
|
||||||
self.contentHandler!(.init())
|
self.contentHandler!(silentContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleSuccessForIncomingCall(_ db: Database, for callMessage: CallMessage) {
|
private func handleSuccessForIncomingCall(_ db: Database, for callMessage: CallMessage) {
|
||||||
|
@ -330,11 +328,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in
|
CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in
|
||||||
if let error = error {
|
if let error = error {
|
||||||
self.handleFailureForVoIP(db, for: callMessage)
|
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 {
|
else {
|
||||||
|
NSLog("[NotificationServiceExtension] Successfully notified main app of call message.")
|
||||||
|
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
|
||||||
self.completeSilenty()
|
self.completeSilenty()
|
||||||
SNLog("Successfully notified main app of call message.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,11 +346,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
|
||||||
let notificationContent = UNMutableNotificationContent()
|
let notificationContent = UNMutableNotificationContent()
|
||||||
notificationContent.userInfo = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
notificationContent.userInfo = [ NotificationServiceExtension.isFromRemoteKey : true ]
|
||||||
notificationContent.title = "Session"
|
notificationContent.title = "Session"
|
||||||
|
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
|
||||||
// Badge Number
|
.map { NSNumber(value: $0) }
|
||||||
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
|
.defaulting(to: NSNumber(value: 0))
|
||||||
notificationContent.badge = NSNumber(value: newBadgeNumber)
|
|
||||||
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
|
|
||||||
|
|
||||||
if let sender: String = callMessage.sender {
|
if let sender: String = callMessage.sender {
|
||||||
let senderDisplayName: String = Profile.displayName(db, id: sender, threadVariant: .contact)
|
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
|
UNUserNotificationCenter.current().add(request) { error in
|
||||||
if let error = error {
|
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.signal()
|
||||||
}
|
}
|
||||||
semaphore.wait()
|
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()
|
Storage.suspendDatabaseAccess()
|
||||||
|
|
||||||
content.body = "You've got a new message"
|
|
||||||
content.title = "Session"
|
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
|
content.userInfo = userInfo
|
||||||
contentHandler!(content)
|
contentHandler!(content)
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,6 @@ final class NotificationServiceExtensionContext : NSObject, AppContext {
|
||||||
func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjectsDescription: String) { }
|
func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjectsDescription: String) { }
|
||||||
func frontmostViewController() -> UIViewController? { nil }
|
func frontmostViewController() -> UIViewController? { nil }
|
||||||
func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { }
|
func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { }
|
||||||
func setMainAppBadgeNumber(_ value: Int) { }
|
|
||||||
func setNetworkActivityIndicatorVisible(_ value: Bool) { }
|
func setNetworkActivityIndicatorVisible(_ value: Bool) { }
|
||||||
func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { }
|
func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { }
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,10 +186,6 @@ final class ShareAppExtensionContext: NSObject, AppContext {
|
||||||
OWSLogger.debug("Ignoring request to block sleep.")
|
OWSLogger.debug("Ignoring request to block sleep.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setMainAppBadgeNumber(_ value: Int) {
|
|
||||||
owsFailDebug("")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setNetworkActivityIndicatorVisible(_ value: Bool) {
|
func setNetworkActivityIndicatorVisible(_ value: Bool) {
|
||||||
owsFailDebug("")
|
owsFailDebug("")
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,7 +157,7 @@ open class Storage {
|
||||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
|
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
|
||||||
let error: Error = (startupError ?? StorageError.startupFailed)
|
let error: Error = (startupError ?? StorageError.startupFailed)
|
||||||
SNLog("[Database Error] Statup failed with error: \(error)")
|
SNLog("[Database Error] Statup failed with error: \(error)")
|
||||||
onComplete(.failure(StorageError.startupFailed), false)
|
onComplete(.failure(error), false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -430,12 +430,16 @@ open class Storage {
|
||||||
try? deleteDbKeys()
|
try? deleteDbKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func reconfigureDatabase() {
|
||||||
|
Storage.shared.configureDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
public static func resetForCleanMigration() {
|
public static func resetForCleanMigration() {
|
||||||
// Clear existing content
|
// Clear existing content
|
||||||
resetAllStorage()
|
resetAllStorage()
|
||||||
|
|
||||||
// Reconfigure
|
// Reconfigure
|
||||||
Storage.shared.configureDatabase()
|
reconfigureDatabase()
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func deleteDatabaseFiles() {
|
private static func deleteDatabaseFiles() {
|
||||||
|
|
|
@ -73,9 +73,6 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
|
||||||
// Should be a NOOP if isMainApp is NO.
|
// Should be a NOOP if isMainApp is NO.
|
||||||
- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray<id> *)blockingObjects;
|
- (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;
|
- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated;
|
||||||
|
|
||||||
@property (nonatomic, readonly) CGFloat statusBarHeight;
|
@property (nonatomic, readonly) CGFloat statusBarHeight;
|
||||||
|
|
|
@ -42,6 +42,7 @@ public enum SNUserDefaults {
|
||||||
case lastOpen
|
case lastOpen
|
||||||
case lastGarbageCollection
|
case lastGarbageCollection
|
||||||
case lastPushNotificationSync
|
case lastPushNotificationSync
|
||||||
|
case lastCallPreOffer
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Double: Swift.String {
|
public enum Double: Swift.String {
|
||||||
|
|
|
@ -2,13 +2,6 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol BencodableType {
|
|
||||||
associatedtype ValueType: BencodableType
|
|
||||||
|
|
||||||
static var isCollection: Bool { get }
|
|
||||||
static var isDictionary: Bool { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct BencodeResponse<T: Codable> {
|
public struct BencodeResponse<T: Codable> {
|
||||||
public let info: T
|
public let info: T
|
||||||
public let data: Data?
|
public let data: Data?
|
||||||
|
@ -58,23 +51,61 @@ public enum Bencode {
|
||||||
using dependencies: Dependencies = Dependencies()
|
using dependencies: Dependencies = Dependencies()
|
||||||
) throws -> BencodeResponse<T> where T: Decodable {
|
) throws -> BencodeResponse<T> where T: Decodable {
|
||||||
guard
|
guard
|
||||||
let result: [Data] = try? decode([Data].self, from: data),
|
let decodedData: (value: Any, remainingData: Data) = decodeData(data),
|
||||||
let responseData: Data = result.first
|
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 }
|
else { throw HTTPError.parsingFailed }
|
||||||
|
|
||||||
return BencodeResponse(
|
return BencodeResponse(
|
||||||
info: try responseData.decoded(as: T.self, using: dependencies),
|
info: try Bencode.decode(T.self, decodedValue: resultArray[0], using: dependencies),
|
||||||
data: (result.count > 1 ? result.last : nil)
|
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
|
guard
|
||||||
let decodedData: (value: Any, remainingData: Data) = decodeData(data),
|
let decodedData: (value: Any, remainingData: Data) = decodeData(data),
|
||||||
decodedData.remainingData.isEmpty == true // Ensure there is no left over data
|
decodedData.remainingData.isEmpty == true // Ensure there is no left over data
|
||||||
else { throw HTTPError.parsingFailed }
|
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
|
// MARK: - Logic
|
||||||
|
@ -190,74 +221,12 @@ public enum Bencode {
|
||||||
|
|
||||||
// MARK: - Internal Functions
|
// MARK: - Internal Functions
|
||||||
|
|
||||||
private static func recursiveCast<T: BencodableType>(_ type: T.Type, from value: Any) throws -> T {
|
private static func jsonify(_ value: Any) throws -> Any {
|
||||||
switch (type.isCollection, type.isDictionary) {
|
switch value {
|
||||||
case (_, true):
|
case let arrayValue as [Any]: return try arrayValue.map { try jsonify($0) } as Any
|
||||||
guard let dictValue: [String: Any] = value as? [String: Any] else { throw HTTPError.parsingFailed }
|
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
|
||||||
return try (
|
default: return value
|
||||||
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 }())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
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
|
// MARK: - Spec
|
||||||
|
|
||||||
override func spec() {
|
override func spec() {
|
||||||
describe("Bencode") {
|
describe("Bencode") {
|
||||||
|
// MARK: - when decoding
|
||||||
context("when decoding") {
|
context("when decoding") {
|
||||||
|
// MARK: -- should decode a basic string
|
||||||
it("should decode a basic string") {
|
it("should decode a basic string") {
|
||||||
let basicStringData: Data = "5:howdy".data(using: .utf8)!
|
let basicStringData: Data = "5:howdy".data(using: .utf8)!
|
||||||
let result = try? Bencode.decode(String.self, from: basicStringData)
|
let result = try? Bencode.decode(String.self, from: basicStringData)
|
||||||
|
@ -25,6 +51,7 @@ class BencodeSpec: QuickSpec {
|
||||||
expect(result).to(equal("howdy"))
|
expect(result).to(equal("howdy"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: -- should decode a basic integer
|
||||||
it("should decode a basic integer") {
|
it("should decode a basic integer") {
|
||||||
let basicIntegerData: Data = "i3e".data(using: .utf8)!
|
let basicIntegerData: Data = "i3e".data(using: .utf8)!
|
||||||
let result = try? Bencode.decode(Int.self, from: basicIntegerData)
|
let result = try? Bencode.decode(Int.self, from: basicIntegerData)
|
||||||
|
@ -32,6 +59,7 @@ class BencodeSpec: QuickSpec {
|
||||||
expect(result).to(equal(3))
|
expect(result).to(equal(3))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: -- should decode a list of integers
|
||||||
it("should decode a list of integers") {
|
it("should decode a list of integers") {
|
||||||
let basicIntListData: Data = "li1ei2ee".data(using: .utf8)!
|
let basicIntListData: Data = "li1ei2ee".data(using: .utf8)!
|
||||||
let result = try? Bencode.decode([Int].self, from: basicIntListData)
|
let result = try? Bencode.decode([Int].self, from: basicIntListData)
|
||||||
|
@ -39,57 +67,246 @@ class BencodeSpec: QuickSpec {
|
||||||
expect(result).to(equal([1, 2]))
|
expect(result).to(equal([1, 2]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: -- should decode a basic dict
|
||||||
it("should decode a basic dict") {
|
it("should decode a basic dict") {
|
||||||
let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)!
|
let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)!
|
||||||
let result = try? Bencode.decode([String: [String]].self, from: basicDictData)
|
let result = try? Bencode.decode([String: [String]].self, from: basicDictData)
|
||||||
|
|
||||||
expect(result).to(equal(["spam": ["a", "b"]]))
|
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") {
|
context("when decoding a response") {
|
||||||
it("decodes successfully") {
|
// MARK: -- with a decodable type
|
||||||
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
|
context("with a decodable type") {
|
||||||
.data(using: .utf8)!
|
// MARK: ---- decodes successfully
|
||||||
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
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)
|
// MARK: -- decodes successfully with no body
|
||||||
.to(equal(
|
it("decodes successfully with no body") {
|
||||||
BencodeResponse(
|
let data: Data = "ld8:intValuei100e11:stringValue4:Teste"
|
||||||
info: TestType(
|
.data(using: .utf8)!
|
||||||
intValue: 100,
|
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
||||||
stringValue: "Test"
|
|
||||||
),
|
expect(result)
|
||||||
data: Data([1, 2, 3, 4, 5])
|
.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") {
|
// MARK: -- with stringified json info
|
||||||
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e"
|
context("with stringified json info") {
|
||||||
.data(using: .utf8)!
|
// MARK: -- decodes successfully
|
||||||
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
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)
|
// MARK: -- decodes successfully with no body
|
||||||
.to(equal(
|
it("decodes successfully with no body") {
|
||||||
BencodeResponse(
|
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e"
|
||||||
info: TestType(
|
.data(using: .utf8)!
|
||||||
intValue: 100,
|
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
|
||||||
stringValue: "Test"
|
|
||||||
),
|
expect(result)
|
||||||
data: nil
|
.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") {
|
// MARK: -- with a string value
|
||||||
let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
|
context("with a string value") {
|
||||||
.data(using: .utf8)!
|
// MARK: ---- decodes successfully
|
||||||
|
it("decodes successfully") {
|
||||||
expect {
|
let data: Data = "l4:Test5:\u{01}\u{02}\u{03}\u{04}\u{05}e".data(using: .utf8)!
|
||||||
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
|
let result: BencodeResponse<String>? = try? Bencode.decodeResponse(from: data)
|
||||||
_ = result
|
|
||||||
}.to(throwError(HTTPError.parsingFailed))
|
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)
|
private static let hasRun: Atomic<Bool> = Atomic(false)
|
||||||
|
|
||||||
public static func setupEnvironment(
|
public static func setupEnvironment(
|
||||||
|
retrySetupIfDatabaseInvalid: Bool = false,
|
||||||
appSpecificBlock: @escaping () -> (),
|
appSpecificBlock: @escaping () -> (),
|
||||||
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
||||||
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> ()
|
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 }
|
AppSetup.hasRun.mutate { $0 = true }
|
||||||
|
|
||||||
|
@ -64,14 +87,6 @@ public enum AppSetup {
|
||||||
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
||||||
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> ()
|
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))
|
var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function))
|
||||||
|
|
||||||
Storage.shared.perform(
|
Storage.shared.perform(
|
||||||
|
|
Loading…
Reference in New Issue