Merge branch 'dev' into feature/app-disguise

pull/1061/head
Morgan Pretty 1 month ago
commit 7dbd150c51

@ -178,6 +178,7 @@
94367C452C6C828500814252 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 94367C422C6C828500814252 /* Localizable.xcstrings */; };
943C6D822B75E061004ACE64 /* Message+DisappearingMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */; };
943C6D842B86B5F1004ACE64 /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943C6D832B86B5F1004ACE64 /* Localization.swift */; };
946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */ = {isa = PBXBuildFile; productRef = 946F5A722D5DA3AC00A5ADCE /* Punycode */; };
9473386E2BDF5F3E00B9E169 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 9473386D2BDF5F3E00B9E169 /* InfoPlist.xcstrings */; };
947AD6902C8968FF000B2730 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 947AD68F2C8968FF000B2730 /* Constants.swift */; };
94B3DC172AF8592200C88531 /* QuoteView_SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94B3DC162AF8592200C88531 /* QuoteView_SwiftUI.swift */; };
@ -311,7 +312,6 @@
C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; };
C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; };
C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */; };
C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */; };
C38EF32E255B6DBF007E1867 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF304255B6DBE007E1867 /* ImageCache.swift */; };
C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */; };
C38EF372255B6DCC007E1867 /* MediaMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF358255B6DCC007E1867 /* MediaMessageView.swift */; };
@ -683,8 +683,6 @@
FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */; };
FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; };
FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */; };
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockUI.swift */; };
FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */; };
FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */; };
FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */; };
FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */; };
@ -709,6 +707,9 @@
FD6673F62D7021E700041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F52D7021E700041530 /* SessionUtil */; };
FD6673F82D7021F200041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F72D7021F200041530 /* SessionUtil */; };
FD6673FA2D7021F800041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F92D7021F800041530 /* SessionUtil */; };
FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */; };
FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FE2D77F9BE00041530 /* ScreenLock.swift */; };
FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockWindow.swift */; };
FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */; };
FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */; };
FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EB2C2A63B500762359 /* KeychainSwift */; };
@ -1581,7 +1582,6 @@
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlaceholderIcon.swift; path = SessionUIKit/Components/PlaceholderIcon.swift; sourceTree = SOURCE_ROOT; };
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProfilePictureView.swift; path = SessionUIKit/Components/ProfilePictureView.swift; sourceTree = SOURCE_ROOT; };
C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIViewController+Utilities.swift"; path = "SignalUtilitiesKit/Utilities/UIViewController+Utilities.swift"; sourceTree = SOURCE_ROOT; };
C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScreenLock.swift; path = "SignalUtilitiesKit/Screen Lock/ScreenLock.swift"; sourceTree = SOURCE_ROOT; };
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximityMonitoringManager.swift; path = SessionMessagingKit/Utilities/ProximityMonitoringManager.swift; sourceTree = SOURCE_ROOT; };
C38EF2EF255B6DBB007E1867 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Weak.swift; path = SessionUtilitiesKit/General/Weak.swift; sourceTree = SOURCE_ROOT; };
C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAudioPlayer.h; path = SessionMessagingKit/Utilities/OWSAudioPlayer.h; sourceTree = SOURCE_ROOT; };
@ -1900,8 +1900,7 @@
FD52090228B4680F006098F6 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewModel.swift; sourceTree = "<group>"; };
FD52090628B49738006098F6 /* ConfirmationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationModal.swift; sourceTree = "<group>"; };
FD52090828B59411006098F6 /* ScreenLockUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockUI.swift; sourceTree = "<group>"; };
FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = "<group>"; };
FD52090828B59411006098F6 /* ScreenLockWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockWindow.swift; sourceTree = "<group>"; };
FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = "<group>"; };
FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = "<group>"; };
FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScopeAdapter+Utilities.swift"; sourceTree = "<group>"; };
@ -1919,6 +1918,8 @@
FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberSpec.swift; sourceTree = "<group>"; };
FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SplitSnodeReceivedMessageInfo.swift; sourceTree = "<group>"; };
FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = "<group>"; };
FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = "<group>"; };
FD6673FE2D77F9BE00041530 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = "<group>"; };
FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionSnodeKit.xctestplan; sourceTree = "<group>"; };
FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = "<group>"; };
FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; };
@ -2315,6 +2316,7 @@
buildActionMask = 2147483647;
files = (
C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
946F5A732D5DA3AC00A5ADCE /* Punycode in Frameworks */,
FD6673F82D7021F200041530 /* SessionUtil in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -2822,6 +2824,7 @@
C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */,
FD848B9228420164000E298B /* UnicodeScalar+Utilities.swift */,
C38EF2EF255B6DBB007E1867 /* Weak.swift */,
FD6673FE2D77F9BE00041530 /* ScreenLock.swift */,
FD5D201D27B0D87C00FEA984 /* SessionId.swift */,
7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */,
7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */,
@ -2860,7 +2863,7 @@
FD71162128D983ED00B47552 /* QRCodeScanningViewController.swift */,
9422569E2C23F8FE00C0FDBF /* ScanQRCodeScreen.swift */,
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */,
FD52090828B59411006098F6 /* ScreenLockUI.swift */,
FD52090828B59411006098F6 /* ScreenLockWindow.swift */,
FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */,
FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */,
FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */,
@ -3118,6 +3121,7 @@
FD52090628B49738006098F6 /* ConfirmationModal.swift */,
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */,
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */,
FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */,
FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */,
FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */,
FD0B77AF29B69A65009169BA /* TopBannerController.swift */,
@ -3132,7 +3136,6 @@
children = (
C33FD9B7255A54A300E217F9 /* Meta */,
C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */,
C36096EE25AD21BC008B62B2 /* Screen Lock */,
C3851CD225624B060061EEB0 /* Shared Views */,
C360970125AD22D3008B62B2 /* Shared View Controllers */,
C3CA3B11255CF17200F4C6D4 /* Utilities */,
@ -3317,15 +3320,6 @@
path = "Media Viewing & Editing";
sourceTree = "<group>";
};
C36096EE25AD21BC008B62B2 /* Screen Lock */ = {
isa = PBXGroup;
children = (
C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */,
FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */,
);
path = "Screen Lock";
sourceTree = "<group>";
};
C360970125AD22D3008B62B2 /* Shared View Controllers */ = {
isa = PBXGroup;
children = (
@ -5257,6 +5251,7 @@
FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */,
FD6DA9D52D017F480092085A /* XCRemoteSwiftPackageReference "session-grdb-swift" */,
FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */,
946F5A712D5DA3AC00A5ADCE /* XCRemoteSwiftPackageReference "PunycodeSwift" */,
FD6673F42D7021E700041530 /* XCRemoteSwiftPackageReference "libsession-util-spm" */,
);
productRefGroup = D221A08A169C9E5E00537ABF /* Products */;
@ -5818,6 +5813,7 @@
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */,
7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */,
FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */,
FDE754BE2C9BA16C002A2623 /* UIButtonConfiguration+Utilities.swift in Sources */,
FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */,
C331FFE02558FB0000070591 /* SearchBar.swift in Sources */,
@ -5853,7 +5849,6 @@
C38EF389255B6DD2007E1867 /* AttachmentTextView.swift in Sources */,
C38EF3FF255B6DF7007E1867 /* TappableView.swift in Sources */,
C38EF3C2255B6DE7007E1867 /* ImageEditorPaletteView.swift in Sources */,
C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */,
C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */,
C38EF3C1255B6DE7007E1867 /* ImageEditorBrushViewController.swift in Sources */,
C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */,
@ -5878,7 +5873,6 @@
C38EF3FA255B6DF7007E1867 /* DirectionalPanGestureRecognizer.swift in Sources */,
C38EF3BB255B6DE7007E1867 /* ImageEditorStrokeItem.swift in Sources */,
C38EF3C0255B6DE7007E1867 /* ImageEditorCropViewController.swift in Sources */,
FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */,
C38EF3BD255B6DE7007E1867 /* ImageEditorTransform.swift in Sources */,
C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */,
C38EF3F9255B6DF7007E1867 /* OWSLayerView.swift in Sources */,
@ -6034,6 +6028,7 @@
FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */,
FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */,
FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */,
FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */,
FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */,
FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */,
FDE754CE2C9BAF37002A2623 /* ImageFormat.swift in Sources */,
@ -6346,7 +6341,6 @@
files = (
FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */,
FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */,
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */,
7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */,
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */,
FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */,
@ -6457,6 +6451,7 @@
FDE754B72C9B96BB002A2623 /* WebRTCSession+MessageHandling.swift in Sources */,
FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */,
7BAFA75A2AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift in Sources */,
FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */,
FD71163E28E2C82900B47552 /* SessionCell.swift in Sources */,
9422569C2C23F8F000C0FDBF /* QRCodeScreen.swift in Sources */,
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
@ -7879,7 +7874,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COMPILE_LIB_SESSION = "";
CURRENT_PROJECT_VERSION = 557;
CURRENT_PROJECT_VERSION = 561;
ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -7918,7 +7913,7 @@
HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/include/**";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util";
MARKETING_VERSION = 2.9.0;
MARKETING_VERSION = 2.9.1;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "-Werror=protocol";
OTHER_SWIFT_FLAGS = "-D DEBUG -Xfrontend -warn-long-expression-type-checking=100";
@ -7957,7 +7952,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Distribution";
COMPILE_LIB_SESSION = "";
CURRENT_PROJECT_VERSION = 557;
CURRENT_PROJECT_VERSION = 561;
ENABLE_BITCODE = NO;
ENABLE_MODULE_VERIFIER = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@ -7992,7 +7987,7 @@
HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/include/**";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util";
MARKETING_VERSION = 2.9.0;
MARKETING_VERSION = 2.9.1;
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = (
"-DNS_BLOCK_ASSERTIONS=1",
@ -8503,7 +8498,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COMPILE_LIB_SESSION = YES;
CURRENT_PROJECT_VERSION = 488;
CURRENT_PROJECT_VERSION = 561;
ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@ -8544,7 +8539,7 @@
);
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util";
MARKETING_VERSION = 2.7.4;
MARKETING_VERSION = 2.9.1;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = (
"-fobjc-arc-exceptions",
@ -9171,7 +9166,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Distribution";
COMPILE_LIB_SESSION = YES;
CURRENT_PROJECT_VERSION = 488;
CURRENT_PROJECT_VERSION = 561;
ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_NO_COMMON_BLOCKS = YES;
@ -9207,7 +9202,7 @@
);
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LIB_SESSION_SOURCE_DIR = "${SRCROOT}/../LibSession-Util";
MARKETING_VERSION = 2.7.4;
MARKETING_VERSION = 2.9.1;
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = (
"-DNS_BLOCK_ASSERTIONS=1",
@ -10184,6 +10179,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
946F5A712D5DA3AC00A5ADCE /* XCRemoteSwiftPackageReference "PunycodeSwift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/gumob/PunycodeSwift.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.0.0;
};
};
FD6673F42D7021E700041530 /* XCRemoteSwiftPackageReference "libsession-util-spm" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/session-foundation/libsession-util-spm";
@ -10277,7 +10280,7 @@
repositoryURL = "https://github.com/session-foundation/session-grdb-swift.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 106.29.3;
minimumVersion = 107.3.0;
};
};
FD756BEE2D06686500BD7199 /* XCRemoteSwiftPackageReference "session-lucide" */ = {
@ -10291,6 +10294,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
946F5A722D5DA3AC00A5ADCE /* Punycode */ = {
isa = XCSwiftPackageProductDependency;
package = 946F5A712D5DA3AC00A5ADCE /* XCRemoteSwiftPackageReference "PunycodeSwift" */;
productName = Punycode;
};
FD0150512CA2446D005B08A1 /* Quick */ = {
isa = XCSwiftPackageProductDependency;
package = FD6A39302C2AD33E00762359 /* XCRemoteSwiftPackageReference "Quick" */;

@ -1,5 +1,5 @@
{
"originHash" : "688abd5f50453ed3433c8c6bf57bb60964d1c199902c2bff846544c6691cd18c",
"originHash" : "e3fdf2f44acd1f05dab295d0c9e3faf05f5e4461d512be1d5a77af42e0a25e48",
"pins" : [
{
"identity" : "cocoalumberjack",
@ -82,6 +82,15 @@
"version" : "5.2.0"
}
},
{
"identity" : "punycodeswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gumob/PunycodeSwift.git",
"state" : {
"revision" : "30a462bdb4398ea835a3585472229e0d74b36ba5",
"version" : "3.0.0"
}
},
{
"identity" : "quick",
"kind" : "remoteSourceControl",
@ -96,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/session-foundation/session-grdb-swift.git",
"state" : {
"revision" : "b3643613f1e0f392fa41072ee499da93b4c06b67",
"version" : "106.29.3"
"revision" : "c69f8bf8a7ede8727c20f7c36eeffd3f55598487",
"version" : "107.3.0"
}
},
{

@ -275,10 +275,10 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
receiveCompletion: { [weak self] result in
switch result {
case .finished:
SNLog("[Calls] Offer message sent")
Log.info(.calls, "Offer message sent")
self?.updateCallDetailedStatus?("Sending Connection Candidates")
case .failure(let error):
SNLog("[Calls] Error initializing call after 5 retries: \(error), ending call...")
Log.error(.calls, "Error initializing call after 5 retries: \(error), ending call...")
self?.handleCallInitializationFailed()
}
}
@ -291,14 +291,14 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
hasStartedConnecting = true
if let sdp = remoteSDP {
SNLog("[Calls] Got remote sdp already")
Log.info(.calls, "Got remote sdp already")
self.updateCallDetailedStatus?("Answering Call")
webRTCSession.handleRemoteSDP(sdp, from: sessionId) // This sends an answer message internally
}
}
func answerSessionCallInBackground() {
SNLog("[Calls] Answering call in background")
Log.info(.calls, "Answering call in background")
self.answerSessionCall()
}

@ -144,7 +144,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
}
func handleCallEnded() {
SNLog("[Calls] Call ended.")
Log.info(.calls, "Call ended.")
WebRTCSession.current = nil
dependencies[defaults: .appGroup, key: .isCallOngoing] = false
dependencies[defaults: .appGroup, key: .lastCallPreOffer] = nil

@ -250,9 +250,12 @@ extension ContextMenuVC {
else { return false }
if cellViewModel.threadVariant == .community {
return dependencies[singleton: .openGroupManager].doesOpenGroupSupport(
capability: .reactions,
on: cellViewModel.threadOpenGroupServer
return (
!forMessageInfoScreen &&
dependencies[singleton: .openGroupManager].doesOpenGroupSupport(
capability: .reactions,
on: cellViewModel.threadOpenGroupServer
)
)
}
return (threadViewModel.threadIsMessageRequest != true && !forMessageInfoScreen)

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Combine
import GRDB
import SignalUtilitiesKit
import SessionUIKit
@ -85,31 +86,25 @@ extension ConversationSearchController: UISearchResultsUpdating {
}
let threadId: String = self.threadId
DispatchQueue.global(qos: .default).async { [weak self] in
let results: [Interaction.TimestampInfo]? = dependencies[singleton: .storage].read { db -> [Interaction.TimestampInfo] in
self?.resultsBar.willStartSearching(readConnection: db)
return try Interaction.idsForTermWithin(
let searchCancellable: AnyCancellable = dependencies[singleton: .storage]
.readPublisher { db -> [Interaction.TimestampInfo] in
try Interaction.idsForTermWithin(
threadId: threadId,
pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText)
)
.fetchAll(db)
}
// If we didn't get results back then we most likely interrupted the query so
// should ignore the results (if there are no results we would succeed and get
// an empty array back)
guard let results: [Interaction.TimestampInfo] = results else { return }
DispatchQueue.main.async {
guard let strongSelf = self else { return }
self?.resultsBar.stopLoading()
self?.resultsBar.updateResults(results: results, visibleItemIds: self?.delegate?.currentVisibleIds())
self?.delegate?.conversationSearchController(strongSelf, didUpdateSearchResults: results, searchText: searchText)
}
}
.subscribe(on: DispatchQueue.global(qos: .default), using: dependencies)
.receive(on: DispatchQueue.main, using: dependencies)
.sink(
receiveCompletion: { _ in },
receiveValue: { [weak self] results in
self?.resultsBar.stopLoading()
self?.resultsBar.updateResults(results: results, visibleItemIds: self?.delegate?.currentVisibleIds())
self?.delegate?.conversationSearchController(self, didUpdateSearchResults: results, searchText: searchText)
}
)
self.resultsBar.willStartSearching(searchCancellable: searchCancellable)
}
}
@ -138,7 +133,7 @@ protocol SearchResultsBarDelegate: AnyObject {
public final class SearchResultsBar: UIView {
@ThreadSafe private var hasResults: Bool = false
@ThreadSafeObject private var results: [Interaction.TimestampInfo] = []
@ThreadSafeObject private var readConnection: Database? = nil
@ThreadSafeObject private var currentSearchCancellable: AnyCancellable? = nil
var currentIndex: Int?
weak var resultsBarDelegate: SearchResultsBarDelegate?
@ -275,8 +270,7 @@ public final class SearchResultsBar: UIView {
// MARK: - Content
/// This method will be called within a DB read block
func willStartSearching(readConnection: Database) {
func willStartSearching(searchCancellable: AnyCancellable) {
let hasNoExistingResults: Bool = hasResults
DispatchQueue.main.async { [weak self] in
@ -287,8 +281,8 @@ public final class SearchResultsBar: UIView {
self?.startLoading()
}
self.readConnection?.interrupt()
self._readConnection.set(to: readConnection)
currentSearchCancellable?.cancel()
_currentSearchCancellable.set(to: searchCancellable)
}
func updateResults(results: [Interaction.TimestampInfo]?, visibleItemIds: [Int64]?) {
@ -311,7 +305,6 @@ public final class SearchResultsBar: UIView {
return 0
}()
self._readConnection.set(to: nil)
self._results.performUpdate { _ in (results ?? []) }
self.hasResults = (results != nil)
@ -366,6 +359,6 @@ public final class SearchResultsBar: UIView {
public protocol ConversationSearchControllerDelegate: UISearchControllerDelegate {
func conversationSearchControllerDependencies() -> Dependencies
func currentVisibleIds() -> [Int64]
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?)
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo: Interaction.TimestampInfo)
func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?)
func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didSelectInteractionInfo: Interaction.TimestampInfo)
}

@ -344,7 +344,7 @@ extension ConversationVC:
Permissions.requestMicrophonePermissionIfNeeded(using: viewModel.dependencies)
if Permissions.microphone != .granted {
SNLog("Proceeding without microphone access. Any recorded video will be silent.")
Log.warn(.conversation, "Proceeding without microphone access. Any recorded video will be silent.")
}
let sendMediaNavController = SendMediaNavigationController.showingCameraFirst(
@ -1107,7 +1107,7 @@ extension ConversationVC:
guard
let originalFilePath: String = mediaView.attachment.originalFilePath(using: viewModel.dependencies),
viewModel.dependencies[singleton: .fileManager].fileExists(atPath: originalFilePath)
else { return SNLog("Missing video file") }
else { return Log.warn(.conversation, "Missing video file") }
/// When playing media we need to change the AVAudioSession to 'playback' mode so the device "silent mode"
/// doesn't prevent video audio from playing
@ -2337,7 +2337,7 @@ extension ConversationVC:
self.audioRecorder = audioRecorder
}
catch {
SNLog("Couldn't start audio recording due to error: \(error).")
Log.error(.conversation, "Couldn't start audio recording due to error: \(error).")
return cancelVoiceMessageRecording()
}
@ -2353,7 +2353,7 @@ extension ConversationVC:
guard successfullyPrepared && startedRecording else {
SNLog(successfullyPrepared ? "Couldn't record audio." : "Couldn't prepare audio recorder.")
Log.error(.conversation, (successfullyPrepared ? "Couldn't record audio." : "Couldn't prepare audio recorder."))
// Dispatch to the next run loop to avoid
DispatchQueue.main.async {
@ -2412,7 +2412,9 @@ extension ConversationVC:
let dataSourceOrNil = DataSourcePath(fileUrl: audioRecorder.url, sourceFilename: nil, shouldDeleteOnDeinit: true, using: viewModel.dependencies)
self.audioRecorder = nil
guard let dataSource = dataSourceOrNil else { return SNLog("Couldn't load recorded data.") }
guard let dataSource = dataSourceOrNil else {
return Log.error(.conversation, "Couldn't load recorded data.")
}
// Create attachment
let fileName = ("messageVoice".localized() as NSString)

@ -422,7 +422,10 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
using: dependencies
)
dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver)
/// Dispatch adding the database observation to a background thread
DispatchQueue.global(qos: .userInitiated).async { [weak viewModel] in
dependencies[singleton: .storage].addObserver(viewModel?.pagedDataObserver)
}
super.init(nibName: nil, bundle: nil)
}
@ -704,14 +707,18 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
// Stop observing changes
self?.stopObservingChanges()
dependencies[singleton: .storage].removeObserver(self?.viewModel.pagedDataObserver)
DispatchQueue.global(qos: .userInitiated).async {
dependencies[singleton: .storage].removeObserver(self?.viewModel.pagedDataObserver)
}
// Swap the observing to the updated thread
let newestVisibleMessageId: Int64? = self?.fullyVisibleCellViewModels()?.last?.id
self?.viewModel.swapToThread(updatedThreadId: unblindedId, focussedMessageId: newestVisibleMessageId)
// Start observing changes again
dependencies[singleton: .storage].addObserver(self?.viewModel.pagedDataObserver)
/// Start observing changes again (on a background thread)
DispatchQueue.global(qos: .userInitiated).async {
dependencies[singleton: .storage].addObserver(self?.viewModel.pagedDataObserver)
}
self?.startObservingChanges()
return
}
@ -1961,12 +1968,12 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
func conversationSearchControllerDependencies() -> Dependencies { return viewModel.dependencies }
func currentVisibleIds() -> [Int64] { return (fullyVisibleCellViewModels() ?? []).map { $0.id } }
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) {
func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didUpdateSearchResults results: [Interaction.TimestampInfo]?, searchText: String?) {
viewModel.lastSearchedText = searchText
tableView.reloadRows(at: tableView.indexPathsForVisibleRows ?? [], with: UITableView.RowAnimation.none)
}
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo interactionInfo: Interaction.TimestampInfo) {
func conversationSearchController(_ conversationSearchController: ConversationSearchController?, didSelectInteractionInfo interactionInfo: Interaction.TimestampInfo) {
scrollToInteractionIfNeeded(with: interactionInfo, focusBehaviour: .highlight)
}
@ -2139,7 +2146,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa
return nil
default:
SNLog("[ConversationVC] Warning: Processing unhandled cell type when marking as read, this could result in intermittent failures")
Log.warn(.conversation, "Processing unhandled cell type when marking as read, this could result in intermittent failures")
return nil
}
})

@ -11,6 +11,14 @@ import SessionMessagingKit
import SessionUtilitiesKit
import SessionUIKit
// MARK: - Log.Category
public extension Log.Category {
static let conversation: Log.Category = .create("Conversation", defaultLevel: .info)
}
// MARK: - ConversationViewModel
public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHolder {
public typealias SectionModel = ArraySection<Section, MessageViewModel>
@ -361,7 +369,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold
}
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[ConversationViewModel] Observation failed with error: \($0)") })
.handleEvents(didFail: { Log.error(.conversation, "Observation failed with error: \($0)") })
}
public func updateThreadData(_ updatedData: SessionThreadViewModel) {

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Combine
import GRDB
import DifferenceKit
import SessionUIKit
@ -8,6 +9,14 @@ import SessionMessagingKit
import SessionUtilitiesKit
import SignalUtilitiesKit
// MARK: - Log.Category
private extension Log.Category {
static let cat: Log.Category = .create("GlobalSearch", defaultLevel: .warn)
}
// MARK: - GlobalSearchViewController
class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UITableViewDelegate, UITableViewDataSource {
fileprivate typealias SectionModel = ArraySection<SearchSection, SessionThreadViewModel>
@ -106,7 +115,7 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI
)
}()
@ThreadSafeObject private var readConnection: Database? = nil
@ThreadSafeObject private var currentSearchCancellable: AnyCancellable? = nil
private lazy var searchResultSet: SearchResultData = defaultSearchResults
private var termForCurrentSearchResultSet: String = ""
private var lastSearchText: String?
@ -256,61 +265,53 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI
guard force || lastSearchText != searchText else { return }
lastSearchText = searchText
DispatchQueue.global(qos: .default).async { [weak self, dependencies] in
self?.readConnection?.interrupt()
let result: Result<[SectionModel], Error>? = dependencies[singleton: .storage].read { db -> Result<[SectionModel], Error> in
self?._readConnection.set(to: db)
currentSearchCancellable?.cancel()
_currentSearchCancellable.set(to: dependencies[singleton: .storage]
.readPublisher { [dependencies] db -> [SectionModel] in
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let contactsAndGroupsResults: [SessionThreadViewModel] = try SessionThreadViewModel
.contactsAndGroupsQuery(
userSessionId: userSessionId,
pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText),
searchTerm: searchText
)
.fetchAll(db)
let messageResults: [SessionThreadViewModel] = try SessionThreadViewModel
.messagesQuery(
userSessionId: userSessionId,
pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText)
)
.fetchAll(db)
do {
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let contactsAndGroupsResults: [SessionThreadViewModel] = try SessionThreadViewModel
.contactsAndGroupsQuery(
userSessionId: userSessionId,
pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText),
searchTerm: searchText
)
.fetchAll(db)
let messageResults: [SessionThreadViewModel] = try SessionThreadViewModel
.messagesQuery(
userSessionId: userSessionId,
pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText)
)
.fetchAll(db)
return .success([
ArraySection(model: .contactsAndGroups, elements: contactsAndGroupsResults),
ArraySection(model: .messages, elements: messageResults)
])
}
catch {
// Don't log the 'interrupt' error as that's just the user typing too fast
if (error as? DatabaseError)?.resultCode != DatabaseError.SQLITE_INTERRUPT {
SNLog("[GlobalSearch] Failed to find results due to error: \(error)")
}
return .failure(error)
}
return [
ArraySection(model: .contactsAndGroups, elements: contactsAndGroupsResults),
ArraySection(model: .messages, elements: messageResults)
]
}
self?._readConnection.set(to: nil)
DispatchQueue.main.async {
switch result {
case .success(let sections):
self?.termForCurrentSearchResultSet = searchText
self?.searchResultSet = SearchResultData(
state: (sections.map { $0.elements.count }.reduce(0, +) > 0) ? .results : .none,
data: sections
)
self?.isLoading = false
self?.tableView.reloadData()
self?.refreshTimer = nil
default: break
.subscribe(on: DispatchQueue.global(qos: .default), using: dependencies)
.receive(on: DispatchQueue.main, using: dependencies)
.sink(
receiveCompletion: { result in
/// Cancelling the search results in `receiveCompletion` not getting called so we can just log any
/// errors we get without needing to filter out "cancelled search" cases
switch result {
case .finished: break
case .failure(let error):
Log.error(.cat, "Failed to find results due to error: \(error)")
}
},
receiveValue: { [weak self] sections in
self?.termForCurrentSearchResultSet = searchText
self?.searchResultSet = SearchResultData(
state: (sections.map { $0.elements.count }.reduce(0, +) > 0) ? .results : .none,
data: sections
)
self?.isLoading = false
self?.tableView.reloadData()
self?.refreshTimer = nil
}
}
}
))
}
@objc func cancel() {

@ -31,7 +31,10 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi
init(using dependencies: Dependencies) {
self.viewModel = HomeViewModel(using: dependencies)
dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver)
/// Dispatch adding the database observation to a background thread
DispatchQueue.global(qos: .userInitiated).async { [weak viewModel] in
dependencies[singleton: .storage].addObserver(viewModel?.pagedDataObserver)
}
super.init(nibName: nil, bundle: nil)
}
@ -353,7 +356,7 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi
if
Identity.userExists(using: viewModel.dependencies),
let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate,
!viewModel.dependencies[singleton: .appContext].isNotInForeground
viewModel.dependencies[singleton: .appContext].isMainAppAndActive
{
appDelegate.startPollersIfNeeded()
}

@ -7,6 +7,14 @@ import SignalUtilitiesKit
import SessionMessagingKit
import SessionUtilitiesKit
// MARK: - Log.Category
private extension Log.Category {
static let cat: Log.Category = .create("HomeViewModel", defaultLevel: .warn)
}
// MARK: - HomeViewModel
public class HomeViewModel: NavigatableStateHolder {
public let navigatableState: NavigatableState = NavigatableState()
@ -227,7 +235,7 @@ public class HomeViewModel: NavigatableStateHolder {
try HomeViewModel.retrieveState(db, using: dependencies)
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[HomeViewModel] Observation failed with error: \($0)") })
.handleEvents(didFail: { Log.error(.cat, "Observation failed with error: \($0)") })
private static func retrieveState(
_ db: Database,

@ -34,7 +34,11 @@ public class DocumentTileViewController: UIViewController, UITableViewDelegate,
init(viewModel: MediaGalleryViewModel, using dependencies: Dependencies) {
self.dependencies = dependencies
self.viewModel = viewModel
dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver)
/// Dispatch adding the database observation to a background thread
DispatchQueue.global(qos: .userInitiated).async { [weak viewModel] in
dependencies[singleton: .storage].addObserver(viewModel?.pagedDataObserver)
}
super.init(nibName: nil, bundle: nil)
}

@ -483,7 +483,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
}
guard let asset: PHAsset = photoCollectionContents.asset(at: indexPath.item) else {
SNLog("Failed to select cell for asset at \(indexPath.item)")
Log.error(.media, "Failed to select cell for asset at \(indexPath.item)")
delegate.imagePicker(self, failedToRetrieveAssetAt: indexPath.item, forCount: photoCollectionContents.assetCount)
return
}
@ -529,7 +529,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
let cell: PhotoGridViewCell = collectionView.dequeue(type: PhotoGridViewCell.self, for: indexPath)
guard let assetItem: PhotoPickerAssetItem = photoCollectionContents.assetItem(at: indexPath.item, photoMediaSize: photoMediaSize) else {
SNLog("Failed to style cell for asset at \(indexPath.item)")
Log.error(.media, "Failed to style cell for asset at \(indexPath.item)")
return cell
}

@ -92,7 +92,7 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate {
updateUICallback()
},
failure: {
SNLog("Could not load media.")
Log.error(.media, "Could not load media.")
}
)
}
@ -175,7 +175,7 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate {
let viewSize: CGSize = self.scrollView.bounds.size
guard imageSize.width > 0 && imageSize.height > 0 else {
SNLog("Invalid image dimensions (\(imageSize.width), \(imageSize.height))")
Log.error(.media, "Invalid image dimensions (\(imageSize.width), \(imageSize.height))")
return
}
@ -350,7 +350,7 @@ class MediaDetailViewController: OWSViewController, UIScrollViewDelegate {
guard
let originalFilePath: String = self.galleryItem.attachment.originalFilePath(using: dependencies),
dependencies[singleton: .fileManager].fileExists(atPath: originalFilePath)
else { return SNLog("Missing video file") }
else { return Log.error(.media, "Missing video file") }
let videoUrl: URL = URL(fileURLWithPath: originalFilePath)
let player: AVPlayer = AVPlayer(url: videoUrl)

@ -395,7 +395,7 @@ public class MediaGalleryViewModel {
.fetchAll(db)
}
.removeDuplicates()
.handleEvents(didFail: { SNLog("[MediaGalleryViewModel] Observation failed with error: \($0)") })
.handleEvents(didFail: { Log.error(.media, "Gallery observation failed with error: \($0)") })
}
@discardableResult public func loadAndCacheAlbumData(for interactionId: Int64, in threadId: String) -> [Item] {

@ -44,7 +44,11 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour
init(viewModel: MediaGalleryViewModel, using dependencies: Dependencies) {
self.dependencies = dependencies
self.viewModel = viewModel
dependencies[singleton: .storage].addObserver(viewModel.pagedDataObserver)
/// Dispatch adding the database observation to a background thread
DispatchQueue.global(qos: .userInitiated).async { [weak viewModel] in
dependencies[singleton: .storage].addObserver(viewModel?.pagedDataObserver)
}
super.init(nibName: nil, bundle: nil)
}

@ -97,13 +97,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// Note: Intentionally dispatching sync as we want to wait for these to complete before
// continuing
DispatchQueue.main.sync {
ScreenLockUI.shared.setupWithRootWindow(rootWindow: mainWindow, using: dependencies)
dependencies[singleton: .screenLock].setupWithRootWindow(rootWindow: mainWindow)
OWSWindowManager.shared().setup(
withRootWindow: mainWindow,
screenBlockingWindow: ScreenLockUI.shared.screenBlockingWindow,
screenBlockingWindow: dependencies[singleton: .screenLock].window,
backgroundWindowLevel: .background
)
ScreenLockUI.shared.startObserving()
}
},
migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in
@ -318,26 +317,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// MARK: - Background Fetching
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
/// It seems like it's possible for this function to be called with an invalid `backgroundTimeRemaining` value
/// (`TimeInterval.greatestFiniteMagnitude`) in which case we just want to mark it as a failure
///
/// Additionally we want to ensure that our timeout timer has enough time to run so make sure we have at least `5 seconds`
/// of background execution (if we don't then the process could incorrectly run longer than it should)
guard
application.backgroundTimeRemaining < TimeInterval.greatestFiniteMagnitude &&
application.backgroundTimeRemaining > 5
else { return completionHandler(.failed) }
Log.appResumedExecution()
Log.info(.backgroundPoller, "Starting background fetch.")
dependencies[singleton: .storage].resumeDatabaseAccess()
dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() }
let queue: DispatchQueue = .global(qos: .userInitiated)
let queue: DispatchQueue = DispatchQueue(label: "com.session.backgroundPoll")
let poller: BackgroundPoller = BackgroundPoller()
var cancellable: AnyCancellable?
// Background tasks only last for a certain amount of time (which can result in a crash and a
// prompt appearing for the user), we want to avoid this and need to make sure to suspend the
// database again before the background task ends so we start a timer that expires 1 second
// before the background task is due to expire in order to do so
let cancelTimer: Timer = Timer.scheduledTimerOnMainThread(
withTimeInterval: (application.backgroundTimeRemaining - 5),
repeats: false,
using: dependencies
) { [poller, dependencies] timer in
timer.invalidate()
/// Background tasks only last for a certain amount of time (which can result in a crash and a prompt appearing for the user),
/// we want to avoid this and need to make sure to suspend the database again before the background task ends so we start
/// a timer that expires before the background task is due to expire in order to do so
///
/// **Note:** We **MUST** capture both `poller` and `cancellable` strongly in the event handler to ensure neither
/// go out of scope until we want them to (we essentually want a retain cycle in this case)
let durationRemainingMs: Int = max(1, Int((application.backgroundTimeRemaining - 5) * 1000))
let timer: DispatchSourceTimer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now() + .milliseconds(durationRemainingMs))
timer.setEventHandler { [poller, dependencies] in
guard cancellable != nil else { return }
Log.info(.backgroundPoller, "Background poll failed due to manual timeout.")
@ -352,32 +360,49 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
_ = poller // Capture poller to ensure it doesn't go out of scope
completionHandler(.failed)
}
timer.resume()
dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [dependencies, poller] in
// If the 'AppReadiness' process takes too long then it's possible for the user to open
// the app after this closure is registered but before it's actually triggered - this can
// result in the `BackgroundPoller` incorrectly getting called in the foreground, this check
// is here to prevent that
/// If the 'AppReadiness' process takes too long then it's possible for the user to open the app after this closure is registered
/// but before it's actually triggered - this can result in the `BackgroundPoller` incorrectly getting called in the foreground,
/// this check is here to prevent that
guard dependencies[singleton: .appContext].isInBackground else { return }
/// Kick off the `BackgroundPoller`
///
/// **Note:** We **MUST** capture both `poller` and `timer` strongly in the completion handler to ensure neither
/// go out of scope until we want them to (we essentually want a retain cycle in this case)
cancellable = poller
.poll(using: dependencies)
.subscribe(on: queue, using: dependencies)
.receive(on: DispatchQueue.main, using: dependencies)
.receive(on: queue, using: dependencies)
.sink(
receiveCompletion: { [poller] result in
receiveCompletion: { [timer, poller] result in
// Ensure we haven't timed out yet
guard cancelTimer.isValid else { return }
guard timer.isCancelled == false else { return }
// Immediately cancel the timer to prevent the timeout being triggered
timer.cancel()
// Update the unread count badge
let unreadCount: Int = dependencies[singleton: .storage]
.read { db in try Interaction.fetchAppBadgeUnreadCount(db, using: dependencies) }
.defaulting(to: 0)
DispatchQueue.main.async(using: dependencies) {
UIApplication.shared.applicationIconBadgeNumber = unreadCount
}
// If we are still running in the background then suspend the network & database
if dependencies[singleton: .appContext].isInBackground {
dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() }
dependencies[singleton: .storage].suspendDatabaseAccess()
Log.flush()
}
cancelTimer.invalidate()
_ = poller // Capture poller to ensure it doesn't go out of scope
// Complete the background task
switch result {
case .failure: completionHandler(.failed)
case .finished: completionHandler(.newData)
@ -850,16 +875,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// MARK: - Polling
public func startPollersIfNeeded(shouldStartGroupPollers: Bool = true) {
public func startPollersIfNeeded() {
guard dependencies[cache: .onboarding].state == .completed else { return }
/// Start the pollers on a background thread so that any database queries they need to run don't
/// block the main thread
DispatchQueue.global(qos: .background).async { [dependencies] in
dependencies[singleton: .currentUserPoller].startIfNeeded()
guard shouldStartGroupPollers else { return }
dependencies.mutate(cache: .groupPollers) { $0.startAllPollers() }
dependencies.mutate(cache: .communityPollers) { $0.startAllPollers() }
}

@ -1225,6 +1225,33 @@ SOFTWARE.
<key>Title</key>
<string>NVActivityIndicatorView</string>
</dict>
<dict>
<key>License</key>
<string>MIT License
Copyright (c) 2018 Gumob
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</string>
<key>Title</key>
<string>PunycodeSwift</string>
</dict>
<dict>
<key>License</key>
<string>Apache License

File diff suppressed because it is too large Load Diff

@ -137,7 +137,7 @@ public class NotificationPresenter: NSObject, UNUserNotificationCenterDelegate,
}
guard notificationBody != nil || notificationTitle != nil else {
SNLog("AppNotifications error: No notification content")
Log.info("AppNotifications error: No notification content")
return
}

@ -315,7 +315,7 @@ public class PushRegistrationManager: NSObject, PKPushRegistryDelegate {
dependencies[singleton: .jobRunner].appDidBecomeActive()
// NOTE: Just start 1-1 poller so that it won't wait for polling group messages
(UIApplication.shared.delegate as? AppDelegate)?.startPollersIfNeeded(shouldStartGroupPollers: false)
dependencies[singleton: .currentUserPoller].startIfNeeded(forceStartInBackground: true)
call.reportIncomingCallIfNeeded { error in
if let error = error {

@ -241,11 +241,11 @@ extension Onboarding {
/// In order to process the config message we need to create and load a `libSession` cache, but we don't want to load this into
/// memory at this stage in case the user cancels the onboarding process part way through
let cache: LibSession.Cache = LibSession.Cache(userSessionId: userSessionId, using: dependencies)
cache.loadDefaultStatesFor(
userConfigVariants: [.userProfile],
groups: [],
userSessionId: userSessionId,
userEd25519KeyPair: identity.ed25519KeyPair
cache.loadDefaultStateFor(
variant: .userProfile,
sessionId: userSessionId,
userEd25519KeyPair: identity.ed25519KeyPair,
groupEd25519SecretKey: nil
)
try cache.unsafeDirectMergeConfigMessage(
swarmPublicKey: userSessionId.hexString,

@ -39,8 +39,8 @@ final class PathStatusView: UIView {
super.init(frame: .zero)
setStatus(to: .unknown) // Default to the unknown status
setUpViewHierarchy()
setStatus(to: .unknown) // Default to the unknown status
registerObservers()
}
@ -51,10 +51,23 @@ final class PathStatusView: UIView {
// MARK: - Layout
private func setUpViewHierarchy() {
layer.cornerRadius = (self.size.pointSize / 2)
layer.masksToBounds = false
self.set(.width, to: self.size.pointSize)
self.set(.height, to: self.size.pointSize)
layer.cornerRadius = (self.size.pointSize / 2)
layer.masksToBounds = false
layer.shadowOffset = CGSize(width: 0, height: 0.8)
layer.shadowPath = UIBezierPath(
ovalIn: CGRect(
origin: CGPoint.zero,
size: CGSize(width: self.size.pointSize, height: self.size.pointSize)
)
).cgPath
ThemeManager.onThemeChange(observer: self) { [weak self] theme, _ in
self?.layer.shadowOpacity = (theme.interfaceStyle == .light ? 0.4 : 1)
self?.layer.shadowRadius = (self?.size.offset(for: theme.interfaceStyle) ?? 0)
}
}
// MARK: - Functions
@ -62,7 +75,6 @@ final class PathStatusView: UIView {
private func registerObservers() {
/// Register for status updates (will be called immediately with current status)
dependencies[cache: .libSessionNetwork].networkStatus
.subscribe(on: DispatchQueue.global(qos: .background), using: dependencies)
.receive(on: DispatchQueue.main, using: dependencies)
.sink(
receiveCompletion: { [weak self] _ in
@ -80,18 +92,6 @@ final class PathStatusView: UIView {
private func setStatus(to status: NetworkStatus) {
themeBackgroundColor = status.themeColor
layer.themeShadowColor = status.themeColor
layer.shadowOffset = CGSize(width: 0, height: 0.8)
layer.shadowPath = UIBezierPath(
ovalIn: CGRect(
origin: CGPoint.zero,
size: CGSize(width: self.size.pointSize, height: self.size.pointSize)
)
).cgPath
ThemeManager.onThemeChange(observer: self) { [weak self] theme, _ in
self?.layer.shadowOpacity = (theme.interfaceStyle == .light ? 0.4 : 1)
self?.layer.shadowRadius = (self?.size.offset(for: theme.interfaceStyle) ?? 0)
}
}
}

@ -8,6 +8,14 @@ import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
// MARK: - Log.Category
private extension Log.Category {
static let version: Log.Category = .create("Version", defaultLevel: .info)
}
// MARK: - HelpViewModel
class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource {
typealias TableItem = Section
@ -204,7 +212,7 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa
using dependencies: Dependencies,
onShareComplete: (() -> ())? = nil
) {
Log.info("[Version] \(dependencies[cache: .appVersion].versionInfo)")
Log.info(.version, "\(dependencies[cache: .appVersion].versionInfo)")
Log.flush()
guard

@ -5,10 +5,20 @@ import AVFoundation
import SessionUIKit
import SessionUtilitiesKit
// MARK: - Log.Category
private extension Log.Category {
static let cat: Log.Category = .create("QRCode", defaultLevel: .warn)
}
// MARK: - QRScannerDelegate
protocol QRScannerDelegate: AnyObject {
func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onSuccess: (() -> ())?, onError: (() -> ())?)
}
// MARK: - QRCodeScanningViewController
class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
public weak var scanDelegate: QRScannerDelegate?
@ -123,7 +133,7 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj
let device: AVCaptureDevice = maybeDevice,
let input: AVCaptureInput = try? AVCaptureDeviceInput(device: device)
else {
return SNLog("Failed to retrieve the device for enabling the QRCode scanning camera")
return Log.error(.cat, "Failed to retrieve the device for enabling the QRCode scanning camera")
}
// Image output
@ -141,11 +151,11 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj
if capture.canAddOutput(metadataOutput) { capture.addOutput(metadataOutput) }
guard !capture.inputs.isEmpty && capture.outputs.count == 2 else {
return SNLog("Failed to attach the input/output to the capture session")
return Log.error(.cat, "Failed to attach the input/output to the capture session")
}
guard metadataOutput.availableMetadataObjectTypes.contains(.qr) else {
return SNLog("The output is unable to process QR codes")
return Log.error(.cat, "The output is unable to process QR codes")
}
// Specify that we want to capture QR Codes (Needs to be done after being added

@ -1,122 +1,79 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import LocalAuthentication
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
import SignalUtilitiesKit
class ScreenLockUI {
public static let shared: ScreenLockUI = ScreenLockUI()
private var dependencies: Dependencies?
public lazy var screenBlockingWindow: UIWindow = {
let result: UIWindow = UIWindow()
result.isHidden = false
result.windowLevel = .background
result.isOpaque = true
result.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary)
result.rootViewController = self.screenBlockingViewController
return result
}()
private lazy var screenBlockingViewController: ScreenLockViewController = {
let result: ScreenLockViewController = ScreenLockViewController { [weak self] in
guard self?.appIsInactiveOrBackground == false else {
// This button can be pressed while the app is inactive
// for a brief window while the iOS auth UI is dismissing.
return
}
// MARK: - Singleton
Log.info("unlockButtonWasTapped")
public extension Singleton {
static let screenLock: SingletonConfig<ScreenLockWindow> = Dependencies.create(
identifier: "screenLock",
createInstance: { dependencies in ScreenLockWindow(using: dependencies) }
)
}
self?.didLastUnlockAttemptFail = false
self?.ensureUI()
}
return result
}()
/// Obscures the app screen:
///
/// * In the app switcher.
/// * During 'Screen Lock' unlock process.
public class ScreenLockWindow {
private let dependencies: Dependencies
/// Unlike UIApplication.applicationState, this state reflects the notifications, i.e. "did become active", "will resign active",
/// "will enter foreground", "did enter background".
///
/// We want to update our state to reflect these transitions and have the "update" logic be consistent with "last reported"
/// state. i.e. when you're responding to "will resign active", we need to behave as though we're already inactive.
/// Indicates whether or not the user is currently locked out of the app. Should only be set if `db[.isScreenLockEnabled]`.
///
/// Secondly, we need to show the screen protection _before_ we become inactive in order for it to be reflected in the
/// app switcher.
private var appIsInactiveOrBackground: Bool = false {
didSet {
if self.appIsInactiveOrBackground {
if !self.isShowingScreenLockUI {
self.didLastUnlockAttemptFail = false
self.tryToActivateScreenLockBasedOnCountdown()
}
}
else if !self.didUnlockJustSucceed {
self.tryToActivateScreenLockBasedOnCountdown()
}
self.didUnlockJustSucceed = false
self.ensureUI()
}
}
private var appIsInBackground: Bool = false {
didSet {
self.didUnlockJustSucceed = false
self.tryToActivateScreenLockBasedOnCountdown()
self.ensureUI()
}
}
/// * The user is locked out by default on app launch.
/// * The user is also locked out if the app is sent to the background
@ThreadSafe private var isScreenLockLocked: Bool = false
private var isShowingScreenLockUI: Bool = false
private var didUnlockJustSucceed: Bool = false
private var didLastUnlockAttemptFail: Bool = false
/// We want to remain in "screen lock" mode while "local auth" UI is dismissing. So we lazily clear isShowingScreenLockUI
/// using this property.
private var shouldClearAuthUIWhenActive: Bool = false
/// Indicates whether or not the user is currently locked out of the app. Should only be set if db[.isScreenLockEnabled].
///
/// * The user is locked out by default on app launch.
/// * The user is also locked out if the app is sent to the background
@ThreadSafe private var isScreenLockLocked: Bool = false
// Determines what the state of the app should be.
private var desiredUIState: ScreenLockViewController.State {
if isScreenLockLocked {
if appIsInactiveOrBackground {
Log.verbose("desiredUIState: screen protection 1.")
return .protection
}
Log.verbose("desiredUIState: screen lock 2.")
return (isShowingScreenLockUI ? .protection : .lock)
}
if !self.appIsInactiveOrBackground {
// App is inactive or background.
Log.verbose("desiredUIState: none 3.");
return .none;
}
// MARK: - UI
public lazy var window: UIWindow = {
let result: UIWindow = UIWindow()
result.isHidden = false
result.windowLevel = .background
result.isOpaque = true
result.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary)
result.rootViewController = self.viewController
if SessionEnvironment.shared?.isRequestingPermission == true {
return .none;
return result
}()
private lazy var viewController: ScreenLockViewController = ScreenLockViewController { [weak self, dependencies] in
guard dependencies[singleton: .appContext].isAppForegroundAndActive else {
// This button can be pressed while the app is inactive
// for a brief window while the iOS auth UI is dismissing.
return
}
Log.verbose("desiredUIState: screen protection 4.")
return .protection;
Log.info(.screenLock, "unlockButtonWasTapped")
self?.didLastUnlockAttemptFail = false
self?.ensureUI()
}
// MARK: - Lifecycle
init(using dependencies: Dependencies) {
self.dependencies = dependencies
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Observations
private func observeNotifications() {
NotificationCenter.default.addObserver(
self,
@ -150,56 +107,72 @@ class ScreenLockUI {
)
}
public func setupWithRootWindow(rootWindow: UIWindow, using dependencies: Dependencies) {
self.dependencies = dependencies
self.screenBlockingWindow.frame = rootWindow.bounds
}
public func startObserving() {
self.appIsInactiveOrBackground = (UIApplication.shared.applicationState != .active)
public func setupWithRootWindow(rootWindow: UIWindow) {
self.window.frame = rootWindow.bounds
self.observeNotifications()
// Hide the screen blocking window until "app is ready" to
// avoid blocking the loading view.
/// Hide the screen blocking window until "app is ready" to avoid blocking the loading view
updateScreenBlockingWindow(state: .none, animated: false)
// Initialize the screen lock state.
//
// It's not safe to access OWSScreenLock.isScreenLockEnabled
// until the app is ready.
dependencies?[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self, dependencies] in
DispatchQueue.global(qos: .background).async {
self?.isScreenLockLocked = (dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true)
DispatchQueue.main.async {
self?.ensureUI()
}
/// Initialize the screen lock state.
///
/// It's not safe to access `isScreenLockEnabled` in `storage` until the app is ready
dependencies[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self, dependencies] in
self?.isScreenLockLocked = (dependencies[singleton: .storage, key: .isScreenLockEnabled] == true)
switch Thread.isMainThread {
case true: self?.ensureUI()
case false: DispatchQueue.main.async { self?.ensureUI() }
}
}
}
// MARK: - Functions
private func determineDesiredUIState() -> ScreenLockViewController.State {
if isScreenLockLocked {
if dependencies[singleton: .appContext].isNotInForeground {
Log.verbose(.screenLock, "App not in foreground, desiredUIState is: protection.")
return .protection
}
Log.verbose(.screenLock, "App in foreground and locked, desiredUIState is: \(isShowingScreenLockUI ? "protection" : "lock").")
return (isShowingScreenLockUI ? .protection : .lock)
}
if dependencies[singleton: .appContext].isAppForegroundAndActive {
// App is inactive or background.
Log.verbose(.screenLock, "App in foreground and not locked, desiredUIState is: none.")
return .none;
}
if SessionEnvironment.shared?.isRequestingPermission == true {
Log.verbose(.screenLock, "App requesting permissions and not locked, desiredUIState is: none.")
return .none;
}
Log.verbose(.screenLock, "desiredUIState is: protection.")
return .protection;
}
private func tryToActivateScreenLockBasedOnCountdown() {
guard dependencies?[singleton: .appReadiness].isAppReady == true else {
// It's not safe to access OWSScreenLock.isScreenLockEnabled
// until the app is ready.
//
// We don't need to try to lock the screen lock;
// It will be initialized by `setupWithRootWindow`.
Log.verbose("tryToActivateScreenLockUponBecomingActive NO 0")
guard dependencies[singleton: .appReadiness].isAppReady else {
/// It's not safe to access `isScreenLockEnabled` in `storage` until the app is ready
///
/// We don't need to try to lock the screen lock;
/// It will be initialized by `setupWithRootWindow`
Log.verbose(.screenLock, "tryToActivateScreenLockUponBecomingActive NO 0")
return
}
guard dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true else {
// Screen lock is not enabled.
Log.verbose("tryToActivateScreenLockUponBecomingActive NO 1")
return;
guard dependencies[singleton: .storage, key: .isScreenLockEnabled] else {
/// Screen lock is not enabled.
Log.verbose(.screenLock, "tryToActivateScreenLockUponBecomingActive NO 1")
return
}
guard !isScreenLockLocked else {
// Screen lock is already activated.
Log.verbose("tryToActivateScreenLockUponBecomingActive NO 2")
return;
/// Screen lock is already activated.
Log.verbose(.screenLock, "tryToActivateScreenLockUponBecomingActive NO 2")
return
}
self.isScreenLockLocked = true
@ -210,49 +183,52 @@ class ScreenLockUI {
/// * The blocking window has the correct state.
/// * That we show the "iOS auth UI to unlock" if necessary.
private func ensureUI() {
guard dependencies?[singleton: .appReadiness].isAppReady == true else {
dependencies?[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self] in
guard dependencies[singleton: .appReadiness].isAppReady else {
dependencies[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self] in
self?.ensureUI()
}
return
}
let desiredUIState: ScreenLockViewController.State = self.desiredUIState
Log.verbose("ensureUI: \(desiredUIState)")
let desiredUIState: ScreenLockViewController.State = determineDesiredUIState()
Log.verbose(.screenLock, "ensureUI: \(desiredUIState)")
// Show the "iOS auth UI to unlock" if necessary.
/// Show the "iOS auth UI to unlock" if necessary.
if desiredUIState == .lock && !didLastUnlockAttemptFail {
tryToPresentAuthUIToUnlockScreenLock()
}
// Note: We want to regenerate the 'desiredUIState' as if we are about to show the
// 'unlock screen' UI then we shouldn't show the "unlock" button
updateScreenBlockingWindow(state: self.desiredUIState, animated: true)
/// Note: We want to regenerate the `desiredUIState` as if we are about to show the "unlock screen" UI then we
/// shouldn't show the "unlock" button
updateScreenBlockingWindow(state: determineDesiredUIState(), animated: true)
}
private func tryToPresentAuthUIToUnlockScreenLock() {
guard !isShowingScreenLockUI else { return } // We're already showing the auth UI; abort
guard !appIsInactiveOrBackground else { return } // Never show the auth UI unless active
/// If we're already showing the auth UI; or the app isn't active then don't do anything
guard
!isShowingScreenLockUI,
dependencies[singleton: .appContext].isAppForegroundAndActive
else { return }
Log.info("try to unlock screen lock")
Log.info(.screenLock, "Try to unlock screen lock")
isShowingScreenLockUI = true
ScreenLock.shared.tryToUnlockScreenLock(
ScreenLock.tryToUnlockScreenLock(
success: { [weak self] in
Log.info("unlock screen lock succeeded.")
Log.info(.screenLock, "Unlock screen lock succeeded")
self?.isShowingScreenLockUI = false
self?.isScreenLockLocked = false
self?.didUnlockJustSucceed = true
self?.ensureUI()
},
failure: { [weak self] error in
Log.info("unlock screen lock failed.")
Log.info(.screenLock, "Unlock screen lock failed")
self?.clearAuthUIWhenActive()
self?.didLastUnlockAttemptFail = true
self?.showScreenLockFailureAlert(message: "\(error)")
},
unexpectedFailure: { [weak self] error in
Log.info("unlock screen lock unexpectedly failed.")
Log.warn(.screenLock, "Unlock screen lock unexpectedly failed")
// Local Authentication isn't working properly.
// This isn't covered by the docs or the forums but in practice
@ -262,7 +238,7 @@ class ScreenLockUI {
}
},
cancel: { [weak self] in
Log.info("unlock screen lock cancelled.")
Log.info(.screenLock, "Unlock screen lock cancelled")
self?.clearAuthUIWhenActive()
self?.didLastUnlockAttemptFail = true
@ -277,7 +253,7 @@ class ScreenLockUI {
private func showScreenLockFailureAlert(message: String) {
let modal: ConfirmationModal = ConfirmationModal(
targetView: screenBlockingWindow.rootViewController?.view,
targetView: viewController.view,
info: ConfirmationModal.Info(
title: "authenticateFailed".localized(),
body: .text(message),
@ -286,36 +262,7 @@ class ScreenLockUI {
afterClosed: { [weak self] in self?.ensureUI() } // After the alert, update the UI
)
)
screenBlockingWindow.rootViewController?.present(modal, animated: true)
}
/// 'Screen Blocking' window obscures the app screen:
///
/// * In the app switcher.
/// * During 'Screen Lock' unlock process.
private func createScreenBlockingWindow(rootWindow: UIWindow) {
let window: UIWindow = UIWindow(frame: rootWindow.bounds)
window.isHidden = false
window.windowLevel = .background
window.isOpaque = true
window.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary)
let viewController: ScreenLockViewController = ScreenLockViewController { [weak self] in
guard self?.appIsInactiveOrBackground == false else {
// This button can be pressed while the app is inactive
// for a brief window while the iOS auth UI is dismissing.
return
}
Log.info("unlockButtonWasTapped")
self?.didLastUnlockAttemptFail = false
self?.ensureUI()
}
window.rootViewController = viewController
self.screenBlockingWindow = window
self.screenBlockingViewController = viewController
viewController.present(modal, animated: true)
}
/// The "screen blocking" window has three possible states:
@ -327,7 +274,7 @@ class ScreenLockUI {
let shouldShowBlockWindow: Bool = (state != .none)
OWSWindowManager.shared().isScreenBlockActive = shouldShowBlockWindow
self.screenBlockingViewController.updateUI(state: state, animated: animated)
self.viewController.updateUI(state: state, animated: animated)
}
// MARK: - Events
@ -335,7 +282,7 @@ class ScreenLockUI {
private func clearAuthUIWhenActive() {
// For continuity, continue to present blocking screen in "screen lock" mode while
// dismissing the "local auth UI".
if self.appIsInactiveOrBackground {
if !dependencies[singleton: .appContext].isAppForegroundAndActive {
self.shouldClearAuthUIWhenActive = true
}
else {
@ -345,42 +292,61 @@ class ScreenLockUI {
}
@objc private func applicationDidBecomeActive() {
if self.shouldClearAuthUIWhenActive {
self.shouldClearAuthUIWhenActive = false
self.isShowingScreenLockUI = false
if shouldClearAuthUIWhenActive {
shouldClearAuthUIWhenActive = false
isShowingScreenLockUI = false
}
if !didUnlockJustSucceed {
tryToActivateScreenLockBasedOnCountdown()
}
self.appIsInactiveOrBackground = false
didUnlockJustSucceed = false
ensureUI()
}
/// When the OS shows the TouchID/FaceID/Pin UI the application will resign active (and we don't want to re-authenticate if we are
/// already locked)
///
/// Secondly, we need to show the screen protection _before_ we become inactive in order for it to be reflected in the app switcher
@objc private func applicationWillResignActive() {
self.appIsInactiveOrBackground = true
if !isShowingScreenLockUI {
didLastUnlockAttemptFail = false
tryToActivateScreenLockBasedOnCountdown()
}
didUnlockJustSucceed = false
ensureUI()
}
@objc private func applicationWillEnterForeground() {
self.appIsInBackground = false
didUnlockJustSucceed = false
tryToActivateScreenLockBasedOnCountdown()
ensureUI()
}
@objc private func applicationDidEnterBackground() {
self.appIsInBackground = true
didUnlockJustSucceed = false
tryToActivateScreenLockBasedOnCountdown()
ensureUI()
}
/// Whenever the device date/time is edited by the user, trigger screen lock immediately if enabled.
@objc private func clockDidChange() {
Log.info("clock did change")
Log.info(.screenLock, "clock did change")
guard dependencies?[singleton: .appReadiness].isAppReady == true else {
guard dependencies[singleton: .appReadiness].isAppReady == true else {
// It's not safe to access OWSScreenLock.isScreenLockEnabled
// until the app is ready.
//
// We don't need to try to lock the screen lock;
// It will be initialized by `setupWithRootWindow`.
Log.verbose("clockDidChange 0")
Log.verbose(.screenLock, "clockDidChange 0")
return;
}
DispatchQueue.global(qos: .background).async { [dependencies] in
self.isScreenLockLocked = (dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true)
self.isScreenLockLocked = (dependencies[singleton: .storage, key: .isScreenLockEnabled] == true)
DispatchQueue.main.async {
// NOTE: this notifications fires _before_ applicationDidBecomeActive,

@ -9,10 +9,20 @@ import SessionUtilitiesKit
import SessionMessagingKit
import SignalUtilitiesKit
// MARK: - Log.Category
private extension Log.Category {
static let cat: Log.Category = .create("SessionTableViewController", defaultLevel: .info)
}
// MARK: - SessionViewModelAccessible
protocol SessionViewModelAccessible {
var viewModelType: AnyObject.Type { get }
}
// MARK: - SessionTableViewController
class SessionTableViewController<ViewModel>: BaseVC, UITableViewDataSource, UITableViewDelegate, SessionViewModelAccessible where ViewModel: (SessionTableViewModel & ObservableTableSource) {
typealias Section = ViewModel.Section
typealias TableItem = ViewModel.TableItem
@ -235,11 +245,11 @@ class SessionTableViewController<ViewModel>: BaseVC, UITableViewDataSource, UITa
// If we got an error then try to restart the stream once, otherwise log the error
guard self?.dataStreamJustFailed == false else {
SNLog("Unable to recover database stream in '\(title)' settings with error: \(error)")
Log.error(.cat, "Unable to recover database stream in '\(title)' settings with error: \(error)")
return
}
SNLog("Atempting recovery for database stream in '\(title)' settings with error: \(error)")
Log.info(.cat, "Atempting recovery for database stream in '\(title)' settings with error: \(error)")
self?.dataStreamJustFailed = true
self?.startObservingChanges(didReturnFromBackground: didReturnFromBackground)
@ -467,7 +477,7 @@ class SessionTableViewController<ViewModel>: BaseVC, UITableViewDataSource, UITa
cell.update(with: threadInfo.id, using: viewModel.dependencies)
default:
SNLog("[SessionTableViewController] Got invalid combination of cellType: \(viewModel.cellType) and tableData: \(SessionCell.Info<TableItem>.self)")
Log.error(.cat, "[SessionTableViewController] Got invalid combination of cellType: \(viewModel.cellType) and tableData: \(SessionCell.Info<TableItem>.self)")
}
return cell

@ -6,6 +6,14 @@ import Combine
import DifferenceKit
import SessionUtilitiesKit
// MARK: - Log.Category
private extension Log.Category {
static func cat(_ viewModel: Any) -> Log.Category {
return .create("ObservableTableSource", customSuffix: "-\(type(of: viewModel))", defaultLevel: .warn)
}
}
// MARK: - ObservableTableSource
public protocol ObservableTableSource: AnyObject, SectionedTableData {
@ -203,11 +211,10 @@ public enum ObservationBuilder {
scheduling: dependencies[singleton: .scheduler],
onError: { error in
let log: String = [
"[\(type(of: viewModel))]", // stringlint:ignore
"Observation failed with error:", // stringlint:ignore
"\(error)" // stringlint:ignore
].joined(separator: " ")
SNLog(log)
Log.error(.cat(viewModel), log)
subject.send(completion: Subscribers.Completion.failure(error))
},
onChange: { subject.send($0) }
@ -259,11 +266,10 @@ public enum ObservationBuilder {
scheduling: dependencies[singleton: .scheduler],
onError: { error in
let log: String = [
"[\(type(of: viewModel))]", // stringlint:ignore
"Observation failed with error:", // stringlint:ignore
"\(error)" // stringlint:ignore
].joined(separator: " ")
SNLog(log)
Log.error(.cat(viewModel), log)
subject.send(completion: Subscribers.Completion.failure(error))
},
onChange: { subject.send($0) }

@ -17,7 +17,10 @@ protocol PagedObservationSource {
extension PagedObservationSource {
public func didInit(using dependencies: Dependencies) {
dependencies[singleton: .storage].addObserver(pagedDataObserver)
/// Dispatch adding the database observation to a background thread
DispatchQueue.global(qos: .userInitiated).async { [weak pagedDataObserver] in
dependencies[singleton: .storage].addObserver(pagedDataObserver)
}
}
}

@ -18,11 +18,17 @@ public extension Log.Category {
// MARK: - BackgroundPoller
public final class BackgroundPoller {
typealias Pollers = (
currentUser: CurrentUserPoller,
groups: [GroupPoller],
communities: [CommunityPoller]
)
public func poll(using dependencies: Dependencies) -> AnyPublisher<Void, Never> {
let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970
return dependencies[singleton: .storage]
.readPublisher { db -> (Set<String>, Set<String>) in
.readPublisher { db -> (Set<String>, Set<String>, [String]) in
(
try ClosedGroup
.select(.threadId)
@ -46,16 +52,36 @@ public final class BackgroundPoller {
)
.distinct()
.asRequest(of: String.self)
.fetchSet(db)
.fetchSet(db),
try OpenGroup
.select(.roomToken)
.filter(
OpenGroup.Columns.roomToken != "" &&
OpenGroup.Columns.isActive &&
OpenGroup.Columns.pollFailureCount < CommunityPoller.maxRoomFailureCountForBackgroundPoll
)
.distinct()
.asRequest(of: String.self)
.fetchAll(db)
)
}
.catch { _ in Just(([], [])).eraseToAnyPublisher() }
.catch { _ in Just(([], [], [])).eraseToAnyPublisher() }
.handleEvents(
receiveOutput: { groupIds, servers in
Log.info(.backgroundPoller, "Fetching Users: 1, Groups: \(groupIds.count), Communities: \(servers.count).")
receiveOutput: { groupIds, servers, rooms in
Log.info(.backgroundPoller, "Fetching Users: 1, Groups: \(groupIds.count), Communities: \(servers.count) (\(rooms.count) room(s)).")
}
)
.map { groupIds, servers -> ([GroupPoller], [CommunityPoller]) in
.map { groupIds, servers, _ -> Pollers in
let currentUserPoller: CurrentUserPoller = CurrentUserPoller(
pollerName: "Background Main Poller",
pollerQueue: DispatchQueue.main,
pollerDestination: .swarm(dependencies[cache: .general].sessionId.hexString),
pollerDrainBehaviour: .limitedReuse(count: 6),
namespaces: CurrentUserPoller.namespaces,
shouldStoreMessages: true,
logStartAndStopCalls: false,
using: dependencies
)
let groupPollers: [GroupPoller] = groupIds.map { groupId in
GroupPoller(
pollerName: "Background Group poller for: \(groupId)", // stringlint:ignore
@ -80,16 +106,18 @@ public final class BackgroundPoller {
)
}
return (groupPollers, communityPollers)
return (currentUserPoller, groupPollers, communityPollers)
}
.flatMap { groupPollers, communityPollers in
.flatMap { currentUserPoller, groupPollers, communityPollers in
/// Need to map back to the pollers to ensure they don't get released until after the polling finishes
Publishers.MergeMany(
[BackgroundPoller.pollUserMessages(using: dependencies)]
[BackgroundPoller.pollUserMessages(poller: currentUserPoller, using: dependencies)]
.appending(contentsOf: BackgroundPoller.poll(pollers: groupPollers, using: dependencies))
.appending(contentsOf: BackgroundPoller.poll(pollerInfo: communityPollers, using: dependencies))
)
.collect()
.map { _ in (currentUserPoller, groupPollers, communityPollers) }
}
.collect()
.map { _ in () }
.handleEvents(
receiveOutput: { _ in
@ -102,18 +130,9 @@ public final class BackgroundPoller {
}
private static func pollUserMessages(
poller: CurrentUserPoller,
using dependencies: Dependencies
) -> AnyPublisher<Void, Never> {
let poller: CurrentUserPoller = CurrentUserPoller(
pollerName: "Background Main Poller",
pollerQueue: DispatchQueue.main,
pollerDestination: .swarm(dependencies[cache: .general].sessionId.hexString),
pollerDrainBehaviour: .limitedReuse(count: 6),
namespaces: CurrentUserPoller.namespaces,
shouldStoreMessages: true,
logStartAndStopCalls: false,
using: dependencies
)
let pollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970
return poller
@ -182,10 +201,10 @@ public final class BackgroundPoller {
return poller
.pollFromBackground()
.handleEvents(
receiveOutput: { [pollerName = poller.pollerName] _ in
receiveOutput: { [pollerName = poller.pollerName] _, _, rawMessageCount, _ in
let endTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970
let duration: TimeUnit = .seconds(endTime - pollStart)
Log.info(.backgroundPoller, "\(pollerName) succeeded after \(duration, unit: .s).")
Log.info(.backgroundPoller, "\(pollerName) received \(rawMessageCount) message(s) succeeded after \(duration, unit: .s).")
},
receiveCompletion: { [pollerName = poller.pollerName] result in
switch result {

@ -115,6 +115,7 @@ public enum MentionUtilities {
result.addAttribute(.currentUserMentionBackgroundCornerRadius, value: (8 * sizeDiff), range: mention.range)
result.addAttribute(.currentUserMentionBackgroundPadding, value: (3 * sizeDiff), range: mention.range)
result.addAttribute(.currentUserMentionBackgroundColor, value: primaryColor.color, range: mention.range)
result.addAttribute(.kern, value: (3 * sizeDiff), range: NSRange(location: mention.range.upperBound, length: 1))
}
switch (location, mention.isCurrentUser, theme.interfaceStyle) {

@ -476,7 +476,7 @@ extension Attachment {
return try builder.build()
}
catch {
SNLog("Couldn't construct attachment proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct attachment proto from: \(self).")
return nil
}
}

@ -625,6 +625,28 @@ public extension ClosedGroup {
}
}
public extension Collection where Element == (String, Profile?) {
func sortedById(userSessionId: SessionId) -> [Element] {
return sorted { lhs, rhs in
guard lhs.0 != userSessionId.hexString else { return true }
guard rhs.0 != userSessionId.hexString else { return false }
return (lhs.0 < rhs.0)
}
}
}
public extension Collection where Element == String {
func sortedById(userSessionId: SessionId) -> [Element] {
return sorted { lhs, rhs in
guard lhs != userSessionId.hexString else { return true }
guard rhs != userSessionId.hexString else { return false }
return (lhs < rhs)
}
}
}
public extension [ClosedGroup.RemovableGroupData] {
static var allData: [ClosedGroup.RemovableGroupData] { ClosedGroup.RemovableGroupData.allCases }
static var noData: [ClosedGroup.RemovableGroupData] { [] }

@ -657,23 +657,44 @@ public extension Interaction {
) throws -> Set<Int64> {
guard db[.areReadReceiptsEnabled] == true else { return [] }
struct InterationRowState: Codable, FetchableRecord {
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey {
case rowId
case state
}
var rowId: Int64
var state: Interaction.State
}
// Get the row ids for the interactions which should be updated
let rowIds: [Int64] = try Interaction
.select(Column.rowID)
let interactionInfo: [InterationRowState] = try Interaction
.select(Column.rowID.forKey(InterationRowState.Columns.rowId), Interaction.Columns.state)
.filter(Interaction.Columns.threadId == threadId)
.filter(timestampMsValues.contains(Columns.timestampMs))
.filter(Variant.variantsWhichSupportReadReceipts.contains(Columns.variant))
.asRequest(of: Int64.self)
.asRequest(of: InterationRowState.self)
.fetchAll(db)
// If there were no 'rowIds' then no need to run the below queries, all of the timestamps
// and for pending read receipts
guard !rowIds.isEmpty else { return timestampMsValues.asSet() }
// If there were no 'interactionInfo' then no need to run the below queries, all of the
// timestamps are for pending read receipts
guard !interactionInfo.isEmpty else { return timestampMsValues.asSet() }
let allRowIds: Set<Int64> = Set(interactionInfo.map { $0.rowId })
let sentInteractionIds: Set<Int64> = interactionInfo
.filter { $0.state != .sending }
.map { $0.rowId }
.asSet()
let sendingInteractionInfo: Set<Int64> = interactionInfo
.filter { $0.state == .sending }
.map { $0.rowId }
.asSet()
// Update the 'recipientReadTimestampMs' if it doesn't match (need to do this to prevent
// the UI update from being triggered for a redundant update)
try Interaction
.filter(rowIds.contains(Column.rowID))
.filter(sentInteractionIds.contains(Column.rowID))
.filter(Interaction.Columns.recipientReadTimestampMs == nil)
.updateAll(
db,
@ -683,7 +704,7 @@ public extension Interaction {
// If the message still appeared to be sending then mark it as sent (can also remove the
// failure text as it's redundant if the message is in the sent state)
try Interaction
.filter(rowIds.contains(Column.rowID))
.filter(sendingInteractionInfo.contains(Column.rowID))
.filter(Interaction.Columns.state == Interaction.State.sending)
.updateAll(
db,
@ -694,7 +715,7 @@ public extension Interaction {
// Retrieve the set of timestamps which were updated
let timestampsUpdated: Set<Int64> = try Interaction
.select(Columns.timestampMs)
.filter(rowIds.contains(Column.rowID))
.filter(allRowIds.contains(Column.rowID))
.filter(timestampMsValues.contains(Columns.timestampMs))
.filter(Variant.variantsWhichSupportReadReceipts.contains(Columns.variant))
.asRequest(of: Int64.self)

@ -187,7 +187,7 @@ public extension Profile {
return try dataMessageProto.build()
}
catch {
SNLog("Couldn't construct profile proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct profile proto from: \(self).")
return nil
}
}

@ -72,9 +72,11 @@ public enum CheckForAppUpdatesJob: JobExecutor {
},
receiveValue: { _, versionInfo in
switch versionInfo.prerelease {
case .none: Log.info(.cat, "Latest version: \(versionInfo.version)")
case .none:
Log.info(.cat, "Latest version: \(versionInfo.version) (Current: \(dependencies[cache: .appVersion].versionInfo))")
case .some(let prerelease):
Log.info(.cat, "Latest version: \(versionInfo.version), pre-release version: \(prerelease.version)")
Log.info(.cat, "Latest version: \(versionInfo.version), pre-release version: \(prerelease.version) (Current: \(dependencies[cache: .appVersion].versionInfo))")
}
}
)

@ -55,11 +55,8 @@ public enum ConfigMessageReceiveJob: JobExecutor {
return failure(job, JobRunnerError.missingRequiredDetails, true)
}
var lastError: Error?
dependencies[singleton: .storage].write { db in
// Send any SharedConfigMessages to the LibSession to handle it
do {
dependencies[singleton: .storage].writeAsync(
updates: { db in
try dependencies.mutate(cache: .libSession) { cache in
try cache.handleConfigMessages(
db,
@ -67,19 +64,19 @@ public enum ConfigMessageReceiveJob: JobExecutor {
messages: details.messages
)
}
}
catch { lastError = error }
}
// Handle the result
switch lastError {
case .some(let error):
Log.error(.cat, "Couldn't receive config message due to error: \(error)")
removeDependencyOnMessageReceiveJobs()
failure(job, error, true)
},
completion: { result in
// Handle the result
switch result {
case .failure(let error):
Log.error(.cat, "Couldn't receive config message due to error: \(error)")
removeDependencyOnMessageReceiveJobs()
failure(job, error, true)
case .none: success(job, false)
}
case .success: success(job, false)
}
}
)
}
}

@ -51,65 +51,73 @@ public enum MessageReceiveJob: JobExecutor {
}
}
dependencies[singleton: .storage].write { db in
for (messageInfo, protoContent) in messageData {
do {
try MessageReceiver.handle(
db,
threadId: threadId,
threadVariant: messageInfo.threadVariant,
message: messageInfo.message,
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
associatedWithProto: protoContent,
using: dependencies
)
dependencies[singleton: .storage].writeAsync(
updates: { db -> Error? in
for (messageInfo, protoContent) in messageData {
do {
try MessageReceiver.handle(
db,
threadId: threadId,
threadVariant: messageInfo.threadVariant,
message: messageInfo.message,
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
associatedWithProto: protoContent,
using: dependencies
)
}
catch {
// If the current message is a permanent failure then override it with the
// new error (we want to retry if there is a single non-permanent error)
switch error {
// Ignore duplicate and self-send errors (these will usually be caught during
// parsing but sometimes can get past and conflict at database insertion - eg.
// for open group messages) we also don't bother logging as it results in
// excessive logging which isn't useful)
case DatabaseError.SQLITE_CONSTRAINT_UNIQUE,
DatabaseError.SQLITE_CONSTRAINT, // Sometimes thrown for UNIQUE
MessageReceiverError.duplicateMessage,
MessageReceiverError.duplicateControlMessage,
MessageReceiverError.selfSend:
break
case let receiverError as MessageReceiverError where !receiverError.isRetryable:
Log.error(.cat, "Permanently failed message due to error: \(error)")
continue
default:
Log.error(.cat, "Couldn't receive message due to error: \(error)")
lastError = error
// We failed to process this message but it is a retryable error
// so add it to the list to re-process
remainingMessagesToProcess.append(messageInfo)
}
}
}
catch {
// If the current message is a permanent failure then override it with the
// new error (we want to retry if there is a single non-permanent error)
switch error {
// Ignore duplicate and self-send errors (these will usually be caught during
// parsing but sometimes can get past and conflict at database insertion - eg.
// for open group messages) we also don't bother logging as it results in
// excessive logging which isn't useful)
case DatabaseError.SQLITE_CONSTRAINT_UNIQUE,
DatabaseError.SQLITE_CONSTRAINT, // Sometimes thrown for UNIQUE
MessageReceiverError.duplicateMessage,
MessageReceiverError.duplicateControlMessage,
MessageReceiverError.selfSend:
break
case let receiverError as MessageReceiverError where !receiverError.isRetryable:
Log.error(.cat, "Permanently failed message due to error: \(error)")
continue
// If any messages failed to process then we want to update the job to only include
// those failed messages
guard !remainingMessagesToProcess.isEmpty else { return nil }
updatedJob = try job
.with(details: Details(messages: remainingMessagesToProcess))
.defaulting(to: job)
.upserted(db)
return lastError
},
completion: { result in
// Handle the result
switch result {
case .failure(let error): failure(updatedJob, error, false)
case .success(.some(let error as MessageReceiverError)) where !error.isRetryable:
failure(updatedJob, error, true)
default:
Log.error(.cat, "Couldn't receive message due to error: \(error)")
lastError = error
// We failed to process this message but it is a retryable error
// so add it to the list to re-process
remainingMessagesToProcess.append(messageInfo)
}
case .success(.some(let error)): failure(updatedJob, error, false)
case .success: success(updatedJob, false)
}
}
// If any messages failed to process then we want to update the job to only include
// those failed messages
guard !remainingMessagesToProcess.isEmpty else { return }
updatedJob = try job
.with(details: Details(messages: remainingMessagesToProcess))
.defaulting(to: job)
.upserted(db)
}
// Handle the result
switch lastError {
case let error as MessageReceiverError where !error.isRetryable: failure(updatedJob, error, true)
case .some(let error): failure(updatedJob, error, false)
case .none: success(updatedJob, false)
}
)
}
}

@ -141,7 +141,7 @@ internal extension LibSession {
try validChanges.forEach { threadInfo in
guard var cThreadId: [CChar] = threadInfo.threadId.cString(using: .utf8) else {
SNLog("Unable to upsert contact volatile info to LibSession: \(LibSessionError.invalidCConversion)")
Log.error(.libSession, "Unable to upsert contact volatile info to LibSession: \(LibSessionError.invalidCConversion)")
throw LibSessionError.invalidCConversion
}
@ -200,7 +200,7 @@ internal extension LibSession {
var cRoomToken: [CChar] = threadInfo.openGroupUrlInfo?.roomToken.cString(using: .utf8),
var cPubkey: [UInt8] = threadInfo.openGroupUrlInfo.map({ Array(Data(hex: $0.publicKey)) })
else {
SNLog("Unable to create community conversation when updating last read timestamp due to missing URL info")
Log.error(.libSession, "Unable to create community conversation when updating last read timestamp due to missing URL info")
return
}
@ -747,7 +747,7 @@ public extension LibSession {
)
}
else {
SNLog("Ignoring unknown conversation type when iterating through volatile conversation info update")
Log.error(.libSession, "Ignoring unknown conversation type when iterating through volatile conversation info update")
}
convo_info_volatile_iterator_advance(convoIterator)

@ -29,7 +29,7 @@ internal extension LibSessionCacheType {
guard groupState[.groupKeys] != nil && groupState[.groupInfo] != nil && groupState[.groupMembers] != nil else {
Log.error(.libSession, "Group config objects were null")
throw LibSessionError.unableToCreateConfigObject
throw LibSessionError.unableToCreateConfigObject(groupSessionId.hexString)
}
groupState.forEach { variant, config in
@ -80,7 +80,7 @@ internal extension LibSession {
// Extract the conf objects from the state to load in the initial data
guard case .groupKeys(let groupKeysConf, let groupInfoConf, let groupMembersConf) = groupState[.groupKeys] else {
Log.error(.libSession, "Group config objects were null")
throw LibSessionError.unableToCreateConfigObject
throw LibSessionError.unableToCreateConfigObject(groupSessionId.hexString)
}
// Set the initial values in the confs
@ -219,7 +219,7 @@ internal extension LibSession {
nil,
0,
&error
).orThrow(error: error)
).orThrow(error: error, groupSessionId: groupSessionId)
try groups_members_init(
&groupMembersConf,
&groupIdentityPublicKey,
@ -227,7 +227,7 @@ internal extension LibSession {
nil,
0,
&error
).orThrow(error: error)
).orThrow(error: error, groupSessionId: groupSessionId)
try groups_keys_init(
&groupKeysConf,
@ -239,7 +239,7 @@ internal extension LibSession {
nil,
0,
&error
).orThrow(error: error)
).orThrow(error: error, groupSessionId: groupSessionId)
case .none:
try groups_info_init(
@ -249,7 +249,7 @@ internal extension LibSession {
nil,
0,
&error
).orThrow(error: error)
).orThrow(error: error, groupSessionId: groupSessionId)
try groups_members_init(
&groupMembersConf,
&groupIdentityPublicKey,
@ -257,7 +257,7 @@ internal extension LibSession {
nil,
0,
&error
).orThrow(error: error)
).orThrow(error: error, groupSessionId: groupSessionId)
try groups_keys_init(
&groupKeysConf,
@ -269,7 +269,7 @@ internal extension LibSession {
nil,
0,
&error
).orThrow(error: error)
).orThrow(error: error, groupSessionId: groupSessionId)
}
guard
@ -278,7 +278,7 @@ internal extension LibSession {
let membersConf: UnsafeMutablePointer<config_object> = groupMembersConf
else {
Log.error(.libSession, "Group config objects were null")
throw LibSessionError.unableToCreateConfigObject
throw LibSessionError.unableToCreateConfigObject(groupSessionId.hexString)
}
// Define the config state map and load it into memory
@ -360,11 +360,11 @@ internal extension LibSessionCacheType {
}
private extension Int32 {
func orThrow(error: [CChar]) throws {
func orThrow(error: [CChar], groupSessionId: SessionId) throws {
guard self != 0 else { return }
Log.error(.libSession, "Unable to create group config objects: \(String(cString: error))")
throw LibSessionError.unableToCreateConfigObject
throw LibSessionError.unableToCreateConfigObject(groupSessionId.hexString)
}
}

@ -722,7 +722,7 @@ internal extension LibSession {
var cBaseUrl: [CChar] = community.urlInfo.server.cString(using: .utf8),
var cRoom: [CChar] = community.urlInfo.roomToken.cString(using: .utf8)
else {
SNLog("Unable to upsert community conversation to LibSession: \(LibSessionError.invalidCConversion)")
Log.error(.libSession, "Unable to upsert community conversation to LibSession: \(LibSessionError.invalidCConversion)")
throw LibSessionError.invalidCConversion
}

@ -205,89 +205,99 @@ public extension LibSession {
// MARK: - State Management
public func loadState(_ db: Database) {
public func loadState(_ db: Database, requestId: String?) {
// Ensure we have the ed25519 key and that we haven't already loaded the state before
// we continue
guard
configStore.isEmpty,
let ed25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db)
else { return Log.warn(.libSession, "Ignoring loadState due to existing state") }
else { return Log.warn(.libSession, "Ignoring loadState\(requestId.map { " for \($0)" } ?? "") due to existing state") }
// Retrieve the existing dumps from the database
let existingDumps: [ConfigDump] = ((try? ConfigDump.fetchSet(db)) ?? [])
.sorted { lhs, rhs in lhs.variant.loadOrder < rhs.variant.loadOrder }
let existingDumpVariants: Set<ConfigDump.Variant> = existingDumps
.map { $0.variant }
.asSet()
let missingRequiredVariants: Set<ConfigDump.Variant> = ConfigDump.Variant.userVariants
.subtracting(existingDumpVariants)
let groupsByKey: [String: ClosedGroup] = (try? ClosedGroup
/// Retrieve the existing dumps from the database
typealias ConfigInfo = (sessionId: SessionId, variant: ConfigDump.Variant, dump: ConfigDump?)
let existingDumpsByKey: [String: [ConfigDump]] = ((try? ConfigDump.fetchAll(db)) ?? [])
.grouped(by: \.sessionId.hexString)
var configsToLoad: [ConfigInfo] = []
/// Load in the user dumps first (it's possible for a user dump to be missing due to some edge-cases so use
/// `ConfigDump.Variant.userVariants` to ensure we will at least load a default state and just assume
/// it will be fixed when we eventually poll for it)
configsToLoad.append(
contentsOf: ConfigDump.Variant.userVariants
.sorted { $0.loadOrder < $1.loadOrder }
.map { variant in
(
userSessionId,
variant,
existingDumpsByKey[userSessionId.hexString]?
.first(where: { $0.variant == variant })
)
}
)
/// Then load in dumps for groups
///
/// Similar to the above it's possible to have a partial group state due to edge-cases where a config could be lost, but also
/// immediately after creating a group (eg. when a crash happens at the right time), for these cases we again assume they
/// will be solved eventually via polling so still want to load their states into memory (if we don't then we likely wouldn't be
/// able to decrypt the poll response and the group would never recover)
///
/// **Note:** We exclude groups in the `invited` state as they should only have their state loaded once the invitation
/// gets accepted
let allGroups: [ClosedGroup] = (try? ClosedGroup
.filter(
ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue &&
ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString
)
.fetchAll(db)
.reduce(into: [:]) { result, next in result[next.threadId] = next })
.defaulting(to: [:])
let groupsWithNoDumps: [ClosedGroup] = groupsByKey
.values
.filter { group in !existingDumps.contains(where: { $0.sessionId.hexString == group.id }) }
// Create the config records for each dump
existingDumps.forEach { dump in
configStore[dump.sessionId, dump.variant] = try? loadState(
for: dump.variant,
sessionId: dump.sessionId,
.filter(ClosedGroup.Columns.invited == false)
.fetchAll(db))
.defaulting(to: [])
let groupsByKey: [String: ClosedGroup] = allGroups
.reduce(into: [:]) { result, group in result[group.threadId] = group }
allGroups.forEach { group in
configsToLoad.append(
contentsOf: ConfigDump.Variant.groupVariants
.sorted { $0.loadOrder < $1.loadOrder }
.map { variant in
(
SessionId(.group, hex: group.threadId),
variant,
existingDumpsByKey[group.threadId]?
.first(where: { $0.variant == variant })
)
}
)
}
/// Now that we have fully populated and sorted `configsToLoad` we should load each into memory
configsToLoad.forEach { sessionId, variant, dump in
configStore[sessionId, variant] = try? loadState(
for: variant,
sessionId: sessionId,
userEd25519SecretKey: ed25519KeyPair.secretKey,
groupEd25519SecretKey: groupsByKey[dump.sessionId.hexString]?
groupEd25519SecretKey: groupsByKey[sessionId.hexString]?
.groupIdentityPrivateKey
.map { Array($0) },
cachedData: dump.data
cachedData: dump?.data
)
}
/// It's possible for there to not be dumps for all of the configs so we load any missing ones to ensure functionality
/// works smoothly
///
/// It's also possible for a group to get created but for a dump to not be created (eg. when a crash happens at the right time), to
/// handle this we also load the state of any groups which don't have dumps if they aren't in the `invited` state (those in
/// the `invited` state will have their state loaded if the invite is accepted)
loadDefaultStatesFor(
userConfigVariants: missingRequiredVariants,
groups: groupsWithNoDumps,
userSessionId: userSessionId,
userEd25519KeyPair: ed25519KeyPair
)
Log.info(.libSession, "Completed loadState")
Log.info(.libSession, "Completed loadState\(requestId.map { " for \($0)" } ?? "")")
}
public func loadDefaultStatesFor(
userConfigVariants: Set<ConfigDump.Variant>,
groups: [ClosedGroup],
userSessionId: SessionId,
userEd25519KeyPair: KeyPair
public func loadDefaultStateFor(
variant: ConfigDump.Variant,
sessionId: SessionId,
userEd25519KeyPair: KeyPair,
groupEd25519SecretKey: [UInt8]?
) {
/// Create an empty state for the specified user config variants
userConfigVariants.forEach { variant in
configStore[userSessionId, variant] = try? loadState(
for: variant,
sessionId: userSessionId,
userEd25519SecretKey: userEd25519KeyPair.secretKey,
groupEd25519SecretKey: nil,
cachedData: nil
)
}
/// Create empty group states for the provided groups
groups
.filter { $0.invited != true }
.forEach { group in
_ = try? createAndLoadGroupState(
groupSessionId: SessionId(.group, hex: group.id),
userED25519KeyPair: userEd25519KeyPair,
groupIdentityPrivateKey: group.groupIdentityPrivateKey
)
}
configStore[sessionId, variant] = try? loadState(
for: variant,
sessionId: sessionId,
userEd25519SecretKey: userEd25519KeyPair.secretKey,
groupEd25519SecretKey: groupEd25519SecretKey,
cachedData: nil
)
}
internal func loadState(
@ -323,8 +333,8 @@ public extension LibSession {
switch (variant, groupEd25519SecretKey) {
case (.invalid, _):
throw LibSessionError.unableToCreateConfigObject
.logging("Unable to create \(variant.rawValue) config object")
throw LibSessionError.unableToCreateConfigObject(sessionId.hexString)
.logging("Unable to create \(variant.rawValue) config object for: \(sessionId.hexString)")
case (.userProfile, _), (.contacts, _), (.convoInfoVolatile, _), (.userGroups, _):
return try (userConfigInitCalls[variant]?(
@ -334,7 +344,7 @@ public extension LibSession {
(cachedDump?.length ?? 0),
&error
))
.toConfig(conf, variant: variant, error: error)
.toConfig(conf, variant: variant, error: error, sessionId: sessionId)
case (.groupInfo, .some(var adminSecretKey)), (.groupMembers, .some(var adminSecretKey)):
var identityPublicKey: [UInt8] = sessionId.publicKey
@ -347,7 +357,7 @@ public extension LibSession {
(cachedDump?.length ?? 0),
&error
))
.toConfig(conf, variant: variant, error: error)
.toConfig(conf, variant: variant, error: error, sessionId: sessionId)
case (.groupKeys, .some(var adminSecretKey)):
var identityPublicKey: [UInt8] = sessionId.publicKey
@ -356,8 +366,8 @@ public extension LibSession {
case .groupInfo(let infoConf) = configStore[sessionId, .groupInfo],
case .groupMembers(let membersConf) = configStore[sessionId, .groupMembers]
else {
throw LibSessionError.unableToCreateConfigObject
.logging("Unable to create \(variant.rawValue) config object for \(sessionId): Group info and member config states not loaded")
throw LibSessionError.unableToCreateConfigObject(sessionId.hexString)
.logging("Unable to create \(variant.rawValue) config object for \(sessionId), group info \(configStore[sessionId, .groupInfo] != nil ? "loaded" : "not loaded") and member config \(configStore[sessionId, .groupMembers] != nil ? "loaded" : "not loaded")")
}
return try groups_keys_init(
@ -371,7 +381,7 @@ public extension LibSession {
(cachedDump?.length ?? 0),
&error
)
.toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error)
.toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error, sessionId: sessionId)
// It looks like C doesn't deal will passing pointers to null variables well so we need
// to explicitly pass 'nil' for the admin key in this case
@ -386,7 +396,7 @@ public extension LibSession {
(cachedDump?.length ?? 0),
&error
))
.toConfig(conf, variant: variant, error: error)
.toConfig(conf, variant: variant, error: error, sessionId: sessionId)
// It looks like C doesn't deal will passing pointers to null variables well so we need
// to explicitly pass 'nil' for the admin key in this case
@ -397,8 +407,8 @@ public extension LibSession {
case .groupInfo(let infoConf) = configStore[sessionId, .groupInfo],
case .groupMembers(let membersConf) = configStore[sessionId, .groupMembers]
else {
throw LibSessionError.unableToCreateConfigObject
.logging("Unable to create \(variant.rawValue) config object for \(sessionId): Group info and member config states not loaded")
throw LibSessionError.unableToCreateConfigObject(sessionId.hexString)
.logging("Unable to create \(variant.rawValue) config object for \(sessionId), group info \(configStore[sessionId, .groupInfo] != nil ? "loaded" : "not loaded") and member config \(configStore[sessionId, .groupMembers] != nil ? "loaded" : "not loaded")")
}
return try groups_keys_init(
@ -412,7 +422,7 @@ public extension LibSession {
(cachedDump?.length ?? 0),
&error
)
.toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error)
.toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error, sessionId: sessionId)
}
}
@ -894,12 +904,12 @@ public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheT
// MARK: - State Management
func loadState(_ db: Database)
func loadDefaultStatesFor(
userConfigVariants: Set<ConfigDump.Variant>,
groups: [ClosedGroup],
userSessionId: SessionId,
userEd25519KeyPair: KeyPair
func loadState(_ db: Database, requestId: String?)
func loadDefaultStateFor(
variant: ConfigDump.Variant,
sessionId: SessionId,
userEd25519KeyPair: KeyPair,
groupEd25519SecretKey: [UInt8]?
)
func hasConfig(for variant: ConfigDump.Variant, sessionId: SessionId) -> Bool
func config(for variant: ConfigDump.Variant, sessionId: SessionId) -> LibSession.Config?
@ -974,6 +984,10 @@ public extension LibSessionCacheType {
func withCustomBehaviour(_ behaviour: LibSession.CacheBehaviour, for sessionId: SessionId, change: @escaping () throws -> ()) throws {
try withCustomBehaviour(behaviour, for: sessionId, variant: nil, change: change)
}
func loadState(_ db: Database) {
loadState(db, requestId: nil)
}
}
private final class NoopLibSessionCache: LibSessionCacheType {
@ -987,12 +1001,12 @@ private final class NoopLibSessionCache: LibSessionCacheType {
// MARK: - State Management
func loadState(_ db: Database) {}
func loadDefaultStatesFor(
userConfigVariants: Set<ConfigDump.Variant>,
groups: [ClosedGroup],
userSessionId: SessionId,
userEd25519KeyPair: KeyPair
func loadState(_ db: Database, requestId: String?) {}
func loadDefaultStateFor(
variant: ConfigDump.Variant,
sessionId: SessionId,
userEd25519KeyPair: KeyPair,
groupEd25519SecretKey: [UInt8]?
) {}
func hasConfig(for variant: ConfigDump.Variant, sessionId: SessionId) -> Bool { return false }
func config(for variant: ConfigDump.Variant, sessionId: SessionId) -> LibSession.Config? { return nil }
@ -1065,10 +1079,11 @@ private extension Optional where Wrapped == Int32 {
func toConfig(
_ maybeConf: UnsafeMutablePointer<config_object>?,
variant: ConfigDump.Variant,
error: [CChar]
error: [CChar],
sessionId: SessionId
) throws -> LibSession.Config {
guard self == 0, let conf: UnsafeMutablePointer<config_object> = maybeConf else {
throw LibSessionError.unableToCreateConfigObject
throw LibSessionError.unableToCreateConfigObject(sessionId.hexString)
.logging("Unable to create \(variant.rawValue) config object: \(String(cString: error))")
}
@ -1080,7 +1095,7 @@ private extension Optional where Wrapped == Int32 {
case .groupInfo: return .groupInfo(conf)
case .groupMembers: return .groupMembers(conf)
case .groupKeys, .invalid: throw LibSessionError.unableToCreateConfigObject
case .groupKeys, .invalid: throw LibSessionError.unableToCreateConfigObject(sessionId.hexString)
}
}
}
@ -1091,16 +1106,17 @@ private extension Int32 {
info: UnsafeMutablePointer<config_object>,
members: UnsafeMutablePointer<config_object>,
variant: ConfigDump.Variant,
error: [CChar]
error: [CChar],
sessionId: SessionId
) throws -> LibSession.Config {
guard self == 0, let conf: UnsafeMutablePointer<config_group_keys> = maybeConf else {
throw LibSessionError.unableToCreateConfigObject
throw LibSessionError.unableToCreateConfigObject(sessionId.hexString)
.logging("Unable to create \(variant.rawValue) config object: \(String(cString: error))")
}
switch variant {
case .groupKeys: return .groupKeys(conf, info: info, members: members)
default: throw LibSessionError.unableToCreateConfigObject
default: throw LibSessionError.unableToCreateConfigObject(sessionId.hexString)
}
}
}

@ -201,7 +201,7 @@ public final class CallMessage: ControlMessage {
return try contentProto.build()
}
catch {
SNLog("Couldn't construct call message proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct call message proto from: \(self).")
return nil
}
}

@ -176,7 +176,7 @@ public final class ClosedGroupControlMessage: ControlMessage {
do {
return try result.build()
} catch {
SNLog("Couldn't construct key pair wrapper proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct key pair wrapper proto from: \(self).")
return nil
}
}
@ -298,7 +298,7 @@ public final class ClosedGroupControlMessage: ControlMessage {
public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? {
guard let kind = kind else {
SNLog("Couldn't construct closed group update proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct closed group update proto from: \(self).")
return nil
}
do {
@ -312,7 +312,7 @@ public final class ClosedGroupControlMessage: ControlMessage {
do {
closedGroupControlMessage.setEncryptionKeyPair(try encryptionKeyPairAsProto.build())
} catch {
SNLog("Couldn't construct closed group update proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct closed group update proto from: \(self).")
return nil
}
closedGroupControlMessage.setMembers(members)
@ -346,7 +346,7 @@ public final class ClosedGroupControlMessage: ControlMessage {
contentProto.setDataMessage(try dataMessageProto.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct closed group update proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct closed group update proto from: \(self).")
return nil
}
}

@ -85,7 +85,7 @@ public final class DataExtractionNotification: ControlMessage {
public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? {
guard let kind = kind else {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
do {
@ -104,7 +104,7 @@ public final class DataExtractionNotification: ControlMessage {
setDisappearingMessagesConfigurationIfNeeded(on: contentProto)
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}

@ -64,7 +64,7 @@ public final class ExpirationTimerUpdate: ControlMessage {
contentProto.setDataMessage(try dataMessageProto.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct expiration timer update proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct expiration timer update proto from: \(self).")
return nil
}
}

@ -141,7 +141,7 @@ public final class GroupUpdateDeleteMemberContentMessage: ControlMessage {
contentProto.setDataMessage(try dataMessage.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}

@ -167,7 +167,7 @@ public final class GroupUpdateInfoChangeMessage: ControlMessage {
contentProto.setDataMessage(try dataMessage.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}

@ -170,7 +170,7 @@ public final class GroupUpdateInviteMessage: ControlMessage {
contentProto.setDataMessage(try dataMessage.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}

@ -87,7 +87,7 @@ public final class GroupUpdateInviteResponseMessage: ControlMessage {
contentProto.setDataMessage(try dataMessage.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}

@ -162,7 +162,7 @@ public final class GroupUpdateMemberChangeMessage: ControlMessage {
contentProto.setDataMessage(try dataMessage.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}

@ -43,7 +43,7 @@ public final class GroupUpdateMemberLeftMessage: ControlMessage {
contentProto.setDataMessage(try dataMessage.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}

@ -43,7 +43,7 @@ public final class GroupUpdateMemberLeftNotificationMessage: ControlMessage {
contentProto.setDataMessage(try dataMessage.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}

@ -95,7 +95,7 @@ public final class GroupUpdatePromoteMessage: ControlMessage {
contentProto.setDataMessage(try dataMessage.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}

@ -77,7 +77,7 @@ public final class MessageRequestResponse: ControlMessage {
contentProto.setMessageRequestResponse(try messageRequestResponseProto.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct unsend request proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct unsend request proto from: \(self).")
return nil
}
}

@ -56,7 +56,7 @@ public final class ReadReceipt: ControlMessage {
public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? {
guard let timestamps = timestamps else {
SNLog("Couldn't construct read receipt proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct read receipt proto from: \(self).")
return nil
}
let receiptProto = SNProtoReceiptMessage.builder(type: .read)
@ -69,7 +69,7 @@ public final class ReadReceipt: ControlMessage {
setDisappearingMessagesConfigurationIfNeeded(on: contentProto)
return try contentProto.build()
} catch {
SNLog("Couldn't construct read receipt proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct read receipt proto from: \(self).")
return nil
}
}

@ -84,7 +84,7 @@ public final class TypingIndicator: ControlMessage {
public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? {
guard let timestampMs = sentTimestampMs, let kind = kind else {
SNLog("Couldn't construct typing indicator proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct typing indicator proto from: \(self).")
return nil
}
let typingIndicatorProto = SNProtoTypingMessage.builder(timestamp: timestampMs, action: kind.toProto())
@ -94,7 +94,7 @@ public final class TypingIndicator: ControlMessage {
contentProto.setTypingMessage(try typingIndicatorProto.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct typing indicator proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct typing indicator proto from: \(self).")
return nil
}
}

@ -63,7 +63,7 @@ public final class UnsendRequest: ControlMessage {
public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? {
guard let timestamp = timestamp, let author = author else {
SNLog("Couldn't construct unsend request proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct unsend request proto from: \(self).")
return nil
}
let unsendRequestProto = SNProtoUnsendRequest.builder(timestamp: timestamp, author: author)
@ -75,7 +75,7 @@ public final class UnsendRequest: ControlMessage {
setDisappearingMessagesConfigurationIfNeeded(on: contentProto)
return try contentProto.build()
} catch {
SNLog("Couldn't construct unsend request proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct unsend request proto from: \(self).")
return nil
}
}

@ -36,7 +36,7 @@ public extension VisibleMessage {
public func toProto(_ db: Database) -> SNProtoDataMessagePreview? {
guard let url = url else {
SNLog("Couldn't construct link preview proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct link preview proto from: \(self).")
return nil
}
let linkPreviewProto = SNProtoDataMessagePreview.builder(url: url)
@ -53,7 +53,7 @@ public extension VisibleMessage {
do {
return try linkPreviewProto.build()
} catch {
SNLog("Couldn't construct link preview proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct link preview proto from: \(self).")
return nil
}
}

@ -27,14 +27,14 @@ public extension VisibleMessage {
public func toProto() -> SNProtoDataMessageOpenGroupInvitation? {
guard let url = url, let name = name else {
SNLog("Couldn't construct open group invitation proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct open group invitation proto from: \(self).")
return nil
}
let openGroupInvitationProto = SNProtoDataMessageOpenGroupInvitation.builder(url: url, name: name)
do {
return try openGroupInvitationProto.build()
} catch {
SNLog("Couldn't construct open group invitation proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct open group invitation proto from: \(self).")
return nil
}
}

@ -69,7 +69,7 @@ public extension VisibleMessage {
let dataMessageProtoBuilder = try? toProtoBuilder(),
let result = try? dataMessageProtoBuilder.build()
else {
SNLog("Couldn't construct profile proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct profile proto from: \(self).")
return nil
}
@ -93,7 +93,7 @@ public extension VisibleMessage {
public func toProto(isApproved: Bool) -> SNProtoMessageRequestResponse? {
guard let displayName = displayName else {
SNLog("Couldn't construct profile proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct profile proto from: \(self).")
return nil
}
let messageRequestResponseProto = SNProtoMessageRequestResponse.builder(
@ -110,7 +110,7 @@ public extension VisibleMessage {
messageRequestResponseProto.setProfile(try profileProto.build())
return try messageRequestResponseProto.build()
} catch {
SNLog("Couldn't construct profile proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct profile proto from: \(self).")
return nil
}
}

@ -40,7 +40,7 @@ public extension VisibleMessage {
public func toProto(_ db: Database) -> SNProtoDataMessageQuote? {
guard let timestamp = timestamp, let publicKey = publicKey else {
SNLog("Couldn't construct quote proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct quote proto from: \(self).")
return nil
}
let quoteProto = SNProtoDataMessageQuote.builder(id: timestamp, author: publicKey)
@ -49,7 +49,7 @@ public extension VisibleMessage {
do {
return try quoteProto.build()
} catch {
SNLog("Couldn't construct quote proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct quote proto from: \(self).")
return nil
}
}
@ -70,13 +70,13 @@ public extension VisibleMessage {
quotedAttachmentProto.setContentType(attachment.contentType)
if let fileName = attachment.sourceFilename { quotedAttachmentProto.setFileName(fileName) }
guard let attachmentProto = attachment.buildProto() else {
return SNLog("Ignoring invalid attachment for quoted message.")
return Log.warn(.messageSender, "Ignoring invalid attachment for quoted message.")
}
quotedAttachmentProto.setThumbnail(attachmentProto)
do {
try quoteProto.addAttachments(quotedAttachmentProto.build())
} catch {
SNLog("Couldn't construct quoted attachment proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct quoted attachment proto from: \(self).")
}
}

@ -86,7 +86,7 @@ public extension VisibleMessage {
do {
return try reactionProto.build()
} catch {
SNLog("Couldn't construct quote proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct quote proto from: \(self).")
return nil
}
}

@ -202,7 +202,7 @@ public final class VisibleMessage: Message {
proto.setDataMessage(try dataMessage.build())
return try proto.build()
} catch {
SNLog("Couldn't construct visible message proto from: \(self).")
Log.warn(.messageSender, "Couldn't construct visible message proto from: \(self).")
return nil
}
}

@ -321,7 +321,7 @@ public enum OpenGroupAPI {
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
.map { (info: ResponseInfoType, response: Network.BatchResponseMap<Endpoint>) -> CapabilitiesAndRoomResponse in
.tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap<Endpoint>) -> CapabilitiesAndRoomResponse in
let maybeCapabilities: Network.BatchSubResponse<Capabilities>? = (response[.capabilities] as? Network.BatchSubResponse<Capabilities>)
let maybeRoomResponse: Any? = response.data
.first(where: { key, _ in
@ -372,7 +372,7 @@ public enum OpenGroupAPI {
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
.map { (info: ResponseInfoType, response: Network.BatchResponseMap<Endpoint>) -> CapabilitiesAndRoomsResponse in
.tryMap { (info: ResponseInfoType, response: Network.BatchResponseMap<Endpoint>) -> CapabilitiesAndRoomsResponse in
let maybeCapabilities: Network.BatchSubResponse<Capabilities>? = (response[.capabilities] as? Network.BatchSubResponse<Capabilities>)
let maybeRooms: Network.BatchSubResponse<[Room]>? = response.data
.first(where: { key, _ in

@ -935,7 +935,7 @@ public extension OpenGroupManager {
class Cache: OGMCacheType {
private let dependencies: Dependencies
private let defaultRoomsSubject: CurrentValueSubject<[DefaultRoomInfo], Error> = CurrentValueSubject([])
private var _timeSinceLastOpen: TimeInterval?
private var _lastSuccessfulCommunityPollTimestamp: TimeInterval?
public var pendingChanges: [OpenGroupAPI.PendingChange] = []
public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error> {
@ -961,18 +961,22 @@ public extension OpenGroupManager {
// MARK: - Functions
public func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen {
return storedTimeSinceLastOpen
public func getLastSuccessfulCommunityPollTimestamp() -> TimeInterval {
if let storedTime: TimeInterval = _lastSuccessfulCommunityPollTimestamp {
return storedTime
}
guard let lastOpen: Date = dependencies[defaults: .standard, key: .lastOpen] else {
_timeSinceLastOpen = .greatestFiniteMagnitude
return .greatestFiniteMagnitude
guard let lastPoll: Date = dependencies[defaults: .standard, key: .lastOpen] else {
return 0
}
_timeSinceLastOpen = dependencies.dateNow.timeIntervalSince(lastOpen)
return dependencies.dateNow.timeIntervalSince(lastOpen)
_lastSuccessfulCommunityPollTimestamp = lastPoll.timeIntervalSince1970
return lastPoll.timeIntervalSince1970
}
public func setLastSuccessfulCommunityPollTimestamp(_ timestamp: TimeInterval) {
dependencies[defaults: .standard, key: .lastOpen] = Date(timeIntervalSince1970: timestamp)
_lastSuccessfulCommunityPollTimestamp = timestamp
}
public func setDefaultRoomInfo(_ info: [DefaultRoomInfo]) {
@ -995,6 +999,7 @@ public protocol OGMCacheType: OGMImmutableCacheType, MutableCacheType {
var pendingChanges: [OpenGroupAPI.PendingChange] { get set }
func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval
func getLastSuccessfulCommunityPollTimestamp() -> TimeInterval
func setLastSuccessfulCommunityPollTimestamp(_ timestamp: TimeInterval)
func setDefaultRoomInfo(_ info: [OpenGroupManager.DefaultRoomInfo])
}

@ -94,7 +94,7 @@ extension MessageReceiver {
guard db[.areCallsEnabled] && Permissions.microphone == .granted else {
let state: CallMessage.MessageInfo.State = (db[.areCallsEnabled] ? .permissionDeniedMicrophone : .permissionDenied)
SNLog("[MessageReceiver+Calls] Microphone permission is \(AVAudioSession.sharedInstance().recordPermission)")
Log.info(.calls, "Microphone permission is \(AVAudioSession.sharedInstance().recordPermission)")
if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: state, using: dependencies) {
let thread: SessionThread = try SessionThread.upsert(

@ -433,7 +433,7 @@ extension MessageReceiver {
.defaulting(to: [])
.reduce(into: [:]) { result, next in result[next.id] = next }
let names: [String] = message.memberSessionIds
.sorted { lhs, rhs in lhs == userSessionId.hexString }
.sortedById(userSessionId: userSessionId)
.map { id in
profiles[id]?.displayName(for: .group) ??
Profile.truncated(id: id, truncating: .middle)

@ -109,7 +109,7 @@ extension MessageReceiver {
!ClosedGroupKeyPair
.filter(ClosedGroupKeyPair.Columns.threadKeyPairHash == newKeyPair.threadKeyPairHash)
.isNotEmpty(db)
else { return SNLog("Ignoring outdated NEW legacy group message due to more recent config state") }
else { return Log.info(.messageReceiver, "Ignoring outdated NEW legacy group message due to more recent config state") }
try newKeyPair.insert(db)
return
@ -298,14 +298,14 @@ extension MessageReceiver {
let legacyGroupId: String = (explicitGroupPublicKey?.toHexString() ?? threadId)
guard let userKeyPair: KeyPair = Identity.fetchUserKeyPair(db) else {
return SNLog("Couldn't find user X25519 key pair.")
return Log.error(.messageReceiver, "Couldn't find user X25519 key pair.")
}
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: legacyGroupId) else {
return SNLog("Ignoring closed group encryption key pair for nonexistent group.")
return Log.warn(.messageReceiver, "Ignoring closed group encryption key pair for nonexistent group.")
}
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return }
guard let sender: String = message.sender, groupAdmins.contains(where: { $0.profileId == sender }) else {
return SNLog("Ignoring closed group encryption key pair from non-admin.")
return Log.info(.messageReceiver, "Ignoring closed group encryption key pair from non-admin.")
}
// Find our wrapper and decrypt it if possible
let userPublicKey: String = SessionId(.standard, publicKey: userKeyPair.publicKey).hexString
@ -325,18 +325,14 @@ extension MessageReceiver {
)
).plaintext
}
catch {
return SNLog("Couldn't decrypt closed group encryption key pair.")
}
catch { return Log.error(.messageReceiver, "Couldn't decrypt closed group encryption key pair.") }
// Parse it
let proto: SNProtoKeyPair
do {
proto = try SNProtoKeyPair.parseData(plaintext)
}
catch {
return SNLog("Couldn't parse closed group encryption key pair.")
}
catch { return Log.error(.messageReceiver, "Couldn't parse closed group encryption key pair.") }
do {
let keyPair: ClosedGroupKeyPair = ClosedGroupKeyPair(
@ -357,13 +353,13 @@ extension MessageReceiver {
}
catch {
if case DatabaseError.SQLITE_CONSTRAINT_UNIQUE = error {
return SNLog("Ignoring duplicate closed group encryption key pair.")
return Log.info(.messageReceiver, "Ignoring duplicate closed group encryption key pair.")
}
throw error
}
SNLog("Received a new closed group encryption key pair.")
Log.info(.messageReceiver, "Received a new closed group encryption key pair.")
}
private static func handleClosedGroupNameChanged(
@ -542,7 +538,7 @@ extension MessageReceiver {
allMembers
.filter({ $0.role == .admin })
.contains(where: { $0.profileId == sender })
else { return SNLog("Ignoring invalid closed group update.") }
else { return Log.warn(.messageReceiver, "Ignoring invalid closed group update.") }
// Update libSession
try? LibSession.update(
@ -682,7 +678,7 @@ extension MessageReceiver {
) throws {
guard let sender: String = message.sender else { return }
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: threadId) else {
return SNLog("Ignoring group update for nonexistent group.")
return Log.warn(.messageReceiver, "Ignoring group update for nonexistent group.")
}
let timestampMs: Int64 = (
@ -699,14 +695,14 @@ extension MessageReceiver {
case .legacyGroup:
// Check that the message isn't from before the group was created
guard Double(message.sentTimestampMs ?? 0) > closedGroup.formationTimestamp else {
return SNLog("Ignoring legacy group update from before thread was created.")
return Log.warn(.messageReceiver, "Ignoring legacy group update from before thread was created.")
}
// If these values are missing then we probably won't be able to validly handle the message
guard
let allMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db),
allMembers.contains(where: { $0.profileId == sender })
else { return SNLog("Ignoring legacy group update from non-member.") }
else { return Log.warn(.messageReceiver, "Ignoring legacy group update from non-member.") }
try legacyGroupChanges(sender, closedGroup, allMembers)

@ -47,7 +47,7 @@ extension MessageReceiver {
dependencies[singleton: .typingIndicators].didStopTyping(db, threadId: threadId, direction: .incoming)
default:
SNLog("Unknown TypingIndicator Kind ignored")
Log.warn(.messageReceiver, "Unknown TypingIndicator Kind ignored")
return
}
}

@ -115,7 +115,7 @@ extension MessageReceiver {
)
case .group:
SNLog("Ignoring message with invalid sender.")
Log.info(.messageReceiver, "Ignoring message with invalid sender.")
throw MessageReceiverError.invalidSender
}
}()

@ -23,6 +23,11 @@ extension MessageSender {
members: [(String, Profile?)],
using dependencies: Dependencies
) -> AnyPublisher<SessionThread, Error> {
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let sortedOtherMembers: [(String, Profile?)] = members
.filter { id, _ in id != userSessionId.hexString }
.sortedById(userSessionId: userSessionId)
return Just(())
.setFailureType(to: Error.self)
.flatMap { _ -> AnyPublisher<DisplayPictureManager.UploadResult?, Error> in
@ -40,8 +45,6 @@ extension MessageSender {
}
.flatMap { (displayPictureInfo: DisplayPictureManager.UploadResult?) -> AnyPublisher<PreparedGroupData, Error> in
dependencies[singleton: .storage].writePublisher { db -> PreparedGroupData in
let userSessionId: SessionId = dependencies[cache: .general].sessionId
/// Create and cache the libSession entries
let createdInfo: LibSession.CreatedGroupInfo = try LibSession.createGroup(
db,
@ -77,12 +80,10 @@ extension MessageSender {
body: ClosedGroup.MessageInfo
.addedUsers(
hasCurrentUser: false,
names: members
.filter { id, _ in id != userSessionId.hexString }
.map { id, profile in
profile?.displayName(for: .group) ??
Profile.truncated(id: id, truncating: .middle)
},
names: sortedOtherMembers.map { id, profile in
profile?.displayName(for: .group) ??
Profile.truncated(id: id, truncating: .middle)
},
historyShared: false
)
.infoString(using: dependencies),
@ -101,9 +102,7 @@ extension MessageSender {
destination: .closedGroup(groupPublicKey: createdInfo.group.id),
message: GroupUpdateMemberChangeMessage(
changeType: .added,
memberSessionIds: members
.filter { id, _ -> Bool in id != userSessionId.hexString }
.map { id, _ in id },
memberSessionIds: sortedOtherMembers.map { id, _ in id },
historyShared: false,
sentTimestampMs: UInt64(createdInfo.group.formationTimestamp * 1000),
authMethod: Authentication.groupAdmin(
@ -553,7 +552,7 @@ extension MessageSender {
public static func addGroupMembers(
groupSessionId: String,
members: [(id: String, profile: Profile?)],
members: [(String, Profile?)],
allowAccessToHistoricMessages: Bool,
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
@ -568,6 +567,10 @@ extension MessageSender {
subaccountToken: [UInt8]
)
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let sortedMembers: [(String, Profile?)] = members
.sortedById(userSessionId: userSessionId)
return dependencies[singleton: .storage]
.writePublisher { db -> ([MemberJobData], Network.PreparedRequest<Void>, Network.PreparedRequest<Void>?) in
guard
@ -578,7 +581,6 @@ extension MessageSender {
.fetchOne(db)
else { throw MessageSenderError.invalidClosedGroupUpdate }
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let changeTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs()
var maybeSupplementalKeyRequest: Network.PreparedRequest<Void>?
@ -601,7 +603,7 @@ extension MessageSender {
let supplementData: Data = try LibSession.keySupplement(
db,
groupSessionId: sessionId,
memberIds: members.map { $0.id }.asSet(),
memberIds: members.map { id, _ in id }.asSet(),
using: dependencies
)
@ -708,13 +710,11 @@ extension MessageSender {
variant: .infoGroupMembersUpdated,
body: ClosedGroup.MessageInfo
.addedUsers(
hasCurrentUser: members.map { $0.id }.contains(userSessionId.hexString),
names: members
.sorted { lhs, rhs in lhs.id == userSessionId.hexString }
.map { id, profile in
profile?.displayName(for: .group) ??
Profile.truncated(id: id, truncating: .middle)
},
hasCurrentUser: members.contains { id, _ in id == userSessionId.hexString },
names: sortedMembers.map { id, profile in
profile?.displayName(for: .group) ??
Profile.truncated(id: id, truncating: .middle)
},
historyShared: allowAccessToHistoricMessages
)
.infoString(using: dependencies),
@ -737,7 +737,7 @@ extension MessageSender {
destination: .closedGroup(groupPublicKey: sessionId.hexString),
message: GroupUpdateMemberChangeMessage(
changeType: .added,
memberSessionIds: members.map { $0.id },
memberSessionIds: sortedMembers.map { id, _ in id },
historyShared: allowAccessToHistoricMessages,
sentTimestampMs: UInt64(changeTimestampMs),
authMethod: Authentication.groupAdmin(
@ -979,6 +979,9 @@ extension MessageSender {
dependencies[cache: .snodeAPI].currentOffsetTimestampMs()
)
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let sortedMemberIds: [String] = memberIds.sortedById(userSessionId: userSessionId)
return dependencies[singleton: .storage]
.writePublisher { db in
guard
@ -1028,7 +1031,6 @@ extension MessageSender {
/// Send the member changed message if desired
if sendMemberChangedMessage {
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let removedMemberProfiles: [String: Profile] = (try? Profile
.filter(ids: memberIds)
.fetchAll(db))
@ -1045,12 +1047,10 @@ extension MessageSender {
body: ClosedGroup.MessageInfo
.removedUsers(
hasCurrentUser: memberIds.contains(userSessionId.hexString),
names: memberIds
.sorted { lhs, rhs in lhs == userSessionId.hexString }
.map { id in
removedMemberProfiles[id]?.displayName(for: .group) ??
Profile.truncated(id: id, truncating: .middle)
}
names: sortedMemberIds.map { id in
removedMemberProfiles[id]?.displayName(for: .group) ??
Profile.truncated(id: id, truncating: .middle)
}
)
.infoString(using: dependencies),
timestampMs: targetChangeTimestampMs,
@ -1072,7 +1072,7 @@ extension MessageSender {
destination: .closedGroup(groupPublicKey: sessionId.hexString),
message: GroupUpdateMemberChangeMessage(
changeType: .removed,
memberSessionIds: Array(memberIds),
memberSessionIds: sortedMemberIds,
historyShared: false,
sentTimestampMs: UInt64(targetChangeTimestampMs),
authMethod: Authentication.groupAdmin(
@ -1098,10 +1098,12 @@ extension MessageSender {
public static func promoteGroupMembers(
groupSessionId: SessionId,
members: [(id: String, profile: Profile?)],
members: [(String, Profile?)],
isResend: Bool,
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
let userSessionId: SessionId = dependencies[cache: .general].sessionId
return dependencies[singleton: .storage]
.writePublisher { db -> Set<String> in
guard
@ -1113,7 +1115,7 @@ extension MessageSender {
else { throw MessageSenderError.invalidClosedGroupUpdate }
/// Determine which members actually need to be promoted (rather than just resent promotions)
let memberIds: Set<String> = Set(members.map(\.id))
let memberIds: Set<String> = Set(members.map { id, _ in id })
let memberIdsRequiringPromotions: Set<String> = try GroupMember
.select(.profileId)
.filter(GroupMember.Columns.groupId == groupSessionId.hexString)
@ -1121,8 +1123,10 @@ extension MessageSender {
.filter(GroupMember.Columns.role == GroupMember.Role.standard)
.asRequest(of: String.self)
.fetchSet(db)
let membersReceivingPromotions: [(id: String, profile: Profile?)] = members
let membersReceivingPromotions: [(String, Profile?)] = members
.filter { id, _ in memberIdsRequiringPromotions.contains(id) }
let sortedMembersReceivingPromotions: [(String, Profile?)] = membersReceivingPromotions
.sortedById(userSessionId: userSessionId)
/// Perform the config changes without triggering a config sync (we will do so manually after the process completes)
try dependencies.mutate(cache: .libSession) { cache in
@ -1173,7 +1177,6 @@ extension MessageSender {
/// that are getting promotions re-sent to them - we only want to send an admin changed message if there
/// is a newly promoted member
if !isResend && !membersReceivingPromotions.isEmpty {
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let changeTimestampMs: Int64 = dependencies[cache: .snodeAPI].currentOffsetTimestampMs()
let disappearingConfig: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration.fetchOne(db, id: groupSessionId.hexString)
@ -1185,14 +1188,12 @@ extension MessageSender {
body: ClosedGroup.MessageInfo
.promotedUsers(
hasCurrentUser: membersReceivingPromotions
.map { $0.id }
.map { id, _ in id }
.contains(userSessionId.hexString),
names: membersReceivingPromotions
.sorted { lhs, rhs in lhs.id == userSessionId.hexString }
.map { id, profile in
profile?.displayName(for: .group) ??
Profile.truncated(id: id, truncating: .middle)
}
names: sortedMembersReceivingPromotions.map { id, profile in
profile?.displayName(for: .group) ??
Profile.truncated(id: id, truncating: .middle)
}
)
.infoString(using: dependencies),
timestampMs: changeTimestampMs,
@ -1214,7 +1215,7 @@ extension MessageSender {
destination: .closedGroup(groupPublicKey: groupSessionId.hexString),
message: GroupUpdateMemberChangeMessage(
changeType: .promoted,
memberSessionIds: membersReceivingPromotions.map { $0.id },
memberSessionIds: sortedMembersReceivingPromotions.map { id, _ in id },
historyShared: false,
sentTimestampMs: UInt64(changeTimestampMs),
authMethod: Authentication.groupAdmin(

@ -319,7 +319,7 @@ extension MessageSender {
// Get the group, check preconditions & prepare
guard (try? SessionThread.exists(db, id: legacyGroupSessionId)) == true else {
SNLog("Can't update nonexistent closed group.")
Log.warn(.messageSender, "Can't update nonexistent closed group.")
throw MessageSenderError.noThread
}
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: legacyGroupSessionId) else {
@ -555,12 +555,12 @@ extension MessageSender {
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
guard !removedMembers.contains(userSessionId.hexString) else {
SNLog("Invalid closed group update.")
Log.warn(.messageSender, "Invalid closed group update.")
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
.eraseToAnyPublisher()
}
guard allGroupMembers.contains(where: { $0.role == .admin && $0.profileId == userSessionId.hexString }) else {
SNLog("Only an admin can remove members from a group.")
Log.warn(.messageSender, "Only an admin can remove members from a group.")
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
.eraseToAnyPublisher()
}
@ -645,7 +645,7 @@ extension MessageSender {
using dependencies: Dependencies
) {
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else {
return SNLog("Couldn't send key pair for nonexistent closed group.")
return Log.warn(.messageSender, "Couldn't send key pair for nonexistent closed group.")
}
guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) else {
return
@ -654,7 +654,7 @@ extension MessageSender {
return
}
guard allGroupMembers.contains(where: { $0.role == .standard && $0.profileId == publicKey }) else {
return SNLog("Refusing to send latest encryption key pair to non-member.")
return Log.error(.messageSender, "Refusing to send latest encryption key pair to non-member.")
}
// Get the latest encryption key pair
@ -692,7 +692,7 @@ extension MessageSender {
)
)
SNLog("Sending latest encryption key pair to: \(publicKey).")
Log.info(.messageSender, "Sending latest encryption key pair to: \(publicKey).")
try MessageSender.send(
db,
message: ClosedGroupControlMessage(

@ -492,7 +492,7 @@ public enum PushNotificationAPI {
let notification: BencodeResponse<NotificationMetadata> = try? BencodeDecoder(using: dependencies)
.decode(BencodeResponse<NotificationMetadata>.self, from: decryptedData)
else {
SNLog("Failed to decrypt or decode notification")
Log.error(.cat, "Failed to decrypt or decode notification")
return (nil, .invalid, .failure)
}
@ -506,7 +506,7 @@ public enum PushNotificationAPI {
notification.info.dataLength == notificationData.count,
!notificationData.isEmpty
else {
SNLog("Get notification data failed")
Log.error(.cat, "Get notification data failed")
return (nil, notification.info, .failureNoContent)
}

@ -247,10 +247,10 @@ public final class CommunityPoller: CommunityPollerType & PollerType {
/// **Note:** The returned messages will have already been processed by the `Poller`, they are only returned
/// for cases where we need explicit/custom behaviours to occur (eg. Onboarding)
public func poll(forceSynchronousProcessing: Bool = false) -> AnyPublisher<PollResult, Error> {
let timeSinceLastPoll: TimeInterval = (self.lastPollStart > 0 ?
let lastSuccessfulPollTimestamp: TimeInterval = (self.lastPollStart > 0 ?
lastPollStart :
dependencies.mutate(cache: .openGroupManager) { cache in
cache.getTimeSinceLastOpen(using: dependencies)
cache.getLastSuccessfulCommunityPollTimestamp()
}
)
@ -260,7 +260,7 @@ public final class CommunityPoller: CommunityPollerType & PollerType {
db,
server: pollerDestination.target,
hasPerformedInitialPoll: (pollCount > 0),
timeSinceLastPoll: timeSinceLastPoll,
timeSinceLastPoll: (dependencies.dateNow.timeIntervalSince1970 - lastSuccessfulPollTimestamp),
using: dependencies
)
}
@ -274,8 +274,14 @@ public final class CommunityPoller: CommunityPollerType & PollerType {
)
}
.handleEvents(
receiveOutput: { [weak self] _ in
receiveOutput: { [weak self, dependencies] _ in
self?.pollCount += 1
dependencies.mutate(cache: .openGroupManager) { cache in
cache.setLastSuccessfulCommunityPollTimestamp(
dependencies.dateNow.timeIntervalSince1970
)
}
}
)
.eraseToAnyPublisher()

@ -72,7 +72,7 @@ public protocol PollerType: AnyObject {
using dependencies: Dependencies
)
func startIfNeeded()
func startIfNeeded(forceStartInBackground: Bool)
func stop()
func pollerDidStart()
@ -84,7 +84,14 @@ public protocol PollerType: AnyObject {
// MARK: - Default Implementations
public extension PollerType {
func startIfNeeded() {
func startIfNeeded() { startIfNeeded(forceStartInBackground: false) }
func startIfNeeded(forceStartInBackground: Bool) {
guard
forceStartInBackground ||
dependencies[singleton: .appContext].isMainAppAndActive
else { return Log.info(.poller, "Ignoring call to start \(pollerName) due to not being active.") }
pollerQueue.async(using: dependencies) { [weak self, pollerName] in
guard self?.isPolling != true else { return }

@ -3,6 +3,12 @@
import Foundation
import SessionUtilitiesKit
// MARK: - Log.Category
private extension Log.Category {
static let cat: Log.Category = .create("Data", defaultLevel: .warn)
}
// MARK: - Decoding
public extension Data {
@ -18,7 +24,7 @@ public extension Data {
break
}
else if bytes[targetIndex] != 0x00 {
SNLog("Failed to remove padding, returning unstripped padding");
Log.error(.cat, "Failed to remove padding, returning unstripped padding");
return self
}
}

@ -58,7 +58,7 @@ public enum MessageWrapper {
builder.setContent(content)
return try builder.build()
} catch let error {
SNLog("Failed to wrap message in envelope: \(error).")
Log.error(.messageSender, "Failed to wrap message in envelope: \(error).")
throw Error.failedToWrapMessageInEnvelope
}
}
@ -71,7 +71,7 @@ public enum MessageWrapper {
messageBuilder.setRequest(try requestBuilder.build())
return try messageBuilder.build()
} catch let error {
SNLog("Failed to wrap envelope in web socket message: \(error).")
Log.error(.messageSender, "Failed to wrap envelope in web socket message: \(error).")
throw Error.failedToWrapEnvelopeInWebSocketMessage
}
}
@ -90,7 +90,7 @@ public enum MessageWrapper {
}()
return try SNProtoEnvelope.parseData(envelopeData)
} catch let error {
SNLog("Failed to unwrap data: \(error).")
Log.error(.messageSender, "Failed to unwrap data: \(error).")
throw Error.failedToUnwrapData
}
}

@ -6,6 +6,14 @@ import GRDB
import DifferenceKit
import SessionUtilitiesKit
// MARK: - Log.Category
private extension Log.Category {
static let cat: Log.Category = .create("Preferences.Sound", defaultLevel: .warn)
}
// MARK: - Preferences
public extension Preferences {
enum Sound: Int, Codable, DatabaseValueConvertible, EnumIntSetting, Differentiable {
public static var defaultiOSIncomingRingtone: Sound = .opening
@ -153,7 +161,7 @@ public extension Preferences {
public func notificationSound(isQuiet: Bool) -> UNNotificationSound {
guard let filename: String = filename(quiet: isQuiet) else {
SNLog("[Preferences.Sound] filename was unexpectedly nil")
Log.warn(.cat, "Filename was unexpectedly nil")
return UNNotificationSound.default
}

@ -7,7 +7,7 @@ import SessionUtilitiesKit
public extension SNProtoEnvelope {
static func from(_ message: SnodeReceivedMessage) -> SNProtoEnvelope? {
guard let result = try? MessageWrapper.unwrap(data: message.data) else {
SNLog("Failed to unwrap data for message: \(String(reflecting: message)).")
Log.error(.messageReceiver, "Failed to unwrap data for message: \(String(reflecting: message)).")
return nil
}

@ -17,6 +17,7 @@ class OpenGroupAPISpec: QuickSpec {
@TestState var dependencies: TestDependencies! = TestDependencies { dependencies in
dependencies.dateNow = Date(timeIntervalSince1970: 1234567890)
dependencies.forceSynchronous = true
}
@TestState(singleton: .storage, in: dependencies) var mockStorage: Storage! = SynchronousStorage(
customWriter: try! DatabaseQueue(),

@ -183,6 +183,7 @@ class OpenGroupManagerSpec: QuickSpec {
@TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults(
initialSetup: { defaults in
defaults.when { $0.integer(forKey: .any) }.thenReturn(0)
defaults.when { $0.set(.any, forKey: .any) }.thenReturn(())
}
)
@TestState(defaults: .appGroup, in: dependencies) var mockAppGroupDefaults: MockUserDefaults! = MockUserDefaults(
@ -199,7 +200,7 @@ class OpenGroupManagerSpec: QuickSpec {
initialSetup: { cache in
cache.when { $0.pendingChanges }.thenReturn([])
cache.when { $0.pendingChanges = .any }.thenReturn(())
cache.when { $0.getTimeSinceLastOpen(using: .any) }.thenReturn(0)
cache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0)
cache.when { $0.setDefaultRoomInfo(.any) }.thenReturn(())
}
)
@ -237,20 +238,19 @@ class OpenGroupManagerSpec: QuickSpec {
// MARK: -- cache data
context("cache data") {
// MARK: ---- defaults the time since last open to greatestFiniteMagnitude
it("defaults the time since last open to greatestFiniteMagnitude") {
// MARK: ---- defaults the time since last open to zero
it("defaults the time since last open to zero") {
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue)
}
.thenReturn(nil)
expect(cache.getTimeSinceLastOpen(using: dependencies))
.to(beCloseTo(.greatestFiniteMagnitude))
expect(cache.getLastSuccessfulCommunityPollTimestamp()).to(equal(0))
}
// MARK: ---- returns the time since the last open
it("returns the time since the last open") {
// MARK: ---- returns the time since the last poll
it("returns the time since the last poll") {
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue)
@ -258,12 +258,12 @@ class OpenGroupManagerSpec: QuickSpec {
.thenReturn(Date(timeIntervalSince1970: 1234567880))
dependencies.dateNow = Date(timeIntervalSince1970: 1234567890)
expect(cache.getTimeSinceLastOpen(using: dependencies))
.to(beCloseTo(10))
expect(cache.getLastSuccessfulCommunityPollTimestamp())
.to(equal(1234567880))
}
// MARK: ---- caches the time since the last open
it("caches the time since the last open") {
// MARK: ---- caches the time since the last poll in memory
it("caches the time since the last poll in memory") {
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
defaults.object(forKey: UserDefaults.DateKey.lastOpen.rawValue)
@ -271,8 +271,8 @@ class OpenGroupManagerSpec: QuickSpec {
.thenReturn(Date(timeIntervalSince1970: 1234567770))
dependencies.dateNow = Date(timeIntervalSince1970: 1234567780)
expect(cache.getTimeSinceLastOpen(using: dependencies))
.to(beCloseTo(10))
expect(cache.getLastSuccessfulCommunityPollTimestamp())
.to(equal(1234567770))
mockUserDefaults
.when { (defaults: inout any UserDefaultsType) -> Any? in
@ -281,8 +281,21 @@ class OpenGroupManagerSpec: QuickSpec {
.thenReturn(Date(timeIntervalSince1970: 1234567890))
// Cached value shouldn't have been updated
expect(cache.getTimeSinceLastOpen(using: dependencies))
.to(beCloseTo(10))
expect(cache.getLastSuccessfulCommunityPollTimestamp())
.to(equal(1234567770))
}
// MARK: ---- updates the time since the last poll in user defaults
it("updates the time since the last poll in user defaults") {
cache.setLastSuccessfulCommunityPollTimestamp(12345)
expect(mockUserDefaults)
.to(call(matchingParameters: .all) {
$0.set(
Date(timeIntervalSince1970: 12345),
forKey: UserDefaults.DateKey.lastOpen.rawValue
)
})
}
}

@ -1433,6 +1433,53 @@ class MessageSenderGroupsSpec: QuickSpec {
)
})
}
// MARK: ---- sorts the members in the control message deterministically
it("sorts the members in the control message deterministically") {
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051234111111111111111111111111111111111111111111111111111111111112", nil),
("051111111111111111111111111111111111111111111111111111111111111112", nil),
("05\(TestConstants.publicKey)", nil)
],
allowAccessToHistoricMessages: false,
using: dependencies
).sinkUntilComplete()
expect(mockJobRunner)
.to(call(.exactly(times: 1), matchingParameters: .all) { jobRunner in
jobRunner.add(
.any,
job: Job(
variant: .messageSend,
behaviour: .runOnceAfterConfigSyncIgnoringPermanentFailure,
threadId: groupId.hexString,
details: MessageSendJob.Details(
destination: .closedGroup(groupPublicKey: groupId.hexString),
message: try GroupUpdateMemberChangeMessage(
changeType: .added,
memberSessionIds: [
"05\(TestConstants.publicKey)",
"051111111111111111111111111111111111111111111111111111111111111112",
"051234111111111111111111111111111111111111111111111111111111111112"
],
historyShared: false,
sentTimestampMs: UInt64(1234567890000),
authMethod: Authentication.groupAdmin(
groupSessionId: SessionId(.group, hex: groupId.hexString),
ed25519SecretKey: [1, 2, 3]
),
using: dependencies
),
requiredConfigSyncVariant: .groupMembers
)
),
dependantJob: nil,
canStartJob: false
)
})
}
}
}
}

@ -69,6 +69,11 @@ class CommunityPollerSpec: QuickSpec {
)
}
)
@TestState(singleton: .appContext, in: dependencies) var mockAppContext: MockAppContext! = MockAppContext(
initialSetup: { context in
context.when { $0.isMainAppAndActive }.thenReturn(false)
}
)
@TestState(defaults: .standard, in: dependencies) var mockUserDefaults: MockUserDefaults! = MockUserDefaults()
@TestState(cache: .general, in: dependencies) var mockGeneralCache: MockGeneralCache! = MockGeneralCache(
initialSetup: { cache in
@ -78,7 +83,7 @@ class CommunityPollerSpec: QuickSpec {
@TestState(cache: .openGroupManager, in: dependencies) var mockOGMCache: MockOGMCache! = MockOGMCache(
initialSetup: { cache in
cache.when { $0.pendingChanges }.thenReturn([])
cache.when { $0.getTimeSinceLastOpen(using: .any) }.thenReturn(0)
cache.when { $0.getLastSuccessfulCommunityPollTimestamp() }.thenReturn(0)
}
)
@TestState var cache: CommunityPoller.Cache! = CommunityPoller.Cache(using: dependencies)
@ -87,6 +92,10 @@ class CommunityPollerSpec: QuickSpec {
describe("a CommunityPollerCache") {
// MARK: -- when starting polling
context("when starting polling") {
beforeEach {
mockAppContext.when { $0.isMainAppAndActive }.thenReturn(true)
}
// MARK: ---- creates pollers for all of the communities
it("creates pollers for all of the communities") {
cache.startAllPollers()

@ -13,14 +13,17 @@ class MockLibSessionCache: Mock<LibSessionCacheType>, LibSessionCacheType {
// MARK: - State Management
func loadState(_ db: Database) { mockNoReturn(untrackedArgs: [db]) }
func loadDefaultStatesFor(
userConfigVariants: Set<ConfigDump.Variant>,
groups: [ClosedGroup],
userSessionId: SessionId,
userEd25519KeyPair: KeyPair
func loadState(_ db: Database, requestId: String?) {
mockNoReturn(args: [requestId], untrackedArgs: [db])
}
func loadDefaultStateFor(
variant: ConfigDump.Variant,
sessionId: SessionId,
userEd25519KeyPair: KeyPair,
groupEd25519SecretKey: [UInt8]?
) {
mockNoReturn(args: [userConfigVariants, groups, userSessionId, userEd25519KeyPair])
mockNoReturn(args: [variant, sessionId, userEd25519KeyPair, groupEd25519SecretKey])
}
func hasConfig(for variant: ConfigDump.Variant, sessionId: SessionId) -> Bool {

@ -16,8 +16,12 @@ class MockOGMCache: Mock<OGMCacheType>, OGMCacheType {
set { mockNoReturn(args: [newValue]) }
}
func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
return mock(args: [dependencies])
func getLastSuccessfulCommunityPollTimestamp() -> TimeInterval {
return mock()
}
func setLastSuccessfulCommunityPollTimestamp(_ timestamp: TimeInterval) {
mockNoReturn(args: [timestamp])
}
func setDefaultRoomInfo(_ info: [OpenGroupManager.DefaultRoomInfo]) {

@ -12,11 +12,18 @@ import SessionSnodeKit
import SignalUtilitiesKit
import SessionUtilitiesKit
// MARK: - Log.Category
private extension Log.Category {
static let cat: Log.Category = .create("NotificationServiceExtension", defaultLevel: .info)
}
// MARK: - NotificationServiceExtension
public final class NotificationServiceExtension: UNNotificationServiceExtension {
// Called via the OS so create a default 'Dependencies' instance
private var dependencies: Dependencies = Dependencies.createEmpty()
private var startTime: CFTimeInterval = 0
private var fallbackRunId: String = "N/A" // stringlint:ignore
private var contentHandler: ((UNNotificationContent) -> Void)?
private var request: UNNotificationRequest?
@ThreadSafe private var hasCompleted: Bool = false
@ -32,9 +39,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// MARK: Did receive a remote push notification request
override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
let runId: String = UUID().uuidString
self.startTime = CACurrentMediaTime()
self.fallbackRunId = runId
self.contentHandler = contentHandler
self.request = request
@ -47,14 +52,14 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// Abort if the main app is running
guard !dependencies[defaults: .appGroup, key: .isMainAppActive] else {
return self.completeSilenty(.ignoreDueToMainAppRunning, runId: runId)
return self.completeSilenty(.ignoreDueToMainAppRunning, requestId: request.identifier)
}
guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
return self.completeSilenty(.ignoreDueToNoContentFromApple, runId: runId)
return self.completeSilenty(.ignoreDueToNoContentFromApple, requestId: request.identifier)
}
Log.info("didReceive called with runId: \(runId).")
Log.info(.cat, "didReceive called with requestId: \(request.identifier).")
/// Create the context if we don't have it (needed before _any_ interaction with the database)
if !dependencies[singleton: .appContext].isValid {
@ -65,14 +70,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
}
/// Actually perform the setup
DispatchQueue.main.sync {
self.performSetup(runId: runId) { [weak self] in
self?.handleNotification(notificationContent, runId: runId)
}
self.performSetup(requestId: request.identifier) { [weak self] in
self?.handleNotification(notificationContent, requestId: request.identifier)
}
}
private func handleNotification(_ notificationContent: UNMutableNotificationContent, runId: String) {
private func handleNotification(_ notificationContent: UNMutableNotificationContent, requestId: String) {
let (maybeData, metadata, result) = PushNotificationAPI.processNotification(
notificationContent: notificationContent,
using: dependencies
@ -92,21 +95,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
threadVariant: nil,
threadDisplayName: nil,
resolution: .errorProcessing(result),
runId: runId
requestId: requestId
)
case (.success, _), (.legacySuccess, _), (.failure, _):
return self.completeSilenty(.errorProcessing(result), runId: runId)
return self.completeSilenty(.errorProcessing(result), requestId: requestId)
// Just log if the notification was too long (a ~2k message should be able to fit so
// these will most commonly be call or config messages)
case (.successTooLong, _):
return self.completeSilenty(.ignoreDueToContentSize(metadata), runId: runId)
return self.completeSilenty(.ignoreDueToContentSize(metadata), requestId: requestId)
case (.failureNoContent, _): return self.completeSilenty(.errorNoContent(metadata), runId: runId)
case (.legacyFailure, _): return self.completeSilenty(.errorNoContentLegacy, runId: runId)
case (.failureNoContent, _): return self.completeSilenty(.errorNoContent(metadata), requestId: requestId)
case (.legacyFailure, _): return self.completeSilenty(.errorNoContentLegacy, requestId: requestId)
case (.legacyForceSilent, _):
return self.completeSilenty(.ignoreDueToNonLegacyGroupLegacyNotification, runId: runId)
return self.completeSilenty(.ignoreDueToNonLegacyGroupLegacyNotification, requestId: requestId)
}
}
@ -242,7 +245,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
using: dependencies
)
return self?.handleSuccessForIncomingCall(db, for: callMessage, runId: runId)
return self?.handleSuccessForIncomingCall(db, for: callMessage, requestId: requestId)
}
// Perform any required post-handling logic
@ -294,8 +297,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
}
db.afterNextTransaction(
onCommit: { _ in self?.completeSilenty(.success(metadata), runId: runId) },
onRollback: { _ in self?.completeSilenty(.errorTransactionFailure, runId: runId) }
onCommit: { _ in self?.completeSilenty(.success(metadata), requestId: requestId) },
onRollback: { _ in self?.completeSilenty(.errorTransactionFailure, requestId: requestId) }
)
}
catch {
@ -307,23 +310,23 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
DispatchQueue.main.async {
switch (error, processedThreadVariant, metadata.namespace.isConfigNamespace) {
case (MessageReceiverError.noGroupKeyPair, _, _):
self?.completeSilenty(.errorLegacyGroupKeysMissing, runId: runId)
self?.completeSilenty(.errorLegacyGroupKeysMissing, requestId: requestId)
case (MessageReceiverError.outdatedMessage, _, _):
self?.completeSilenty(.ignoreDueToOutdatedMessage, runId: runId)
self?.completeSilenty(.ignoreDueToOutdatedMessage, requestId: requestId)
case (MessageReceiverError.ignorableMessage, _, _):
self?.completeSilenty(.ignoreDueToRequiresNoNotification, runId: runId)
self?.completeSilenty(.ignoreDueToRequiresNoNotification, requestId: requestId)
case (MessageReceiverError.duplicateMessage, _, _),
(MessageReceiverError.duplicateControlMessage, _, _),
(MessageReceiverError.duplicateMessageNewSnode, _, _):
self?.completeSilenty(.ignoreDueToDuplicateMessage, runId: runId)
self?.completeSilenty(.ignoreDueToDuplicateMessage, requestId: requestId)
/// If it was a `decryptionFailed` error, but it was for a config namespace then just fail silently (don't
/// want to show the fallback notification in this case)
case (MessageReceiverError.decryptionFailed, _, true):
self?.completeSilenty(.errorMessageHandling(.decryptionFailed), runId: runId)
self?.completeSilenty(.errorMessageHandling(.decryptionFailed), requestId: requestId)
/// If it was a `decryptionFailed` error for a group conversation and the group doesn't exist or
/// doesn't have auth info (ie. group destroyed or member kicked), then just fail silently (don't want
@ -336,7 +339,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
group.authData != nil
)
else {
self?.completeSilenty(.errorMessageHandling(.decryptionFailed), runId: runId)
self?.completeSilenty(.errorMessageHandling(.decryptionFailed), requestId: requestId)
return
}
@ -347,7 +350,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
threadVariant: processedThreadVariant,
threadDisplayName: threadDisplayName,
resolution: .errorMessageHandling(.decryptionFailed),
runId: runId
requestId: requestId
)
case (let msgError as MessageReceiverError, _, _):
@ -357,7 +360,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
threadVariant: processedThreadVariant,
threadDisplayName: threadDisplayName,
resolution: .errorMessageHandling(msgError),
runId: runId
requestId: requestId
)
default:
@ -367,7 +370,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
threadVariant: processedThreadVariant,
threadDisplayName: threadDisplayName,
resolution: .errorOther(error),
runId: runId
requestId: requestId
)
}
}
@ -384,12 +387,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// MARK: Setup
private func performSetup(runId: String, completion: @escaping () -> Void) {
Log.info("Performing setup for runId: \(runId).")
private func performSetup(requestId: String, completion: @escaping () -> Void) {
Log.info(.cat, "Performing setup for requestId: \(requestId).")
dependencies.warmCache(cache: .appVersion)
AppSetup.setupEnvironment(
requestId: requestId,
appSpecificBlock: { [dependencies] in
// stringlint:ignore_start
Log.setup(with: Logger(
@ -416,12 +420,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
},
migrationsCompletion: { [weak self, dependencies] result in
switch result {
case .failure(let error): self?.completeSilenty(.errorDatabaseMigrations(error), runId: runId)
case .failure(let error): self?.completeSilenty(.errorDatabaseMigrations(error), requestId: requestId)
case .success:
DispatchQueue.main.async {
// Ensure storage is actually valid
guard dependencies[singleton: .storage].isValid else {
self?.completeSilenty(.errorDatabaseInvalid, runId: runId)
self?.completeSilenty(.errorDatabaseInvalid, requestId: requestId)
return
}
@ -431,7 +435,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// so it is possible that could change in the future. If it does, do nothing
// and don't disturb the user. Messages will be processed when they open the app.
guard dependencies[singleton: .storage, key: .isReadyForAppExtensions] else {
self?.completeSilenty(.errorNotReadyForExtensions, runId: runId)
self?.completeSilenty(.errorNotReadyForExtensions, requestId: requestId)
return
}
@ -454,10 +458,18 @@ 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.
completeSilenty(.errorTimeout, runId: fallbackRunId)
completeSilenty(.errorTimeout, requestId: (request?.identifier ?? "N/A")) // stringlint:ignore
}
private func completeSilenty(_ resolution: NotificationResolution, runId: String) {
private func completeSilenty(_ resolution: NotificationResolution, requestId: String) {
// This can be called from within database threads so to prevent blocking and weird
// behaviours make sure to send it to the main thread instead
guard Thread.isMainThread else {
return DispatchQueue.main.async { [weak self] in
self?.completeSilenty(resolution, requestId: requestId)
}
}
// Ensure we only run this once
guard _hasCompleted.performUpdateAndMap({ (true, $0) }) == false else { return }
@ -474,8 +486,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
}
let duration: CFTimeInterval = (CACurrentMediaTime() - startTime)
Log.custom(resolution.logLevel, [], "\(resolution) after \(.seconds(duration), unit: .ms), runId: \(runId).")
Log.custom(resolution.logLevel, [.cat], "\(resolution) after \(.seconds(duration), unit: .ms), requestId: \(requestId).")
Log.flush()
Log.reset()
self.contentHandler!(silentContent)
}
@ -483,7 +496,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
private func handleSuccessForIncomingCall(
_ db: Database,
for callMessage: CallMessage,
runId: String
requestId: String
) {
if #available(iOSApplicationExtension 14.5, *), Preferences.isCallKitSupported {
guard let caller: String = callMessage.sender, let timestamp = callMessage.sentTimestampMs else { return }
@ -499,14 +512,14 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in
if let error = error {
Log.error("Failed to notify main app of call message: \(error).")
Log.error(.cat, "Failed to notify main app of call message: \(error).")
dependencies[singleton: .storage].read { db in
self?.handleFailureForVoIP(db, for: callMessage, runId: runId)
self?.handleFailureForVoIP(db, for: callMessage, requestId: requestId)
}
}
else {
dependencies[defaults: .appGroup, key: .lastCallPreOffer] = Date()
self?.completeSilenty(.successCall, runId: runId)
self?.completeSilenty(.successCall, requestId: requestId)
}
}
}
@ -517,11 +530,11 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
)
}
else {
self.handleFailureForVoIP(db, for: callMessage, runId: runId)
self.handleFailureForVoIP(db, for: callMessage, requestId: requestId)
}
}
private func handleFailureForVoIP(_ db: Database, for callMessage: CallMessage, runId: String) {
private func handleFailureForVoIP(_ db: Database, for callMessage: CallMessage, requestId: String) {
let notificationContent = UNMutableNotificationContent()
notificationContent.userInfo = [ NotificationServiceExtension.isFromRemoteKey : true ]
notificationContent.title = Constants.app_name
@ -545,16 +558,16 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
Log.error("Failed to add notification request due to error: \(error).")
Log.error(.cat, "Failed to add notification request for requestId: \(requestId) due to error: \(error).")
}
semaphore.signal()
}
semaphore.wait()
Log.info("Add remote notification request.")
Log.info(.cat, "Add remote notification request for requestId: \(requestId).")
db.afterNextTransaction(
onCommit: { [weak self] _ in self?.completeSilenty(.errorCallFailure, runId: runId) },
onRollback: { [weak self] _ in self?.completeSilenty(.errorTransactionFailure, runId: runId) }
onCommit: { [weak self] _ in self?.completeSilenty(.errorCallFailure, requestId: requestId) },
onRollback: { [weak self] _ in self?.completeSilenty(.errorTransactionFailure, requestId: requestId) }
)
}
@ -564,24 +577,42 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
threadVariant: SessionThread.Variant?,
threadDisplayName: String?,
resolution: NotificationResolution,
runId: String
requestId: String
) {
// This can be called from within database threads so to prevent blocking and weird
// behaviours make sure to send it to the main thread instead
guard Thread.isMainThread else {
return DispatchQueue.main.async { [weak self] in
self?.handleFailure(
for: content,
metadata: metadata,
threadVariant: threadVariant,
threadDisplayName: threadDisplayName,
resolution: resolution,
requestId: requestId
)
}
}
let duration: CFTimeInterval = (CACurrentMediaTime() - startTime)
Log.error("\(resolution) after \(.seconds(duration), unit: .ms), showing generic failure message for message from namespace: \(metadata.namespace), runId: \(runId).")
Log.flush()
let previewType: Preferences.NotificationPreviewType = dependencies[singleton: .storage, key: .preferencesNotificationPreviewType]
.defaulting(to: .nameAndPreview)
Log.error(.cat, "\(resolution) after \(.seconds(duration), unit: .ms), showing generic failure message for message from namespace: \(metadata.namespace), requestId: \(requestId).")
/// Now we are done with the database, we should suspend it
if !dependencies[defaults: .appGroup, key: .isMainAppActive] {
dependencies[singleton: .storage].suspendDatabaseAccess()
}
/// Clear the logger
Log.flush()
Log.reset()
content.title = Constants.app_name
content.userInfo = [ NotificationServiceExtension.isFromRemoteKey: true ]
/// If it's a notification for a group conversation, the notification preferences are right and we have a name for the group
/// then we should include it in the notification content
let previewType: Preferences.NotificationPreviewType = dependencies[singleton: .storage, key: .preferencesNotificationPreviewType]
.defaulting(to: .nameAndPreview)
switch (threadVariant, previewType, threadDisplayName) {
case (.group, .nameAndPreview, .some(let name)), (.group, .nameNoPreview, .some(let name)),
(.legacyGroup, .nameAndPreview, .some(let name)), (.legacyGroup, .nameNoPreview, .some(let name)):

@ -107,7 +107,7 @@ final class SAEScreenLockViewController: ScreenLockViewController {
isShowingAuthUI = true
ScreenLock.shared.tryToUnlockScreenLock(
ScreenLock.tryToUnlockScreenLock(
success: { [weak self] in
Log.assertOnMainThread()
Log.info("unlock screen lock succeeded.")

@ -96,5 +96,9 @@ final class SimplifiedConversationCell: UITableViewCell {
using: dependencies
)
displayNameLabel.text = cellViewModel.displayName
self.isAccessibilityElement = true
self.accessibilityIdentifier = "Contact"
self.accessibilityLabel = cellViewModel.displayName
}
}

@ -28,10 +28,10 @@ extension DeleteAllBeforeResponse: ValidatableResponse {
result[next.key] = false
if let reason: String = next.value.reason, let statusCode: Int = next.value.code {
SNLog("Couldn't delete data from: \(next.key) due to error: \(reason) (\(statusCode)).")
Log.warn(.validator(self), "Couldn't delete data from: \(next.key) due to error: \(reason) (\(statusCode)).")
}
else {
SNLog("Couldn't delete data from: \(next.key).")
Log.warn(.validator(self), "Couldn't delete data from: \(next.key).")
}
return
}

@ -70,10 +70,10 @@ extension DeleteAllMessagesResponse: ValidatableResponse {
result[next.key] = false
if let reason: String = next.value.reason, let statusCode: Int = next.value.code {
SNLog("Couldn't delete data from: \(next.key) due to error: \(reason) (\(statusCode)).")
Log.warn(.validator(self), "Couldn't delete data from: \(next.key) due to error: \(reason) (\(statusCode)).")
}
else {
SNLog("Couldn't delete data from: \(next.key).")
Log.warn(.validator(self), "Couldn't delete data from: \(next.key).")
}
return
}

@ -26,10 +26,10 @@ extension RevokeSubaccountResponse: ValidatableResponse {
let encodedSignature: Data = Data(base64Encoded: signatureBase64)
else {
if let reason: String = next.value.reason, let statusCode: Int = next.value.code {
SNLog("Couldn't revoke subaccount from: \(next.key) due to error: \(reason) (\(statusCode)).")
Log.warn(.validator(self), "Couldn't revoke subaccount from: \(next.key) due to error: \(reason) (\(statusCode)).")
}
else {
SNLog("Couldn't revoke subaccount from: \(next.key).")
Log.warn(.validator(self), "Couldn't revoke subaccount from: \(next.key).")
}
return
}

@ -105,10 +105,10 @@ extension SendMessagesResponse: ValidatableResponse {
result[next.key] = false
if let reason: String = next.value.reason, let statusCode: Int = next.value.code {
SNLog("Couldn't store message on: \(next.key) due to error: \(reason) (\(statusCode)).")
Log.warn(.validator(self), "Couldn't store message on: \(next.key) due to error: \(reason) (\(statusCode)).")
}
else {
SNLog("Couldn't store message on: \(next.key).")
Log.warn(.validator(self), "Couldn't store message on: \(next.key).")
}
return
}

@ -22,7 +22,7 @@ public struct SnodeReceivedMessage: CustomDebugStringConvertible {
rawMessage: GetMessagesResponse.RawMessage
) {
guard let data: Data = Data(base64Encoded: rawMessage.base64EncodedDataString) else {
SNLog("Failed to decode data for message: \(rawMessage).")
Log.error(.network, "Failed to decode data for message: \(rawMessage).")
return nil
}

@ -26,10 +26,10 @@ extension UnrevokeSubaccountResponse: ValidatableResponse {
let encodedSignature: Data = Data(base64Encoded: signatureBase64)
else {
if let reason: String = next.value.reason, let statusCode: Int = next.value.code {
SNLog("Couldn't revoke subaccount from: \(next.key) due to error: \(reason) (\(statusCode)).")
Log.warn(.validator(self), "Couldn't revoke subaccount from: \(next.key) due to error: \(reason) (\(statusCode)).")
}
else {
SNLog("Couldn't revoke subaccount from: \(next.key).")
Log.warn(.validator(self), "Couldn't revoke subaccount from: \(next.key).")
}
return
}

@ -70,10 +70,10 @@ extension UpdateExpiryAllResponse: ValidatableResponse {
result[next.key] = []
if let reason: String = next.value.reason, let statusCode: Int = next.value.code {
SNLog("Couldn't update expiry from: \(next.key) due to error: \(reason) (\(statusCode)).")
Log.warn(.validator(self), "Couldn't update expiry from: \(next.key) due to error: \(reason) (\(statusCode)).")
}
else {
SNLog("Couldn't update expiry from: \(next.key).")
Log.warn(.validator(self), "Couldn't update expiry from: \(next.key).")
}
return
}

@ -72,10 +72,10 @@ extension UpdateExpiryResponse: ValidatableResponse {
result[next.key] = UpdateExpiryResponseResult(changed: [:], unchanged: [:], didError: true)
if let reason: String = next.value.reason, let statusCode: Int = next.value.code {
SNLog("Couldn't update expiry from: \(next.key) due to error: \(reason) (\(statusCode)).")
Log.warn(.validator(self), "Couldn't update expiry from: \(next.key) due to error: \(reason) (\(statusCode)).")
}
else {
SNLog("Couldn't update expiry from: \(next.key).")
Log.warn(.validator(self), "Couldn't update expiry from: \(next.key).")
}
return
}

File diff suppressed because it is too large Load Diff

@ -5,6 +5,7 @@
import Foundation
import Combine
import GRDB
import Punycode
import SessionUtilitiesKit
public final class SnodeAPI {
@ -226,7 +227,7 @@ public final class SnodeAPI {
let validationCount = 3
// The name must be lowercased
let onsName = onsName.lowercased()
let onsName = onsName.lowercased().idnaEncoded ?? onsName.lowercased()
// Hash the ONS name using BLAKE2b
guard

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save